mirror of
https://github.com/tiennm99/thptqg2017.git
synced 2026-09-18 10:23:46 +00:00
The repo held two near-duplicate projects. 2016/ and 2017/ each carried their
own React frontend, their own copy of the same Rust crate, and their own
package manager setup. 2016/tools/sync-from-thptqg2017.sh existed purely to
copy the parser source between them.
New layout:
index.html + src/ the 2017 frontend, now the only one
data/<id>/ 2016, 2017, 2017-old, 2017-old2
parser/ the single Rust crate, configs renamed to <id>.toml
docs/ both projects' docs, 2016 copies suffixed -2016-legacy
pending the merge pass
<id> is now one identifier end to end: data/<id>/ feeds parser/configs/<id>.toml
and produces db/<id>.db.gz.
pnpm gives way to npm. pnpm-workspace.yaml existed only to whitelist
better-sqlite3's native build, which npm permits by default, so it has no
equivalent and is simply gone. Lockfiles cannot be converted; package-lock.json
is generated fresh. The migration direction is safe — pnpm's strict layout
forbids phantom dependencies, so anything that resolved under pnpm resolves
under npm's flat tree.
Adds parser/scripts/build-db.js and src/datasets.js: the four dataset IDs are
declared once and read by both the build tooling and (from the next phase) the
frontend.
Follow-on fixes the move made necessary:
- eslint's Node-globals override pointed at scripts/, now parser/scripts/
- crawl-baotintuc.js wrote to <root>/data, now data/2017
- golden tests loaded configs by their old thptqg*-data.toml names
Drops the #[ignore]d Rust-vs-Node golden test. It shelled out to pnpm to run
scripts/build-database.js, a file removed when the parser was ported to Rust,
so it could never pass. check-duplicates.js and diff-datasets.js were already
broken before this change and are annotated as such rather than half-fixed.
63 Rust tests pass and clippy is clean from the new location.
80 lines
2.4 KiB
JavaScript
80 lines
2.4 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Dump per-dataset statistics from built SQLite databases as JSON.
|
|
*
|
|
* Used twice: once against the pre-refactor databases to capture a baseline,
|
|
* and again after the schema unification. Comparing the two outputs is what
|
|
* proves no score data was lost or invented.
|
|
*
|
|
* Schema-agnostic on purpose — columns come from PRAGMA table_info, so the same
|
|
* script runs against the old 18/20-column tables and the new 22-column one.
|
|
*
|
|
* Usage:
|
|
* node db-stats.js <label>=<db-path> [<label>=<db-path> ...] > stats.json
|
|
*/
|
|
|
|
import { DatabaseSync } from "node:sqlite";
|
|
import { statSync } from "node:fs";
|
|
|
|
// Deterministic value-level sample: every SBD ending in these digits.
|
|
// Re-running against a rebuilt DB compares the same students.
|
|
const SAMPLE_SUFFIX = "0000";
|
|
|
|
function collect(dbPath) {
|
|
const db = new DatabaseSync(dbPath, { readOnly: true });
|
|
|
|
const columns = db
|
|
.prepare("PRAGMA table_info(student)")
|
|
.all()
|
|
.map((c) => c.name);
|
|
|
|
const rowCount = db.prepare("SELECT COUNT(*) AS c FROM student").get().c;
|
|
|
|
// One pass over the table counting non-NULLs for every column at once.
|
|
const sums = columns
|
|
.map((c) => `SUM(CASE WHEN "${c}" IS NOT NULL THEN 1 ELSE 0 END) AS "${c}"`)
|
|
.join(", ");
|
|
const nonNull = db.prepare(`SELECT ${sums} FROM student`).get();
|
|
|
|
const sample = db
|
|
.prepare(
|
|
`SELECT * FROM student WHERE so_bao_danh LIKE '%${SAMPLE_SUFFIX}'
|
|
ORDER BY so_bao_danh`,
|
|
)
|
|
.all();
|
|
|
|
db.close();
|
|
|
|
return {
|
|
rowCount,
|
|
columns,
|
|
nonNull: Object.fromEntries(columns.map((c) => [c, Number(nonNull[c])])),
|
|
sizeBytes: statSync(dbPath).size,
|
|
sampleCount: sample.length,
|
|
// Store the sample keyed by SBD so a later diff can report which student
|
|
// and which field changed, not just that something did.
|
|
sample: Object.fromEntries(sample.map((r) => [r.so_bao_danh, r])),
|
|
};
|
|
}
|
|
|
|
const args = process.argv.slice(2);
|
|
if (args.length === 0) {
|
|
console.error("usage: db-stats.js <label>=<db-path> [...]");
|
|
process.exit(2);
|
|
}
|
|
|
|
const out = {};
|
|
for (const arg of args) {
|
|
const idx = arg.indexOf("=");
|
|
if (idx === -1) {
|
|
console.error(`bad argument (expected label=path): ${arg}`);
|
|
process.exit(2);
|
|
}
|
|
const label = arg.slice(0, idx);
|
|
const path = arg.slice(idx + 1);
|
|
process.stderr.write(`collecting ${label} from ${path}\n`);
|
|
out[label] = collect(path);
|
|
}
|
|
|
|
process.stdout.write(JSON.stringify(out, null, 2) + "\n");
|