mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 01:21:50 +08:00
157 lines
4.4 KiB
JavaScript
157 lines
4.4 KiB
JavaScript
#!/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 };
|