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.
121 lines
4.3 KiB
JavaScript
121 lines
4.3 KiB
JavaScript
// Compare two builds of the 2017 database, column by column.
|
|
//
|
|
// BROKEN as committed, for two reasons that both predate the repo unification:
|
|
// - `better-sqlite3` is imported but declared in no package.json
|
|
// - the public/ and backup/ paths it reads no longer exist
|
|
//
|
|
// Left unchanged rather than half-fixed. For a working comparison of two
|
|
// builds use db-stats.js + verify-parity.js, which need no dependencies.
|
|
import Database from "better-sqlite3";
|
|
|
|
const NEW = new Database("public/thptqg2017.db", { readonly: true });
|
|
const OLD = new Database("backup/thptqg2017.old.db", { readonly: true });
|
|
|
|
const SCORE_COLS = [
|
|
"toan", "ngu_van", "vat_ly", "hoa_hoc", "sinh_hoc", "khtn",
|
|
"lich_su", "dia_ly", "gdcd", "khxh",
|
|
"tieng_anh", "tieng_phap", "tieng_nga", "tieng_trung",
|
|
];
|
|
|
|
// Old schema may lack some newer columns — detect and project a subset
|
|
const oldCols = new Set(
|
|
OLD.prepare("PRAGMA table_info(student)").all().map((c) => c.name),
|
|
);
|
|
const commonCols = SCORE_COLS.filter((c) => oldCols.has(c));
|
|
|
|
const newCount = NEW.prepare("SELECT COUNT(*) c FROM student").get().c;
|
|
const oldCount = OLD.prepare("SELECT COUNT(*) c FROM student").get().c;
|
|
console.log(`\n=== Row counts ===`);
|
|
console.log(` new: ${newCount}`);
|
|
console.log(` old: ${oldCount}`);
|
|
console.log(` diff: ${newCount - oldCount >= 0 ? "+" : ""}${newCount - oldCount}`);
|
|
|
|
// Build SBD sets (memory: 861k strings ~50 MB, fine)
|
|
const newSbd = new Set(
|
|
NEW.prepare("SELECT so_bao_danh FROM student").all().map((r) => r.so_bao_danh),
|
|
);
|
|
const oldSbd = new Set(
|
|
OLD.prepare("SELECT so_bao_danh FROM student").all().map((r) => r.so_bao_danh),
|
|
);
|
|
|
|
let newOnly = 0, oldOnly = 0, common = 0;
|
|
for (const s of newSbd) (oldSbd.has(s) ? common++ : newOnly++);
|
|
for (const s of oldSbd) if (!newSbd.has(s)) oldOnly++;
|
|
|
|
console.log(`\n=== SBD membership ===`);
|
|
console.log(` new-only (added): ${newOnly}`);
|
|
console.log(` old-only (removed): ${oldOnly}`);
|
|
console.log(` common: ${common}`);
|
|
|
|
// Sample 5 new-only SBDs to peek
|
|
const sampleNewOnly = [];
|
|
for (const s of newSbd) {
|
|
if (!oldSbd.has(s)) sampleNewOnly.push(s);
|
|
if (sampleNewOnly.length >= 5) break;
|
|
}
|
|
if (sampleNewOnly.length) {
|
|
const rows = NEW.prepare(
|
|
`SELECT so_bao_danh, ho_ten FROM student WHERE so_bao_danh IN (${sampleNewOnly.map(() => "?").join(",")})`,
|
|
).all(...sampleNewOnly);
|
|
console.log(` sample new-only rows:`);
|
|
rows.forEach((r) => console.log(` ${r.so_bao_danh} ${r.ho_ten}`));
|
|
}
|
|
|
|
// Score comparison on common SBDs
|
|
console.log(`\n=== Score comparison (common SBDs) ===`);
|
|
console.log(` columns compared: ${commonCols.join(", ")}`);
|
|
|
|
const newStmt = NEW.prepare(
|
|
`SELECT so_bao_danh, ${commonCols.join(",")} FROM student`,
|
|
);
|
|
const oldStmt = OLD.prepare(
|
|
`SELECT so_bao_danh, ${commonCols.join(",")} FROM student`,
|
|
);
|
|
|
|
// Build old map
|
|
const oldMap = new Map();
|
|
for (const r of oldStmt.iterate()) oldMap.set(r.so_bao_danh, r);
|
|
|
|
let identical = 0,
|
|
differ = 0;
|
|
const colChanges = Object.fromEntries(commonCols.map((c) => [c, { changed: 0, newNull: 0, oldNull: 0 }]));
|
|
const sampleDiffs = [];
|
|
|
|
for (const nr of newStmt.iterate()) {
|
|
const or = oldMap.get(nr.so_bao_danh);
|
|
if (!or) continue;
|
|
let rowDiffers = false;
|
|
const rowChanges = [];
|
|
for (const c of commonCols) {
|
|
const a = nr[c], b = or[c];
|
|
if (a === b) continue;
|
|
// treat very-close floats as equal (xlsx may emit 5.5 vs 5.50)
|
|
if (a !== null && b !== null && Math.abs(a - b) < 1e-9) continue;
|
|
rowDiffers = true;
|
|
colChanges[c].changed++;
|
|
if (a === null) colChanges[c].newNull++;
|
|
if (b === null) colChanges[c].oldNull++;
|
|
rowChanges.push(`${c}: ${b} → ${a}`);
|
|
}
|
|
if (rowDiffers) {
|
|
differ++;
|
|
if (sampleDiffs.length < 5) sampleDiffs.push({ sbd: nr.so_bao_danh, changes: rowChanges });
|
|
} else {
|
|
identical++;
|
|
}
|
|
}
|
|
|
|
console.log(` identical: ${identical}`);
|
|
console.log(` differing: ${differ}`);
|
|
console.log(` per-column changes:`);
|
|
for (const [c, info] of Object.entries(colChanges)) {
|
|
if (info.changed === 0) continue;
|
|
console.log(` ${c.padEnd(14)} changed=${info.changed} (new-null=${info.newNull}, old-null=${info.oldNull})`);
|
|
}
|
|
if (sampleDiffs.length) {
|
|
console.log(` sample diffs:`);
|
|
sampleDiffs.forEach((d) => {
|
|
console.log(` SBD ${d.sbd}: ${d.changes.slice(0, 4).join("; ")}${d.changes.length > 4 ? " ..." : ""}`);
|
|
});
|
|
}
|