feat(ql3): establish 3.0 incubation baseline

This commit is contained in:
whyour
2026-08-12 00:25:26 +08:00
parent 4bf92dcfeb
commit c699c32461
2817 changed files with 779642 additions and 653 deletions
+156
View File
@@ -0,0 +1,156 @@
#!/usr/bin/env node
require('ts-node/register/transpile-only');
const fs = require('node:fs');
const path = require('node:path');
const { auditSqliteSchema } = require('../back/migrations/schemaAudit');
function parseArguments(argv) {
const options = {
database: undefined,
failOnDrift: false,
json: false,
};
const seen = new Set();
for (const argument of argv) {
if (argument === '--') continue;
if (argument === '--json') {
if (seen.has('json')) throw new Error('--json must not be duplicated');
seen.add('json');
options.json = true;
} else if (argument === '--fail-on-drift') {
if (seen.has('fail-on-drift')) {
throw new Error('--fail-on-drift must not be duplicated');
}
seen.add('fail-on-drift');
options.failOnDrift = true;
} else if (argument.startsWith('--database=')) {
if (seen.has('database')) {
throw new Error('--database must not be duplicated');
}
const database = argument.slice('--database='.length);
if (!database) throw new Error('--database must not be empty');
seen.add('database');
options.database = path.resolve(database);
} else {
throw new Error(`Unsupported argument: ${argument}`);
}
}
if (!options.database) {
throw new Error('Legacy schema audit requires an explicit --database path');
}
return options;
}
function collectSnapshot(database) {
const tableRows = database
.prepare(
"SELECT name FROM sqlite_master WHERE type = 'table' ORDER BY name",
)
.all();
const indexRows = database
.prepare(
"SELECT tbl_name AS table_name, name FROM sqlite_master WHERE type = 'index' ORDER BY name",
)
.all();
const indexesByTable = new Map();
for (const row of indexRows) {
const indexes = indexesByTable.get(row.table_name) || [];
indexes.push(row.name);
indexesByTable.set(row.table_name, indexes);
}
const tableInfo = database.prepare(
'SELECT name FROM pragma_table_info(?) ORDER BY cid',
);
const tables = tableRows.map((row) => ({
name: row.name,
columns: tableInfo.all(row.name).map((column) => column.name),
indexes: indexesByTable.get(row.name) || [],
}));
const migrationIds = tableRows.some(
(table) => table.name === 'SchemaMigrations',
)
? database
.prepare('SELECT id FROM SchemaMigrations ORDER BY id')
.all()
.map((migration) => migration.id)
: [];
return { tables, migrationIds };
}
function renderText(report, databasePath) {
const lines = [
`QingLong 3.0 legacy/Shadow schema audit: ${path.basename(databasePath)}`,
`compatible: ${report.compatible}`,
`drift detected: ${report.driftDetected}`,
];
for (const key of [
'missingTables',
'missingColumns',
'missingIndexes',
'missingMigrationIds',
'unknownTables',
'unknownColumns',
'unknownIndexes',
'extraMigrationIds',
]) {
if (report[key].length > 0) {
lines.push(`${key}: ${JSON.stringify(report[key])}`);
}
}
return `${lines.join('\n')}\n`;
}
function main() {
const options = parseArguments(process.argv.slice(2));
const nodeMajor = Number(process.versions.node.split('.')[0]);
if (nodeMajor < 24) {
throw new Error('ql3-schema-audit requires Node.js 24 or newer');
}
if (!fs.existsSync(options.database)) {
throw new Error(`Database does not exist: ${options.database}`);
}
const { DatabaseSync } = require('node:sqlite');
const database = new DatabaseSync(options.database, {
allowExtension: false,
allowUnknownNamedParameters: false,
defensive: true,
enableDoubleQuotedStringLiterals: false,
enableForeignKeyConstraints: true,
readOnly: true,
timeout: 1_000,
});
let report;
try {
report = auditSqliteSchema(collectSnapshot(database));
} finally {
database.close();
}
process.stdout.write(
options.json
? `${JSON.stringify({
schemaVersion: 1,
mode: 'legacy-shadow',
database: path.basename(options.database),
...report,
})}\n`
: renderText(report, options.database),
);
if (!report.compatible || (options.failOnDrift && report.driftDetected)) {
process.exitCode = 1;
}
}
if (require.main === module) {
try {
main();
} catch (error) {
process.stderr.write(
`${error instanceof Error ? error.message : String(error)}\n`,
);
process.exitCode = 1;
}
}
module.exports = { collectSnapshot, parseArguments, renderText };