latticesql 1.3.1 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -12
- package/dist/cli.js +374 -26
- package/dist/index.cjs +365 -25
- package/dist/index.d.cts +33 -1
- package/dist/index.d.ts +33 -1
- package/dist/index.js +374 -26
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -321,7 +321,15 @@ import { join as join3 } from "path";
|
|
|
321
321
|
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
322
322
|
|
|
323
323
|
// src/render/writer.ts
|
|
324
|
-
import {
|
|
324
|
+
import {
|
|
325
|
+
writeFileSync as writeFileSync2,
|
|
326
|
+
mkdirSync as mkdirSync2,
|
|
327
|
+
renameSync,
|
|
328
|
+
copyFileSync,
|
|
329
|
+
unlinkSync,
|
|
330
|
+
existsSync as existsSync2,
|
|
331
|
+
readFileSync as readFileSync2
|
|
332
|
+
} from "fs";
|
|
325
333
|
import { createHash } from "crypto";
|
|
326
334
|
import { dirname as dirname3, join as join2 } from "path";
|
|
327
335
|
import { tmpdir } from "os";
|
|
@@ -389,15 +397,21 @@ function writeManifest(outputDir, manifest) {
|
|
|
389
397
|
|
|
390
398
|
// src/db/sqlite.ts
|
|
391
399
|
import Database from "better-sqlite3";
|
|
400
|
+
var DDL_RE = /^\s*(CREATE|ALTER|DROP|PRAGMA)\b/i;
|
|
401
|
+
var DEFAULT_CACHE_MAX = 500;
|
|
392
402
|
var SQLiteAdapter = class {
|
|
393
403
|
_db = null;
|
|
394
404
|
_path;
|
|
395
405
|
_wal;
|
|
396
406
|
_busyTimeout;
|
|
407
|
+
/** Cached prepared statements keyed by SQL string. */
|
|
408
|
+
_stmtCache = /* @__PURE__ */ new Map();
|
|
409
|
+
_cacheMax;
|
|
397
410
|
constructor(path, options) {
|
|
398
411
|
this._path = path;
|
|
399
412
|
this._wal = options?.wal ?? true;
|
|
400
413
|
this._busyTimeout = options?.busyTimeout ?? 5e3;
|
|
414
|
+
this._cacheMax = options?.cacheMax ?? DEFAULT_CACHE_MAX;
|
|
401
415
|
}
|
|
402
416
|
get db() {
|
|
403
417
|
if (!this._db) throw new Error("SQLiteAdapter: not open \u2014 call open() first");
|
|
@@ -411,20 +425,46 @@ var SQLiteAdapter = class {
|
|
|
411
425
|
}
|
|
412
426
|
}
|
|
413
427
|
close() {
|
|
428
|
+
this._stmtCache.clear();
|
|
414
429
|
this._db?.close();
|
|
415
430
|
this._db = null;
|
|
416
431
|
}
|
|
432
|
+
/** Clear the prepared-statement cache. Call after DDL changes (schema/migrations). */
|
|
433
|
+
clearStatementCache() {
|
|
434
|
+
this._stmtCache.clear();
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Return a cached prepared statement for the given SQL, or compile and cache a new one.
|
|
438
|
+
* DDL statements bypass the cache entirely.
|
|
439
|
+
*/
|
|
440
|
+
_cachedPrepare(sql) {
|
|
441
|
+
if (DDL_RE.test(sql)) {
|
|
442
|
+
return this.db.prepare(sql);
|
|
443
|
+
}
|
|
444
|
+
let stmt = this._stmtCache.get(sql);
|
|
445
|
+
if (stmt) return stmt;
|
|
446
|
+
stmt = this.db.prepare(sql);
|
|
447
|
+
if (this._stmtCache.size >= this._cacheMax) {
|
|
448
|
+
this._stmtCache.clear();
|
|
449
|
+
}
|
|
450
|
+
this._stmtCache.set(sql, stmt);
|
|
451
|
+
return stmt;
|
|
452
|
+
}
|
|
417
453
|
run(sql, params = []) {
|
|
418
|
-
this.
|
|
454
|
+
this._cachedPrepare(sql).run(...params);
|
|
419
455
|
}
|
|
420
456
|
get(sql, params = []) {
|
|
421
|
-
return this.
|
|
457
|
+
return this._cachedPrepare(sql).get(...params);
|
|
422
458
|
}
|
|
423
459
|
all(sql, params = []) {
|
|
424
|
-
return this.
|
|
460
|
+
return this._cachedPrepare(sql).all(...params);
|
|
461
|
+
}
|
|
462
|
+
/** Execute raw SQL that may contain multiple statements (e.g. migrations). */
|
|
463
|
+
exec(sql) {
|
|
464
|
+
this.db.exec(sql);
|
|
425
465
|
}
|
|
426
466
|
prepare(sql) {
|
|
427
|
-
const stmt = this.
|
|
467
|
+
const stmt = this._cachedPrepare(sql);
|
|
428
468
|
return {
|
|
429
469
|
run: (...params) => stmt.run(...params),
|
|
430
470
|
get: (...params) => stmt.get(...params),
|
|
@@ -518,19 +558,40 @@ var SchemaManager = class {
|
|
|
518
558
|
});
|
|
519
559
|
}
|
|
520
560
|
/** Run explicit versioned migrations in order, idempotently */
|
|
521
|
-
applyMigrations(adapter, migrations) {
|
|
561
|
+
applyMigrations(adapter, migrations, validator) {
|
|
522
562
|
const sorted = [...migrations].sort((a, b) => {
|
|
523
563
|
const va = String(a.version);
|
|
524
564
|
const vb = String(b.version);
|
|
525
565
|
return va.localeCompare(vb, void 0, { numeric: true });
|
|
526
566
|
});
|
|
567
|
+
if (validator) {
|
|
568
|
+
const errors = [];
|
|
569
|
+
for (const m of sorted) {
|
|
570
|
+
const versionStr = String(m.version);
|
|
571
|
+
const exists = adapter.get("SELECT 1 FROM __lattice_migrations WHERE version = ?", [
|
|
572
|
+
versionStr
|
|
573
|
+
]);
|
|
574
|
+
if (!exists) {
|
|
575
|
+
const result = validator(m.sql);
|
|
576
|
+
if (!result.valid) {
|
|
577
|
+
errors.push(
|
|
578
|
+
`Migration ${versionStr}: ${(result.errors ?? ["invalid SQL"]).join("; ")}`
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
if (errors.length > 0) {
|
|
584
|
+
throw new Error(`Migration validation failed:
|
|
585
|
+
${errors.join("\n")}`);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
527
588
|
for (const m of sorted) {
|
|
528
589
|
const versionStr = String(m.version);
|
|
529
590
|
const exists = adapter.get("SELECT 1 FROM __lattice_migrations WHERE version = ?", [
|
|
530
591
|
versionStr
|
|
531
592
|
]);
|
|
532
593
|
if (!exists) {
|
|
533
|
-
adapter.
|
|
594
|
+
adapter.exec(m.sql);
|
|
534
595
|
adapter.run("INSERT INTO __lattice_migrations (version, applied_at) VALUES (?, ?)", [
|
|
535
596
|
versionStr,
|
|
536
597
|
(/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -868,6 +929,223 @@ function truncateContent(content, budget) {
|
|
|
868
929
|
if (budget === void 0 || content.length <= budget) return content;
|
|
869
930
|
return content.slice(0, budget) + "\n\n*[truncated \u2014 context budget exceeded]*";
|
|
870
931
|
}
|
|
932
|
+
var BATCH_CHUNK_SIZE = 500;
|
|
933
|
+
function groupBy(rows, keyCol) {
|
|
934
|
+
const map = /* @__PURE__ */ new Map();
|
|
935
|
+
for (const row of rows) {
|
|
936
|
+
const key = String(row[keyCol] ?? "");
|
|
937
|
+
let arr = map.get(key);
|
|
938
|
+
if (!arr) {
|
|
939
|
+
arr = [];
|
|
940
|
+
map.set(key, arr);
|
|
941
|
+
}
|
|
942
|
+
arr.push(row);
|
|
943
|
+
}
|
|
944
|
+
return map;
|
|
945
|
+
}
|
|
946
|
+
function buildBatchClauses(params, opts, tableAlias) {
|
|
947
|
+
let sql = "";
|
|
948
|
+
const prefix = tableAlias ? `${tableAlias}.` : "";
|
|
949
|
+
for (const f of effectiveFilters(opts)) {
|
|
950
|
+
if (!SAFE_COL_RE.test(f.col)) continue;
|
|
951
|
+
switch (f.op) {
|
|
952
|
+
case "eq":
|
|
953
|
+
sql += ` AND ${prefix}"${f.col}" = ?`;
|
|
954
|
+
params.push(f.val);
|
|
955
|
+
break;
|
|
956
|
+
case "ne":
|
|
957
|
+
sql += ` AND ${prefix}"${f.col}" != ?`;
|
|
958
|
+
params.push(f.val);
|
|
959
|
+
break;
|
|
960
|
+
case "gt":
|
|
961
|
+
sql += ` AND ${prefix}"${f.col}" > ?`;
|
|
962
|
+
params.push(f.val);
|
|
963
|
+
break;
|
|
964
|
+
case "gte":
|
|
965
|
+
sql += ` AND ${prefix}"${f.col}" >= ?`;
|
|
966
|
+
params.push(f.val);
|
|
967
|
+
break;
|
|
968
|
+
case "lt":
|
|
969
|
+
sql += ` AND ${prefix}"${f.col}" < ?`;
|
|
970
|
+
params.push(f.val);
|
|
971
|
+
break;
|
|
972
|
+
case "lte":
|
|
973
|
+
sql += ` AND ${prefix}"${f.col}" <= ?`;
|
|
974
|
+
params.push(f.val);
|
|
975
|
+
break;
|
|
976
|
+
case "like":
|
|
977
|
+
sql += ` AND ${prefix}"${f.col}" LIKE ?`;
|
|
978
|
+
params.push(f.val);
|
|
979
|
+
break;
|
|
980
|
+
case "in": {
|
|
981
|
+
const arr = f.val;
|
|
982
|
+
if (arr.length === 0) {
|
|
983
|
+
sql += " AND 0";
|
|
984
|
+
} else {
|
|
985
|
+
sql += ` AND ${prefix}"${f.col}" IN (${arr.map(() => "?").join(", ")})`;
|
|
986
|
+
params.push(...arr);
|
|
987
|
+
}
|
|
988
|
+
break;
|
|
989
|
+
}
|
|
990
|
+
case "isNull":
|
|
991
|
+
sql += ` AND ${prefix}"${f.col}" IS NULL`;
|
|
992
|
+
break;
|
|
993
|
+
case "isNotNull":
|
|
994
|
+
sql += ` AND ${prefix}"${f.col}" IS NOT NULL`;
|
|
995
|
+
break;
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
if (opts.orderBy) {
|
|
999
|
+
if (typeof opts.orderBy === "string") {
|
|
1000
|
+
if (SAFE_COL_RE.test(opts.orderBy)) {
|
|
1001
|
+
const dir = opts.orderDir === "desc" ? "DESC" : "ASC";
|
|
1002
|
+
sql += ` ORDER BY ${prefix}"${opts.orderBy}" ${dir}`;
|
|
1003
|
+
}
|
|
1004
|
+
} else {
|
|
1005
|
+
const clauses = opts.orderBy.filter((spec) => SAFE_COL_RE.test(spec.col)).map((spec) => `${prefix}"${spec.col}" ${spec.dir === "desc" ? "DESC" : "ASC"}`);
|
|
1006
|
+
if (clauses.length > 0) {
|
|
1007
|
+
sql += ` ORDER BY ${clauses.join(", ")}`;
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
return sql;
|
|
1012
|
+
}
|
|
1013
|
+
function batchQuery(adapter, buildSql, inValues, extraParams) {
|
|
1014
|
+
if (inValues.length === 0) return [];
|
|
1015
|
+
const allRows = [];
|
|
1016
|
+
for (let i = 0; i < inValues.length; i += BATCH_CHUNK_SIZE) {
|
|
1017
|
+
const chunk = inValues.slice(i, i + BATCH_CHUNK_SIZE);
|
|
1018
|
+
const placeholders = chunk.map(() => "?").join(", ");
|
|
1019
|
+
const sql = buildSql(placeholders);
|
|
1020
|
+
allRows.push(...adapter.all(sql, [...chunk, ...extraParams]));
|
|
1021
|
+
}
|
|
1022
|
+
return allRows;
|
|
1023
|
+
}
|
|
1024
|
+
function batchPrefetchEntitySources(files, allEntityRows, entityPk, adapter, protection) {
|
|
1025
|
+
const results = /* @__PURE__ */ new Map();
|
|
1026
|
+
const unbatched = /* @__PURE__ */ new Set();
|
|
1027
|
+
const allPkValues = allEntityRows.map((r) => r[entityPk]);
|
|
1028
|
+
for (const [filename, spec] of Object.entries(files)) {
|
|
1029
|
+
const source = spec.source;
|
|
1030
|
+
if (source.type === "self") {
|
|
1031
|
+
continue;
|
|
1032
|
+
}
|
|
1033
|
+
if (source.type === "custom" || source.type === "enriched") {
|
|
1034
|
+
unbatched.add(filename);
|
|
1035
|
+
continue;
|
|
1036
|
+
}
|
|
1037
|
+
if (source.type === "hasMany") {
|
|
1038
|
+
if (protection?.protectedTables.has(source.table)) {
|
|
1039
|
+
unbatched.add(filename);
|
|
1040
|
+
continue;
|
|
1041
|
+
}
|
|
1042
|
+
const ref = source.references ?? entityPk;
|
|
1043
|
+
const pkValues = allEntityRows.map((r) => r[ref]);
|
|
1044
|
+
const extraParams = [];
|
|
1045
|
+
const clauses = buildBatchClauses(extraParams, source);
|
|
1046
|
+
const rows = batchQuery(
|
|
1047
|
+
adapter,
|
|
1048
|
+
(ph) => `SELECT * FROM "${source.table}" WHERE "${source.foreignKey}" IN (${ph})${clauses}`,
|
|
1049
|
+
pkValues,
|
|
1050
|
+
extraParams
|
|
1051
|
+
);
|
|
1052
|
+
const grouped = groupBy(rows, source.foreignKey);
|
|
1053
|
+
if (source.limit !== void 0 && source.limit > 0) {
|
|
1054
|
+
for (const [key, arr] of grouped) {
|
|
1055
|
+
if (arr.length > source.limit) grouped.set(key, arr.slice(0, source.limit));
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
results.set(filename, grouped);
|
|
1059
|
+
continue;
|
|
1060
|
+
}
|
|
1061
|
+
if (source.type === "manyToMany") {
|
|
1062
|
+
if (protection?.protectedTables.has(source.remoteTable)) {
|
|
1063
|
+
unbatched.add(filename);
|
|
1064
|
+
continue;
|
|
1065
|
+
}
|
|
1066
|
+
const remotePk = source.references ?? "id";
|
|
1067
|
+
const extraParams = [];
|
|
1068
|
+
const clauses = buildBatchClauses(extraParams, source, "r");
|
|
1069
|
+
let selectCols = "r.*";
|
|
1070
|
+
if (source.junctionColumns?.length) {
|
|
1071
|
+
const jCols = source.junctionColumns.map((jc) => {
|
|
1072
|
+
if (typeof jc === "string") {
|
|
1073
|
+
if (!SAFE_COL_RE.test(jc)) return null;
|
|
1074
|
+
return `j."${jc}"`;
|
|
1075
|
+
}
|
|
1076
|
+
if (!SAFE_COL_RE.test(jc.col) || !SAFE_COL_RE.test(jc.as)) return null;
|
|
1077
|
+
return `j."${jc.col}" AS "${jc.as}"`;
|
|
1078
|
+
}).filter(Boolean);
|
|
1079
|
+
if (jCols.length > 0) selectCols += ", " + jCols.join(", ");
|
|
1080
|
+
}
|
|
1081
|
+
const batchKeyCol = "__lattice_batch_key";
|
|
1082
|
+
const rows = batchQuery(
|
|
1083
|
+
adapter,
|
|
1084
|
+
(ph) => `SELECT ${selectCols}, j."${source.localKey}" AS "${batchKeyCol}" FROM "${source.remoteTable}" r JOIN "${source.junctionTable}" j ON j."${source.remoteKey}" = r."${remotePk}" WHERE j."${source.localKey}" IN (${ph})${clauses}`,
|
|
1085
|
+
allPkValues,
|
|
1086
|
+
extraParams
|
|
1087
|
+
);
|
|
1088
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
1089
|
+
for (const row of rows) {
|
|
1090
|
+
const key = String(row[batchKeyCol] ?? "");
|
|
1091
|
+
delete row[batchKeyCol];
|
|
1092
|
+
let arr = grouped.get(key);
|
|
1093
|
+
if (!arr) {
|
|
1094
|
+
arr = [];
|
|
1095
|
+
grouped.set(key, arr);
|
|
1096
|
+
}
|
|
1097
|
+
arr.push(row);
|
|
1098
|
+
}
|
|
1099
|
+
if (source.limit !== void 0 && source.limit > 0) {
|
|
1100
|
+
for (const [key, arr] of grouped) {
|
|
1101
|
+
if (arr.length > source.limit) grouped.set(key, arr.slice(0, source.limit));
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
results.set(filename, grouped);
|
|
1105
|
+
continue;
|
|
1106
|
+
}
|
|
1107
|
+
if (source.type === "belongsTo") {
|
|
1108
|
+
if (protection?.protectedTables.has(source.table)) {
|
|
1109
|
+
unbatched.add(filename);
|
|
1110
|
+
continue;
|
|
1111
|
+
}
|
|
1112
|
+
const fkValues = [
|
|
1113
|
+
...new Set(allEntityRows.map((r) => r[source.foreignKey]).filter((v) => v != null))
|
|
1114
|
+
];
|
|
1115
|
+
if (fkValues.length === 0) {
|
|
1116
|
+
results.set(filename, /* @__PURE__ */ new Map());
|
|
1117
|
+
continue;
|
|
1118
|
+
}
|
|
1119
|
+
const refCol = source.references ?? "id";
|
|
1120
|
+
const extraParams = [];
|
|
1121
|
+
const clauses = buildBatchClauses(extraParams, source);
|
|
1122
|
+
const rows = batchQuery(
|
|
1123
|
+
adapter,
|
|
1124
|
+
(ph) => `SELECT * FROM "${source.table}" WHERE "${refCol}" IN (${ph})${clauses}`,
|
|
1125
|
+
fkValues,
|
|
1126
|
+
extraParams
|
|
1127
|
+
);
|
|
1128
|
+
const lookup = /* @__PURE__ */ new Map();
|
|
1129
|
+
for (const row of rows) {
|
|
1130
|
+
lookup.set(String(row[refCol] ?? ""), row);
|
|
1131
|
+
}
|
|
1132
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
1133
|
+
for (const entityRow of allEntityRows) {
|
|
1134
|
+
const fkVal = entityRow[source.foreignKey];
|
|
1135
|
+
const pkVal = String(entityRow[entityPk] ?? "");
|
|
1136
|
+
if (fkVal == null) {
|
|
1137
|
+
grouped.set(pkVal, []);
|
|
1138
|
+
} else {
|
|
1139
|
+
const related = lookup.get(String(fkVal));
|
|
1140
|
+
grouped.set(pkVal, related ? [related] : []);
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
results.set(filename, grouped);
|
|
1144
|
+
continue;
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
return { results, unbatched };
|
|
1148
|
+
}
|
|
871
1149
|
|
|
872
1150
|
// src/render/markdown.ts
|
|
873
1151
|
function frontmatter(fields) {
|
|
@@ -1168,7 +1446,9 @@ var RenderEngine = class {
|
|
|
1168
1446
|
}
|
|
1169
1447
|
}
|
|
1170
1448
|
if (!def.prioritizeBy) {
|
|
1171
|
-
rows.sort(
|
|
1449
|
+
rows.sort(
|
|
1450
|
+
(a, b) => (b._reward_total ?? 0) - (a._reward_total ?? 0)
|
|
1451
|
+
);
|
|
1172
1452
|
}
|
|
1173
1453
|
}
|
|
1174
1454
|
if (def.enrich) {
|
|
@@ -1270,13 +1550,25 @@ var RenderEngine = class {
|
|
|
1270
1550
|
counters.skipped++;
|
|
1271
1551
|
}
|
|
1272
1552
|
}
|
|
1553
|
+
const protection = protectedTables.size > 0 ? { protectedTables, currentTable: table } : void 0;
|
|
1554
|
+
const mergedFiles = {};
|
|
1555
|
+
for (const [filename, spec] of Object.entries(def.files)) {
|
|
1556
|
+
const mergeDefaults = def.sourceDefaults && spec.source.type !== "self" && spec.source.type !== "custom" && spec.source.type !== "enriched";
|
|
1557
|
+
const source = mergeDefaults ? { ...def.sourceDefaults, ...spec.source } : spec.source;
|
|
1558
|
+
mergedFiles[filename] = { source, limit: spec.budget };
|
|
1559
|
+
}
|
|
1560
|
+
const batch = batchPrefetchEntitySources(
|
|
1561
|
+
mergedFiles,
|
|
1562
|
+
allRows,
|
|
1563
|
+
entityPk,
|
|
1564
|
+
this._adapter,
|
|
1565
|
+
protection
|
|
1566
|
+
);
|
|
1273
1567
|
for (const entityRow of allRows) {
|
|
1274
1568
|
const rawSlug = def.slug(entityRow);
|
|
1275
1569
|
const slug = rawSlug.replace(/[\u00A0\u2000-\u200B\u202F\u205F\u3000]/g, " ").replace(/[\x00-\x1F\x7F]/g, "");
|
|
1276
1570
|
if (/[^a-zA-Z0-9.\-_ @(),#&'+:;!~\[\]]/.test(slug)) {
|
|
1277
|
-
throw new Error(
|
|
1278
|
-
`Invalid slug "${slug}": contains characters outside the allowed set`
|
|
1279
|
-
);
|
|
1571
|
+
throw new Error(`Invalid slug "${slug}": contains characters outside the allowed set`);
|
|
1280
1572
|
}
|
|
1281
1573
|
const entityDir = def.directory ? join5(outputDir, def.directory(entityRow)) : join5(outputDir, directoryRoot, slug);
|
|
1282
1574
|
const resolvedDir = resolve3(entityDir);
|
|
@@ -1303,11 +1595,19 @@ var RenderEngine = class {
|
|
|
1303
1595
|
}
|
|
1304
1596
|
const renderedFiles = /* @__PURE__ */ new Map();
|
|
1305
1597
|
const entityFileHashes = {};
|
|
1306
|
-
const
|
|
1598
|
+
const entityPkVal = String(entityRow[entityPk] ?? "");
|
|
1307
1599
|
for (const [filename, spec] of Object.entries(def.files)) {
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1600
|
+
let rows;
|
|
1601
|
+
if (spec.source.type === "self") {
|
|
1602
|
+
rows = [entityRow];
|
|
1603
|
+
} else if (batch.unbatched.has(filename)) {
|
|
1604
|
+
const source = mergedFiles[filename].source;
|
|
1605
|
+
rows = resolveEntitySource(source, entityRow, entityPk, this._adapter, protection);
|
|
1606
|
+
} else if (batch.results.has(filename)) {
|
|
1607
|
+
rows = batch.results.get(filename).get(entityPkVal) ?? [];
|
|
1608
|
+
} else {
|
|
1609
|
+
rows = [];
|
|
1610
|
+
}
|
|
1311
1611
|
if (spec.omitIfEmpty && rows.length === 0) continue;
|
|
1312
1612
|
const renderFn = compileEntityRender(spec.render);
|
|
1313
1613
|
const content = truncateContent(renderFn(rows), spec.budget);
|
|
@@ -1787,10 +2087,10 @@ async function storeEmbedding(adapter, table, pk, row, config) {
|
|
|
1787
2087
|
);
|
|
1788
2088
|
}
|
|
1789
2089
|
function removeEmbedding(adapter, table, pk) {
|
|
1790
|
-
adapter.run(
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
);
|
|
2090
|
+
adapter.run(`DELETE FROM "${EMBEDDINGS_TABLE}" WHERE "table_name" = ? AND "row_pk" = ?`, [
|
|
2091
|
+
table,
|
|
2092
|
+
pk
|
|
2093
|
+
]);
|
|
1794
2094
|
}
|
|
1795
2095
|
function cosineSimilarity(a, b) {
|
|
1796
2096
|
const len = Math.min(a.length, b.length);
|
|
@@ -1851,6 +2151,10 @@ var Lattice = class {
|
|
|
1851
2151
|
_encryptionKeyRaw;
|
|
1852
2152
|
/** Current task context string for relevance filtering. */
|
|
1853
2153
|
_taskContext = "";
|
|
2154
|
+
/** Per-table write version counter, incremented on every mutation. */
|
|
2155
|
+
_tableWriteVersion = /* @__PURE__ */ new Map();
|
|
2156
|
+
/** Snapshot of write versions at the last successful render. */
|
|
2157
|
+
_lastRenderedVersions = /* @__PURE__ */ new Map();
|
|
1854
2158
|
_auditHandlers = [];
|
|
1855
2159
|
_renderHandlers = [];
|
|
1856
2160
|
_writebackHandlers = [];
|
|
@@ -1944,8 +2248,9 @@ var Lattice = class {
|
|
|
1944
2248
|
this._adapter.open();
|
|
1945
2249
|
this._schema.applySchema(this._adapter);
|
|
1946
2250
|
if (options.migrations?.length) {
|
|
1947
|
-
this._schema.applyMigrations(this._adapter, options.migrations);
|
|
2251
|
+
this._schema.applyMigrations(this._adapter, options.migrations, options.validateMigrationSQL);
|
|
1948
2252
|
}
|
|
2253
|
+
this._adapter.clearStatementCache();
|
|
1949
2254
|
for (const tableName of this._schema.getTables().keys()) {
|
|
1950
2255
|
const rows = this._adapter.all(`PRAGMA table_info("${tableName}")`);
|
|
1951
2256
|
this._columnCache.set(tableName, new Set(rows.map((r) => r.name)));
|
|
@@ -1969,6 +2274,7 @@ var Lattice = class {
|
|
|
1969
2274
|
return Promise.reject(new Error("Lattice: not initialized \u2014 call init() first"));
|
|
1970
2275
|
}
|
|
1971
2276
|
this._schema.applyMigrations(this._adapter, migrations);
|
|
2277
|
+
this._adapter.clearStatementCache();
|
|
1972
2278
|
for (const tableName of this._schema.getTables().keys()) {
|
|
1973
2279
|
const rows = this._adapter.all(`PRAGMA table_info("${tableName}")`);
|
|
1974
2280
|
this._columnCache.set(tableName, new Set(rows.map((r) => r.name)));
|
|
@@ -1993,6 +2299,20 @@ var Lattice = class {
|
|
|
1993
2299
|
this._taskContext = context;
|
|
1994
2300
|
return this;
|
|
1995
2301
|
}
|
|
2302
|
+
/**
|
|
2303
|
+
* Mark one or all tables as dirty so the next render() will run a full cycle.
|
|
2304
|
+
* Use this after writing directly via the `db` escape hatch.
|
|
2305
|
+
*/
|
|
2306
|
+
markDirty(table) {
|
|
2307
|
+
if (table) {
|
|
2308
|
+
this._bumpWriteVersion(table);
|
|
2309
|
+
} else {
|
|
2310
|
+
for (const t of this._schema.getTables().keys()) {
|
|
2311
|
+
this._bumpWriteVersion(t);
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2314
|
+
return this;
|
|
2315
|
+
}
|
|
1996
2316
|
/** Return the current task context string. */
|
|
1997
2317
|
getTaskContext() {
|
|
1998
2318
|
return this._taskContext;
|
|
@@ -2113,6 +2433,7 @@ var Lattice = class {
|
|
|
2113
2433
|
const rawPk = rowWithPk[pkCol];
|
|
2114
2434
|
const pkValue = rawPk != null ? String(rawPk) : "";
|
|
2115
2435
|
this._sanitizer.emitAudit(table, "update", pkValue);
|
|
2436
|
+
this._bumpWriteVersion(table);
|
|
2116
2437
|
return Promise.resolve(pkValue);
|
|
2117
2438
|
}
|
|
2118
2439
|
upsertBy(table, col, val, row) {
|
|
@@ -2289,6 +2610,7 @@ var Lattice = class {
|
|
|
2289
2610
|
AND (deleted_at IS NULL OR deleted_at = '')`,
|
|
2290
2611
|
[sourceFile, ...currentKeys]
|
|
2291
2612
|
);
|
|
2613
|
+
this._bumpWriteVersion(table);
|
|
2292
2614
|
}
|
|
2293
2615
|
return Promise.resolve(count);
|
|
2294
2616
|
}
|
|
@@ -2345,6 +2667,7 @@ var Lattice = class {
|
|
|
2345
2667
|
`${verb} INTO "${junctionTable}" (${colNames}) VALUES (${placeholders})`,
|
|
2346
2668
|
Object.values(filtered)
|
|
2347
2669
|
);
|
|
2670
|
+
this._bumpWriteVersion(junctionTable);
|
|
2348
2671
|
return Promise.resolve();
|
|
2349
2672
|
}
|
|
2350
2673
|
/**
|
|
@@ -2360,6 +2683,7 @@ var Lattice = class {
|
|
|
2360
2683
|
`DELETE FROM "${junctionTable}" WHERE ${where}`,
|
|
2361
2684
|
entries.map(([, v]) => v)
|
|
2362
2685
|
);
|
|
2686
|
+
this._bumpWriteVersion(junctionTable);
|
|
2363
2687
|
return Promise.resolve();
|
|
2364
2688
|
}
|
|
2365
2689
|
// -------------------------------------------------------------------------
|
|
@@ -2563,9 +2887,7 @@ var Lattice = class {
|
|
|
2563
2887
|
if (notInit) return notInit;
|
|
2564
2888
|
const def = this._schema.getTables().get(table);
|
|
2565
2889
|
if (!def?.rewardTracking) {
|
|
2566
|
-
return Promise.reject(
|
|
2567
|
-
new Error(`Table "${table}" does not have rewardTracking enabled`)
|
|
2568
|
-
);
|
|
2890
|
+
return Promise.reject(new Error(`Table "${table}" does not have rewardTracking enabled`));
|
|
2569
2891
|
}
|
|
2570
2892
|
const vals = Object.values(scores);
|
|
2571
2893
|
if (vals.length === 0) return Promise.resolve();
|
|
@@ -2575,6 +2897,7 @@ var Lattice = class {
|
|
|
2575
2897
|
`UPDATE "${table}" SET "_reward_total" = ("_reward_total" * "_reward_count" + ?) / ("_reward_count" + 1), "_reward_count" = "_reward_count" + 1 WHERE ${clause}`,
|
|
2576
2898
|
[avg, ...pkParams]
|
|
2577
2899
|
);
|
|
2900
|
+
this._bumpWriteVersion(table);
|
|
2578
2901
|
return Promise.resolve();
|
|
2579
2902
|
}
|
|
2580
2903
|
// -------------------------------------------------------------------------
|
|
@@ -2594,9 +2917,7 @@ var Lattice = class {
|
|
|
2594
2917
|
if (notInit) return notInit;
|
|
2595
2918
|
const def = this._schema.getTables().get(table);
|
|
2596
2919
|
if (!def?.embeddings) {
|
|
2597
|
-
return Promise.reject(
|
|
2598
|
-
new Error(`Table "${table}" does not have embeddings configured`)
|
|
2599
|
-
);
|
|
2920
|
+
return Promise.reject(new Error(`Table "${table}" does not have embeddings configured`));
|
|
2600
2921
|
}
|
|
2601
2922
|
const pkCol = this._schema.getPrimaryKey(table)[0] ?? "id";
|
|
2602
2923
|
return searchByEmbedding(
|
|
@@ -2683,13 +3004,27 @@ var Lattice = class {
|
|
|
2683
3004
|
const notInit = this._notInitError();
|
|
2684
3005
|
if (notInit) return notInit;
|
|
2685
3006
|
const result = await this._render.render(outputDir);
|
|
3007
|
+
for (const [t, v] of this._tableWriteVersion) {
|
|
3008
|
+
this._lastRenderedVersions.set(t, v);
|
|
3009
|
+
}
|
|
2686
3010
|
for (const h of this._renderHandlers) h(result);
|
|
2687
3011
|
return result;
|
|
2688
3012
|
}
|
|
3013
|
+
/**
|
|
3014
|
+
* Returns `true` when no table has been written to since the last render.
|
|
3015
|
+
* Useful for consumers implementing their own polling/watch loops to skip
|
|
3016
|
+
* redundant render cycles.
|
|
3017
|
+
*/
|
|
3018
|
+
isDirty() {
|
|
3019
|
+
return !this._isClean();
|
|
3020
|
+
}
|
|
2689
3021
|
async sync(outputDir) {
|
|
2690
3022
|
const notInit = this._notInitError();
|
|
2691
3023
|
if (notInit) return notInit;
|
|
2692
3024
|
const renderResult = await this._render.render(outputDir);
|
|
3025
|
+
for (const [t, v] of this._tableWriteVersion) {
|
|
3026
|
+
this._lastRenderedVersions.set(t, v);
|
|
3027
|
+
}
|
|
2693
3028
|
for (const h of this._renderHandlers) h(renderResult);
|
|
2694
3029
|
const writebackProcessed = await this._writeback.process();
|
|
2695
3030
|
return { ...renderResult, writebackProcessed };
|
|
@@ -2850,8 +3185,21 @@ var Lattice = class {
|
|
|
2850
3185
|
}
|
|
2851
3186
|
return { clauses, params };
|
|
2852
3187
|
}
|
|
3188
|
+
/** Increment the write version for a table, marking it dirty for render. */
|
|
3189
|
+
_bumpWriteVersion(table) {
|
|
3190
|
+
this._tableWriteVersion.set(table, (this._tableWriteVersion.get(table) ?? 0) + 1);
|
|
3191
|
+
}
|
|
3192
|
+
/** True when no table has been written to since the last render. */
|
|
3193
|
+
_isClean() {
|
|
3194
|
+
if (this._lastRenderedVersions.size === 0) return false;
|
|
3195
|
+
for (const [table, version] of this._tableWriteVersion) {
|
|
3196
|
+
if (version !== (this._lastRenderedVersions.get(table) ?? -1)) return false;
|
|
3197
|
+
}
|
|
3198
|
+
return true;
|
|
3199
|
+
}
|
|
2853
3200
|
/** Returns a rejected Promise if not initialized; null if ready. */
|
|
2854
3201
|
_fireWriteHooks(table, op, row, pk, changedColumns) {
|
|
3202
|
+
this._bumpWriteVersion(table);
|
|
2855
3203
|
for (const hook of this._writeHooks) {
|
|
2856
3204
|
if (hook.table !== table) continue;
|
|
2857
3205
|
if (!hook.on.includes(op)) continue;
|