sqlite-zod-orm 3.24.0 → 3.26.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/dist/index.js +143 -129
- package/package.json +1 -1
- package/src/context.ts +12 -0
- package/src/crud.ts +17 -15
- package/src/database.ts +125 -107
- package/src/query.ts +6 -5
package/dist/index.js
CHANGED
|
@@ -5220,12 +5220,12 @@ function executeProxyQuery(schemas, callback, executor) {
|
|
|
5220
5220
|
function createQueryBuilder(ctx, entityName, initialCols) {
|
|
5221
5221
|
const schema = ctx.schemas[entityName];
|
|
5222
5222
|
const executor = (sql, params, raw) => {
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
|
|
5226
|
-
|
|
5227
|
-
return rows;
|
|
5228
|
-
|
|
5223
|
+
return ctx._m(`SQL: ${sql.slice(0, 60)}`, () => {
|
|
5224
|
+
const rows = ctx._stmt(sql).all(...params);
|
|
5225
|
+
if (raw)
|
|
5226
|
+
return rows;
|
|
5227
|
+
return rows.map((row) => ctx.attachMethods(entityName, transformFromStorage(row, schema)));
|
|
5228
|
+
});
|
|
5229
5229
|
};
|
|
5230
5230
|
const singleExecutor = (sql, params, raw) => {
|
|
5231
5231
|
const results = executor(sql, params, raw);
|
|
@@ -5269,7 +5269,7 @@ function createQueryBuilder(ctx, entityName, initialCols) {
|
|
|
5269
5269
|
if (belongsTo2) {
|
|
5270
5270
|
const fk = belongsTo2.foreignKey;
|
|
5271
5271
|
const placeholders = parentIds.map(() => "?").join(", ");
|
|
5272
|
-
const childRows = ctx.
|
|
5272
|
+
const childRows = ctx._stmt(`SELECT * FROM ${hasMany.to} WHERE ${fk} IN (${placeholders})`).all(...parentIds);
|
|
5273
5273
|
const groups = new Map;
|
|
5274
5274
|
const childSchema = ctx.schemas[hasMany.to];
|
|
5275
5275
|
for (const rawRow of childRows) {
|
|
@@ -5381,21 +5381,21 @@ function buildWhereClause(conditions, tablePrefix) {
|
|
|
5381
5381
|
|
|
5382
5382
|
// src/crud.ts
|
|
5383
5383
|
function getById(ctx, entityName, id) {
|
|
5384
|
-
const row = ctx.
|
|
5384
|
+
const row = ctx._stmt(`SELECT * FROM "${entityName}" WHERE id = ?`).get(id);
|
|
5385
5385
|
if (!row)
|
|
5386
5386
|
return null;
|
|
5387
5387
|
return ctx.attachMethods(entityName, transformFromStorage(row, ctx.schemas[entityName]));
|
|
5388
5388
|
}
|
|
5389
5389
|
function getOne(ctx, entityName, conditions) {
|
|
5390
5390
|
const { clause, values } = ctx.buildWhereClause(conditions);
|
|
5391
|
-
const row = ctx.
|
|
5391
|
+
const row = ctx._stmt(`SELECT * FROM "${entityName}" ${clause} LIMIT 1`).get(...values);
|
|
5392
5392
|
if (!row)
|
|
5393
5393
|
return null;
|
|
5394
5394
|
return ctx.attachMethods(entityName, transformFromStorage(row, ctx.schemas[entityName]));
|
|
5395
5395
|
}
|
|
5396
5396
|
function findMany(ctx, entityName, conditions = {}) {
|
|
5397
5397
|
const { clause, values } = ctx.buildWhereClause(conditions);
|
|
5398
|
-
const rows = ctx.
|
|
5398
|
+
const rows = ctx._stmt(`SELECT * FROM "${entityName}" ${clause}`).all(...values);
|
|
5399
5399
|
return rows.map((row) => ctx.attachMethods(entityName, transformFromStorage(row, ctx.schemas[entityName])));
|
|
5400
5400
|
}
|
|
5401
5401
|
function insert(ctx, entityName, data) {
|
|
@@ -5403,9 +5403,9 @@ function insert(ctx, entityName, data) {
|
|
|
5403
5403
|
let inputData = { ...data };
|
|
5404
5404
|
const hooks = ctx.hooks[entityName];
|
|
5405
5405
|
if (hooks?.beforeInsert) {
|
|
5406
|
-
const
|
|
5407
|
-
if (
|
|
5408
|
-
inputData =
|
|
5406
|
+
const result = hooks.beforeInsert(inputData);
|
|
5407
|
+
if (result)
|
|
5408
|
+
inputData = result;
|
|
5409
5409
|
}
|
|
5410
5410
|
const validatedData = asZodObject(schema).passthrough().parse(inputData);
|
|
5411
5411
|
const transformed = transformForStorage(validatedData);
|
|
@@ -5417,10 +5417,12 @@ function insert(ctx, entityName, data) {
|
|
|
5417
5417
|
const columns = Object.keys(transformed);
|
|
5418
5418
|
const quotedCols = columns.map((c) => `"${c}"`);
|
|
5419
5419
|
const sql = columns.length === 0 ? `INSERT INTO "${entityName}" DEFAULT VALUES` : `INSERT INTO "${entityName}" (${quotedCols.join(", ")}) VALUES (${columns.map(() => "?").join(", ")})`;
|
|
5420
|
-
|
|
5421
|
-
|
|
5422
|
-
|
|
5423
|
-
|
|
5420
|
+
let lastId = 0;
|
|
5421
|
+
ctx._m(`SQL: ${sql.slice(0, 40)}`, () => {
|
|
5422
|
+
const result = ctx._stmt(sql).run(...Object.values(transformed));
|
|
5423
|
+
lastId = result.lastInsertRowid;
|
|
5424
|
+
});
|
|
5425
|
+
const newEntity = getById(ctx, entityName, lastId);
|
|
5424
5426
|
if (!newEntity)
|
|
5425
5427
|
throw new Error("Failed to retrieve entity after insertion");
|
|
5426
5428
|
if (hooks?.afterInsert)
|
|
@@ -5445,9 +5447,9 @@ function update(ctx, entityName, id, data) {
|
|
|
5445
5447
|
}
|
|
5446
5448
|
const setClause = Object.keys(transformed).map((key) => `"${key}" = ?`).join(", ");
|
|
5447
5449
|
const sql = `UPDATE "${entityName}" SET ${setClause} WHERE id = ?`;
|
|
5448
|
-
|
|
5449
|
-
|
|
5450
|
-
|
|
5450
|
+
ctx._m(`SQL: UPDATE ${entityName} SET ...`, () => {
|
|
5451
|
+
ctx._stmt(sql).run(...Object.values(transformed), id);
|
|
5452
|
+
});
|
|
5451
5453
|
const updated = getById(ctx, entityName, id);
|
|
5452
5454
|
if (hooks?.afterUpdate && updated)
|
|
5453
5455
|
hooks.afterUpdate(updated);
|
|
@@ -5464,7 +5466,7 @@ function updateWhere(ctx, entityName, data, conditions) {
|
|
|
5464
5466
|
throw new Error("update().where() requires at least one condition");
|
|
5465
5467
|
const setCols = Object.keys(transformed);
|
|
5466
5468
|
const setClause = setCols.map((key) => `"${key}" = ?`).join(", ");
|
|
5467
|
-
const result = ctx.
|
|
5469
|
+
const result = ctx._stmt(`UPDATE "${entityName}" SET ${setClause} ${clause}`).run(...setCols.map((key) => transformed[key]), ...whereValues);
|
|
5468
5470
|
return result.changes ?? 0;
|
|
5469
5471
|
}
|
|
5470
5472
|
function createUpdateBuilder(ctx, entityName, data) {
|
|
@@ -5505,7 +5507,7 @@ function deleteEntity(ctx, entityName, id) {
|
|
|
5505
5507
|
if (result === false)
|
|
5506
5508
|
return;
|
|
5507
5509
|
}
|
|
5508
|
-
ctx.
|
|
5510
|
+
ctx._stmt(`DELETE FROM "${entityName}" WHERE id = ?`).run(id);
|
|
5509
5511
|
if (hooks?.afterDelete)
|
|
5510
5512
|
hooks.afterDelete(id);
|
|
5511
5513
|
}
|
|
@@ -5516,15 +5518,11 @@ function deleteWhere(ctx, entityName, conditions) {
|
|
|
5516
5518
|
if (ctx.softDeletes) {
|
|
5517
5519
|
const now = new Date().toISOString();
|
|
5518
5520
|
const sql2 = `UPDATE "${entityName}" SET "deletedAt" = ? ${clause}`;
|
|
5519
|
-
|
|
5520
|
-
console.log("[satidb]", sql2, [now, ...values]);
|
|
5521
|
-
const result2 = ctx.db.query(sql2).run(now, ...values);
|
|
5521
|
+
const result2 = ctx._m(`SQL: ${sql2.slice(0, 50)}`, () => ctx._stmt(sql2).run(now, ...values));
|
|
5522
5522
|
return result2.changes ?? 0;
|
|
5523
5523
|
}
|
|
5524
5524
|
const sql = `DELETE FROM "${entityName}" ${clause}`;
|
|
5525
|
-
|
|
5526
|
-
console.log("[satidb]", sql, values);
|
|
5527
|
-
const result = ctx.db.query(sql).run(...values);
|
|
5525
|
+
const result = ctx._m(`SQL: ${sql.slice(0, 50)}`, () => ctx._stmt(sql).run(...values));
|
|
5528
5526
|
return result.changes ?? 0;
|
|
5529
5527
|
}
|
|
5530
5528
|
function createDeleteBuilder(ctx, entityName) {
|
|
@@ -5563,7 +5561,7 @@ function insertMany(ctx, entityName, rows) {
|
|
|
5563
5561
|
const columns = Object.keys(transformed);
|
|
5564
5562
|
const quotedCols = columns.map((c) => `"${c}"`);
|
|
5565
5563
|
const sql = columns.length === 0 ? `INSERT INTO "${entityName}" DEFAULT VALUES` : `INSERT INTO "${entityName}" (${quotedCols.join(", ")}) VALUES (${columns.map(() => "?").join(", ")})`;
|
|
5566
|
-
const result = ctx.
|
|
5564
|
+
const result = ctx._stmt(sql).run(...Object.values(transformed));
|
|
5567
5565
|
ids2.push(result.lastInsertRowid);
|
|
5568
5566
|
}
|
|
5569
5567
|
return ids2;
|
|
@@ -5649,6 +5647,15 @@ class _Database {
|
|
|
5649
5647
|
_pollTimer = null;
|
|
5650
5648
|
_pollInterval;
|
|
5651
5649
|
_measure;
|
|
5650
|
+
_stmtCache = new Map;
|
|
5651
|
+
_stmt(sql) {
|
|
5652
|
+
let stmt = this._stmtCache.get(sql);
|
|
5653
|
+
if (!stmt) {
|
|
5654
|
+
stmt = this.db.query(sql);
|
|
5655
|
+
this._stmtCache.set(sql, stmt);
|
|
5656
|
+
}
|
|
5657
|
+
return stmt;
|
|
5658
|
+
}
|
|
5652
5659
|
_m(label, fn) {
|
|
5653
5660
|
if (this._debug)
|
|
5654
5661
|
return this._measure.measureSync.assert(label, fn);
|
|
@@ -5679,7 +5686,9 @@ class _Database {
|
|
|
5679
5686
|
softDeletes: this._softDeletes,
|
|
5680
5687
|
hooks: options.hooks ?? {},
|
|
5681
5688
|
computed: options.computed ?? {},
|
|
5682
|
-
cascade: options.cascade ?? {}
|
|
5689
|
+
cascade: options.cascade ?? {},
|
|
5690
|
+
_m: (label, fn) => this._m(label, fn),
|
|
5691
|
+
_stmt: (sql) => this._stmt(sql)
|
|
5683
5692
|
};
|
|
5684
5693
|
this._m("Init tables", () => this.initializeTables());
|
|
5685
5694
|
if (this._reactive)
|
|
@@ -5696,55 +5705,59 @@ class _Database {
|
|
|
5696
5705
|
insertMany: (rows) => this._m(`${entityName}.insertMany(${rows.length})`, () => insertMany(this._ctx, entityName, rows)),
|
|
5697
5706
|
update: (idOrData, data) => {
|
|
5698
5707
|
if (typeof idOrData === "number")
|
|
5699
|
-
return update(this._ctx, entityName, idOrData, data);
|
|
5708
|
+
return this._m(`${entityName}.update(${idOrData})`, () => update(this._ctx, entityName, idOrData, data));
|
|
5700
5709
|
return createUpdateBuilder(this._ctx, entityName, idOrData);
|
|
5701
5710
|
},
|
|
5702
|
-
upsert: (conditions, data) => upsert(this._ctx, entityName, data, conditions),
|
|
5703
|
-
upsertMany: (rows, conditions) => upsertMany(this._ctx, entityName, rows, conditions),
|
|
5704
|
-
findOrCreate: (conditions, defaults) => findOrCreate(this._ctx, entityName, conditions, defaults),
|
|
5711
|
+
upsert: (conditions, data) => this._m(`${entityName}.upsert`, () => upsert(this._ctx, entityName, data, conditions)),
|
|
5712
|
+
upsertMany: (rows, conditions) => this._m(`${entityName}.upsertMany(${rows.length})`, () => upsertMany(this._ctx, entityName, rows, conditions)),
|
|
5713
|
+
findOrCreate: (conditions, defaults) => this._m(`${entityName}.findOrCreate`, () => findOrCreate(this._ctx, entityName, conditions, defaults)),
|
|
5705
5714
|
delete: (id) => {
|
|
5706
5715
|
if (typeof id === "number") {
|
|
5707
|
-
|
|
5708
|
-
|
|
5709
|
-
|
|
5710
|
-
|
|
5711
|
-
|
|
5712
|
-
|
|
5713
|
-
|
|
5714
|
-
|
|
5715
|
-
|
|
5716
|
-
|
|
5717
|
-
|
|
5718
|
-
if (
|
|
5719
|
-
|
|
5720
|
-
|
|
5721
|
-
|
|
5722
|
-
|
|
5716
|
+
return this._m(`${entityName}.delete(${id})`, () => {
|
|
5717
|
+
const hooks = this._ctx.hooks[entityName];
|
|
5718
|
+
if (hooks?.beforeDelete) {
|
|
5719
|
+
const result = hooks.beforeDelete(id);
|
|
5720
|
+
if (result === false)
|
|
5721
|
+
return;
|
|
5722
|
+
}
|
|
5723
|
+
const cascadeTargets = this._ctx.cascade[entityName];
|
|
5724
|
+
if (cascadeTargets) {
|
|
5725
|
+
for (const childTable of cascadeTargets) {
|
|
5726
|
+
const rel = this._ctx.relationships.find((r) => r.type === "belongs-to" && r.from === childTable && r.to === entityName);
|
|
5727
|
+
if (rel) {
|
|
5728
|
+
if (this._softDeletes) {
|
|
5729
|
+
const now = new Date().toISOString();
|
|
5730
|
+
this._stmt(`UPDATE "${childTable}" SET "deletedAt" = ? WHERE "${rel.foreignKey}" = ?`).run(now, id);
|
|
5731
|
+
} else {
|
|
5732
|
+
this._stmt(`DELETE FROM "${childTable}" WHERE "${rel.foreignKey}" = ?`).run(id);
|
|
5733
|
+
}
|
|
5723
5734
|
}
|
|
5724
5735
|
}
|
|
5725
5736
|
}
|
|
5726
|
-
|
|
5727
|
-
|
|
5728
|
-
|
|
5729
|
-
|
|
5730
|
-
|
|
5731
|
-
|
|
5732
|
-
|
|
5733
|
-
|
|
5734
|
-
|
|
5737
|
+
if (this._softDeletes) {
|
|
5738
|
+
const now = new Date().toISOString();
|
|
5739
|
+
this._stmt(`UPDATE "${entityName}" SET "deletedAt" = ? WHERE id = ?`).run(now, id);
|
|
5740
|
+
if (hooks?.afterDelete)
|
|
5741
|
+
hooks.afterDelete(id);
|
|
5742
|
+
return;
|
|
5743
|
+
}
|
|
5744
|
+
return deleteEntity(this._ctx, entityName, id);
|
|
5745
|
+
});
|
|
5735
5746
|
}
|
|
5736
5747
|
return createDeleteBuilder(this._ctx, entityName);
|
|
5737
5748
|
},
|
|
5738
5749
|
restore: (id) => {
|
|
5739
5750
|
if (!this._softDeletes)
|
|
5740
5751
|
throw new Error("restore() requires softDeletes: true");
|
|
5741
|
-
this.
|
|
5752
|
+
this._m(`${entityName}.restore(${id})`, () => {
|
|
5753
|
+
this._stmt(`UPDATE "${entityName}" SET "deletedAt" = NULL WHERE id = ?`).run(id);
|
|
5754
|
+
});
|
|
5742
5755
|
},
|
|
5743
5756
|
select: (...cols) => createQueryBuilder(this._ctx, entityName, cols),
|
|
5744
|
-
count: () => {
|
|
5745
|
-
const row = this.
|
|
5757
|
+
count: () => this._m(`${entityName}.count`, () => {
|
|
5758
|
+
const row = this._stmt(`SELECT COUNT(*) as count FROM "${entityName}"${this._softDeletes ? ' WHERE "deletedAt" IS NULL' : ""}`).get();
|
|
5746
5759
|
return row?.count ?? 0;
|
|
5747
|
-
},
|
|
5760
|
+
}),
|
|
5748
5761
|
on: (event, callback) => {
|
|
5749
5762
|
return this._registerListener(entityName, event, callback);
|
|
5750
5763
|
},
|
|
@@ -5858,11 +5871,11 @@ class _Database {
|
|
|
5858
5871
|
}
|
|
5859
5872
|
}
|
|
5860
5873
|
_processChanges() {
|
|
5861
|
-
const head = this.
|
|
5874
|
+
const head = this._stmt('SELECT MAX(id) as m FROM "_changes"').get();
|
|
5862
5875
|
const maxId = head?.m ?? 0;
|
|
5863
5876
|
if (maxId <= this._changeWatermark)
|
|
5864
5877
|
return;
|
|
5865
|
-
const changes = this.
|
|
5878
|
+
const changes = this._stmt('SELECT id, tbl, op, row_id FROM "_changes" WHERE id > ? ORDER BY id').all(this._changeWatermark);
|
|
5866
5879
|
for (const change of changes) {
|
|
5867
5880
|
const listeners = this._listeners.filter((l) => l.table === change.tbl && l.event === change.op);
|
|
5868
5881
|
if (listeners.length > 0) {
|
|
@@ -5886,31 +5899,26 @@ class _Database {
|
|
|
5886
5899
|
}
|
|
5887
5900
|
this._changeWatermark = change.id;
|
|
5888
5901
|
}
|
|
5889
|
-
this.
|
|
5902
|
+
this._stmt('DELETE FROM "_changes" WHERE id <= ?').run(this._changeWatermark);
|
|
5890
5903
|
}
|
|
5891
5904
|
transaction(callback) {
|
|
5892
|
-
return this.db.transaction(callback)();
|
|
5905
|
+
return this._m("transaction", () => this.db.transaction(callback)());
|
|
5893
5906
|
}
|
|
5894
5907
|
close() {
|
|
5895
5908
|
this._stopPolling();
|
|
5909
|
+
this._stmtCache.clear();
|
|
5896
5910
|
this.db.close();
|
|
5897
5911
|
}
|
|
5898
5912
|
query(callback) {
|
|
5899
|
-
return executeProxyQuery(this.schemas, callback, (sql, params) => {
|
|
5900
|
-
|
|
5901
|
-
|
|
5902
|
-
return this.db.query(sql).all(...params);
|
|
5903
|
-
});
|
|
5913
|
+
return this._m("query(proxy)", () => executeProxyQuery(this.schemas, callback, (sql, params) => {
|
|
5914
|
+
return this._stmt(sql).all(...params);
|
|
5915
|
+
}));
|
|
5904
5916
|
}
|
|
5905
5917
|
raw(sql, ...params) {
|
|
5906
|
-
|
|
5907
|
-
console.log("[satidb]", sql, params);
|
|
5908
|
-
return this.db.query(sql).all(...params);
|
|
5918
|
+
return this._m(`raw: ${sql.slice(0, 60)}`, () => this._stmt(sql).all(...params));
|
|
5909
5919
|
}
|
|
5910
5920
|
exec(sql, ...params) {
|
|
5911
|
-
|
|
5912
|
-
console.log("[satidb]", sql, params);
|
|
5913
|
-
this.db.run(sql, ...params);
|
|
5921
|
+
this._m(`exec: ${sql.slice(0, 60)}`, () => this.db.run(sql, ...params));
|
|
5914
5922
|
}
|
|
5915
5923
|
tables() {
|
|
5916
5924
|
return Object.keys(this.schemas);
|
|
@@ -5919,70 +5927,76 @@ class _Database {
|
|
|
5919
5927
|
return this.db.query(`PRAGMA table_info("${tableName}")`).all();
|
|
5920
5928
|
}
|
|
5921
5929
|
dump() {
|
|
5922
|
-
|
|
5923
|
-
|
|
5924
|
-
|
|
5925
|
-
|
|
5926
|
-
|
|
5930
|
+
return this._m("dump", () => {
|
|
5931
|
+
const result = {};
|
|
5932
|
+
for (const tableName of Object.keys(this.schemas)) {
|
|
5933
|
+
result[tableName] = this.db.query(`SELECT * FROM "${tableName}"`).all();
|
|
5934
|
+
}
|
|
5935
|
+
return result;
|
|
5936
|
+
});
|
|
5927
5937
|
}
|
|
5928
5938
|
load(data, options) {
|
|
5929
|
-
|
|
5930
|
-
|
|
5931
|
-
|
|
5932
|
-
|
|
5933
|
-
|
|
5934
|
-
|
|
5935
|
-
|
|
5936
|
-
|
|
5937
|
-
const
|
|
5938
|
-
|
|
5939
|
-
|
|
5940
|
-
const
|
|
5941
|
-
|
|
5942
|
-
|
|
5943
|
-
|
|
5944
|
-
|
|
5945
|
-
|
|
5946
|
-
|
|
5939
|
+
this._m(`load(${Object.keys(data).join(",")})`, () => {
|
|
5940
|
+
const txn = this.db.transaction(() => {
|
|
5941
|
+
for (const [tableName, rows] of Object.entries(data)) {
|
|
5942
|
+
if (!this.schemas[tableName])
|
|
5943
|
+
continue;
|
|
5944
|
+
if (!options?.append) {
|
|
5945
|
+
this.db.run(`DELETE FROM "${tableName}"`);
|
|
5946
|
+
}
|
|
5947
|
+
for (const row of rows) {
|
|
5948
|
+
const cols = Object.keys(row).filter((k) => k !== "id");
|
|
5949
|
+
const placeholders = cols.map(() => "?").join(", ");
|
|
5950
|
+
const values = cols.map((c) => {
|
|
5951
|
+
const v = row[c];
|
|
5952
|
+
if (v !== null && v !== undefined && typeof v === "object" && !(v instanceof Buffer)) {
|
|
5953
|
+
return JSON.stringify(v);
|
|
5954
|
+
}
|
|
5955
|
+
return v;
|
|
5956
|
+
});
|
|
5957
|
+
this.db.query(`INSERT INTO "${tableName}" (${cols.map((c) => `"${c}"`).join(", ")}) VALUES (${placeholders})`).run(...values);
|
|
5958
|
+
}
|
|
5947
5959
|
}
|
|
5948
|
-
}
|
|
5960
|
+
});
|
|
5961
|
+
txn();
|
|
5949
5962
|
});
|
|
5950
|
-
txn();
|
|
5951
5963
|
}
|
|
5952
5964
|
seed(fixtures) {
|
|
5953
5965
|
this.load(fixtures, { append: true });
|
|
5954
5966
|
}
|
|
5955
5967
|
diff() {
|
|
5956
|
-
|
|
5957
|
-
|
|
5958
|
-
|
|
5959
|
-
const
|
|
5960
|
-
|
|
5961
|
-
|
|
5962
|
-
|
|
5963
|
-
|
|
5964
|
-
|
|
5965
|
-
|
|
5966
|
-
|
|
5967
|
-
|
|
5968
|
-
|
|
5969
|
-
|
|
5970
|
-
|
|
5971
|
-
|
|
5972
|
-
|
|
5968
|
+
return this._m("diff", () => {
|
|
5969
|
+
const result = {};
|
|
5970
|
+
const systemCols = new Set(["id", "createdAt", "updatedAt", "deletedAt"]);
|
|
5971
|
+
for (const [tableName, schema] of Object.entries(this.schemas)) {
|
|
5972
|
+
const schemaFields = getStorableFields(schema);
|
|
5973
|
+
const schemaColMap = new Map(schemaFields.map((f) => [f.name, zodTypeToSqlType(f.type)]));
|
|
5974
|
+
const liveColumns = this.columns(tableName);
|
|
5975
|
+
const liveColMap = new Map(liveColumns.map((c) => [c.name, c.type]));
|
|
5976
|
+
const added = [];
|
|
5977
|
+
const removed = [];
|
|
5978
|
+
const typeChanged = [];
|
|
5979
|
+
for (const [col, expectedType] of schemaColMap) {
|
|
5980
|
+
if (!liveColMap.has(col)) {
|
|
5981
|
+
added.push(col);
|
|
5982
|
+
} else {
|
|
5983
|
+
const actualType = liveColMap.get(col);
|
|
5984
|
+
if (actualType !== expectedType) {
|
|
5985
|
+
typeChanged.push({ column: col, expected: expectedType, actual: actualType });
|
|
5986
|
+
}
|
|
5973
5987
|
}
|
|
5974
5988
|
}
|
|
5975
|
-
|
|
5976
|
-
|
|
5977
|
-
|
|
5978
|
-
|
|
5989
|
+
for (const col of liveColMap.keys()) {
|
|
5990
|
+
if (!systemCols.has(col) && !schemaColMap.has(col)) {
|
|
5991
|
+
removed.push(col);
|
|
5992
|
+
}
|
|
5993
|
+
}
|
|
5994
|
+
if (added.length > 0 || removed.length > 0 || typeChanged.length > 0) {
|
|
5995
|
+
result[tableName] = { added, removed, typeChanged };
|
|
5979
5996
|
}
|
|
5980
5997
|
}
|
|
5981
|
-
|
|
5982
|
-
|
|
5983
|
-
}
|
|
5984
|
-
}
|
|
5985
|
-
return result;
|
|
5998
|
+
return result;
|
|
5999
|
+
});
|
|
5986
6000
|
}
|
|
5987
6001
|
}
|
|
5988
6002
|
var Database = _Database;
|
package/package.json
CHANGED
package/src/context.ts
CHANGED
|
@@ -40,4 +40,16 @@ export interface DatabaseContext {
|
|
|
40
40
|
|
|
41
41
|
/** Cascade delete config — parent table → list of child tables to auto-delete. */
|
|
42
42
|
cascade: Record<string, string[]>;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Conditional measurement helper — wraps fn with measure-fn when debug is on.
|
|
46
|
+
* When debug is off, executes fn directly with zero overhead.
|
|
47
|
+
*/
|
|
48
|
+
_m<T>(label: string, fn: () => T): T;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Get a cached prepared statement. Compiles SQL once, reuses on subsequent calls.
|
|
52
|
+
* Falls back to `db.query(sql)` if the statement was finalized.
|
|
53
|
+
*/
|
|
54
|
+
_stmt(sql: string): ReturnType<SqliteDatabase['query']>;
|
|
43
55
|
}
|
package/src/crud.ts
CHANGED
|
@@ -14,21 +14,21 @@ import type { DatabaseContext } from './context';
|
|
|
14
14
|
// ---------------------------------------------------------------------------
|
|
15
15
|
|
|
16
16
|
export function getById(ctx: DatabaseContext, entityName: string, id: number): AugmentedEntity<any> | null {
|
|
17
|
-
const row = ctx.
|
|
17
|
+
const row = ctx._stmt(`SELECT * FROM "${entityName}" WHERE id = ?`).get(id) as any;
|
|
18
18
|
if (!row) return null;
|
|
19
19
|
return ctx.attachMethods(entityName, transformFromStorage(row, ctx.schemas[entityName]!));
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
export function getOne(ctx: DatabaseContext, entityName: string, conditions: Record<string, any>): AugmentedEntity<any> | null {
|
|
23
23
|
const { clause, values } = ctx.buildWhereClause(conditions);
|
|
24
|
-
const row = ctx.
|
|
24
|
+
const row = ctx._stmt(`SELECT * FROM "${entityName}" ${clause} LIMIT 1`).get(...values) as any;
|
|
25
25
|
if (!row) return null;
|
|
26
26
|
return ctx.attachMethods(entityName, transformFromStorage(row, ctx.schemas[entityName]!));
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
export function findMany(ctx: DatabaseContext, entityName: string, conditions: Record<string, any> = {}): AugmentedEntity<any>[] {
|
|
30
30
|
const { clause, values } = ctx.buildWhereClause(conditions);
|
|
31
|
-
const rows = ctx.
|
|
31
|
+
const rows = ctx._stmt(`SELECT * FROM "${entityName}" ${clause}`).all(...values);
|
|
32
32
|
return rows.map((row: any) =>
|
|
33
33
|
ctx.attachMethods(entityName, transformFromStorage(row, ctx.schemas[entityName]!))
|
|
34
34
|
);
|
|
@@ -66,9 +66,12 @@ export function insert<T extends Record<string, any>>(ctx: DatabaseContext, enti
|
|
|
66
66
|
? `INSERT INTO "${entityName}" DEFAULT VALUES`
|
|
67
67
|
: `INSERT INTO "${entityName}" (${quotedCols.join(', ')}) VALUES (${columns.map(() => '?').join(', ')})`;
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
69
|
+
let lastId = 0;
|
|
70
|
+
ctx._m(`SQL: ${sql.slice(0, 40)}`, () => {
|
|
71
|
+
const result = ctx._stmt(sql).run(...Object.values(transformed));
|
|
72
|
+
lastId = result.lastInsertRowid as number;
|
|
73
|
+
});
|
|
74
|
+
const newEntity = getById(ctx, entityName, lastId);
|
|
72
75
|
if (!newEntity) throw new Error('Failed to retrieve entity after insertion');
|
|
73
76
|
|
|
74
77
|
// afterInsert hook
|
|
@@ -99,8 +102,9 @@ export function update<T extends Record<string, any>>(ctx: DatabaseContext, enti
|
|
|
99
102
|
|
|
100
103
|
const setClause = Object.keys(transformed).map(key => `"${key}" = ?`).join(', ');
|
|
101
104
|
const sql = `UPDATE "${entityName}" SET ${setClause} WHERE id = ?`;
|
|
102
|
-
|
|
103
|
-
|
|
105
|
+
ctx._m(`SQL: UPDATE ${entityName} SET ...`, () => {
|
|
106
|
+
ctx._stmt(sql).run(...Object.values(transformed), id);
|
|
107
|
+
});
|
|
104
108
|
|
|
105
109
|
const updated = getById(ctx, entityName, id);
|
|
106
110
|
|
|
@@ -121,7 +125,7 @@ export function updateWhere(ctx: DatabaseContext, entityName: string, data: Reco
|
|
|
121
125
|
|
|
122
126
|
const setCols = Object.keys(transformed);
|
|
123
127
|
const setClause = setCols.map(key => `"${key}" = ?`).join(', ');
|
|
124
|
-
const result = ctx.
|
|
128
|
+
const result = ctx._stmt(`UPDATE "${entityName}" SET ${setClause} ${clause}`).run(
|
|
125
129
|
...setCols.map(key => transformed[key]),
|
|
126
130
|
...whereValues
|
|
127
131
|
);
|
|
@@ -177,7 +181,7 @@ export function deleteEntity(ctx: DatabaseContext, entityName: string, id: numbe
|
|
|
177
181
|
if (result === false) return;
|
|
178
182
|
}
|
|
179
183
|
|
|
180
|
-
ctx.
|
|
184
|
+
ctx._stmt(`DELETE FROM "${entityName}" WHERE id = ?`).run(id);
|
|
181
185
|
|
|
182
186
|
// afterDelete hook
|
|
183
187
|
if (hooks?.afterDelete) hooks.afterDelete(id);
|
|
@@ -192,14 +196,12 @@ export function deleteWhere(ctx: DatabaseContext, entityName: string, conditions
|
|
|
192
196
|
// Soft delete: set deletedAt instead of removing rows
|
|
193
197
|
const now = new Date().toISOString();
|
|
194
198
|
const sql = `UPDATE "${entityName}" SET "deletedAt" = ? ${clause}`;
|
|
195
|
-
|
|
196
|
-
const result = ctx.db.query(sql).run(now, ...values);
|
|
199
|
+
const result = ctx._m(`SQL: ${sql.slice(0, 50)}`, () => ctx._stmt(sql).run(now, ...values));
|
|
197
200
|
return (result as any).changes ?? 0;
|
|
198
201
|
}
|
|
199
202
|
|
|
200
203
|
const sql = `DELETE FROM "${entityName}" ${clause}`;
|
|
201
|
-
|
|
202
|
-
const result = ctx.db.query(sql).run(...values);
|
|
204
|
+
const result = ctx._m(`SQL: ${sql.slice(0, 50)}`, () => ctx._stmt(sql).run(...values));
|
|
203
205
|
return (result as any).changes ?? 0;
|
|
204
206
|
}
|
|
205
207
|
|
|
@@ -245,7 +247,7 @@ export function insertMany<T extends Record<string, any>>(ctx: DatabaseContext,
|
|
|
245
247
|
const sql = columns.length === 0
|
|
246
248
|
? `INSERT INTO "${entityName}" DEFAULT VALUES`
|
|
247
249
|
: `INSERT INTO "${entityName}" (${quotedCols.join(', ')}) VALUES (${columns.map(() => '?').join(', ')})`;
|
|
248
|
-
const result = ctx.
|
|
250
|
+
const result = ctx._stmt(sql).run(...Object.values(transformed));
|
|
249
251
|
ids.push(result.lastInsertRowid as number);
|
|
250
252
|
}
|
|
251
253
|
return ids;
|
package/src/database.ts
CHANGED
|
@@ -67,6 +67,19 @@ class _Database<Schemas extends SchemaMap> {
|
|
|
67
67
|
/** Scoped measure-fn instance for instrumentation. */
|
|
68
68
|
private _measure: ReturnType<typeof createMeasure>;
|
|
69
69
|
|
|
70
|
+
/** Prepared statement cache — avoids re-compiling identical SQL. */
|
|
71
|
+
private _stmtCache = new Map<string, ReturnType<SqliteDatabase['query']>>();
|
|
72
|
+
|
|
73
|
+
/** Get or create a cached prepared statement. */
|
|
74
|
+
private _stmt(sql: string): ReturnType<SqliteDatabase['query']> {
|
|
75
|
+
let stmt = this._stmtCache.get(sql);
|
|
76
|
+
if (!stmt) {
|
|
77
|
+
stmt = this.db.query(sql);
|
|
78
|
+
this._stmtCache.set(sql, stmt);
|
|
79
|
+
}
|
|
80
|
+
return stmt;
|
|
81
|
+
}
|
|
82
|
+
|
|
70
83
|
/**
|
|
71
84
|
* Conditional measurement helper — wraps with measure-fn only when debug is on.
|
|
72
85
|
* When debug is off, executes fn directly with zero overhead.
|
|
@@ -104,6 +117,8 @@ class _Database<Schemas extends SchemaMap> {
|
|
|
104
117
|
hooks: options.hooks ?? {},
|
|
105
118
|
computed: options.computed ?? {},
|
|
106
119
|
cascade: options.cascade ?? {},
|
|
120
|
+
_m: <T>(label: string, fn: () => T): T => this._m(label, fn),
|
|
121
|
+
_stmt: (sql: string) => this._stmt(sql),
|
|
107
122
|
};
|
|
108
123
|
|
|
109
124
|
this._m('Init tables', () => this.initializeTables());
|
|
@@ -119,60 +134,62 @@ class _Database<Schemas extends SchemaMap> {
|
|
|
119
134
|
insert: (data) => this._m(`${entityName}.insert`, () => insert(this._ctx, entityName, data)),
|
|
120
135
|
insertMany: (rows: any[]) => this._m(`${entityName}.insertMany(${rows.length})`, () => insertMany(this._ctx, entityName, rows)),
|
|
121
136
|
update: (idOrData: any, data?: any) => {
|
|
122
|
-
if (typeof idOrData === 'number') return update(this._ctx, entityName, idOrData, data);
|
|
137
|
+
if (typeof idOrData === 'number') return this._m(`${entityName}.update(${idOrData})`, () => update(this._ctx, entityName, idOrData, data));
|
|
123
138
|
return createUpdateBuilder(this._ctx, entityName, idOrData);
|
|
124
139
|
},
|
|
125
|
-
upsert: (conditions, data) => upsert(this._ctx, entityName, data, conditions),
|
|
126
|
-
upsertMany: (rows: any[], conditions?: any) => upsertMany(this._ctx, entityName, rows, conditions),
|
|
127
|
-
findOrCreate: (conditions: any, defaults?: any) => findOrCreate(this._ctx, entityName, conditions, defaults),
|
|
140
|
+
upsert: (conditions, data) => this._m(`${entityName}.upsert`, () => upsert(this._ctx, entityName, data, conditions)),
|
|
141
|
+
upsertMany: (rows: any[], conditions?: any) => this._m(`${entityName}.upsertMany(${rows.length})`, () => upsertMany(this._ctx, entityName, rows, conditions)),
|
|
142
|
+
findOrCreate: (conditions: any, defaults?: any) => this._m(`${entityName}.findOrCreate`, () => findOrCreate(this._ctx, entityName, conditions, defaults)),
|
|
128
143
|
delete: ((id?: any) => {
|
|
129
144
|
if (typeof id === 'number') {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
145
|
+
return this._m(`${entityName}.delete(${id})`, () => {
|
|
146
|
+
// beforeDelete hook — return false to cancel
|
|
147
|
+
const hooks = this._ctx.hooks[entityName];
|
|
148
|
+
if (hooks?.beforeDelete) {
|
|
149
|
+
const result = hooks.beforeDelete(id);
|
|
150
|
+
if (result === false) return;
|
|
151
|
+
}
|
|
136
152
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
153
|
+
// Cascade delete children first
|
|
154
|
+
const cascadeTargets = this._ctx.cascade[entityName];
|
|
155
|
+
if (cascadeTargets) {
|
|
156
|
+
for (const childTable of cascadeTargets) {
|
|
157
|
+
const rel = this._ctx.relationships.find(
|
|
158
|
+
r => r.type === 'belongs-to' && r.from === childTable && r.to === entityName
|
|
159
|
+
);
|
|
160
|
+
if (rel) {
|
|
161
|
+
if (this._softDeletes) {
|
|
162
|
+
const now = new Date().toISOString();
|
|
163
|
+
this._stmt(`UPDATE "${childTable}" SET "deletedAt" = ? WHERE "${rel.foreignKey}" = ?`).run(now, id);
|
|
164
|
+
} else {
|
|
165
|
+
this._stmt(`DELETE FROM "${childTable}" WHERE "${rel.foreignKey}" = ?`).run(id);
|
|
166
|
+
}
|
|
151
167
|
}
|
|
152
168
|
}
|
|
153
169
|
}
|
|
154
|
-
}
|
|
155
170
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
171
|
+
if (this._softDeletes) {
|
|
172
|
+
const now = new Date().toISOString();
|
|
173
|
+
this._stmt(`UPDATE "${entityName}" SET "deletedAt" = ? WHERE id = ?`).run(now, id);
|
|
174
|
+
if (hooks?.afterDelete) hooks.afterDelete(id);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
return deleteEntity(this._ctx, entityName, id);
|
|
178
|
+
});
|
|
163
179
|
}
|
|
164
180
|
return createDeleteBuilder(this._ctx, entityName);
|
|
165
181
|
}) as any,
|
|
166
182
|
restore: ((id: number) => {
|
|
167
183
|
if (!this._softDeletes) throw new Error('restore() requires softDeletes: true');
|
|
168
|
-
|
|
169
|
-
|
|
184
|
+
this._m(`${entityName}.restore(${id})`, () => {
|
|
185
|
+
this._stmt(`UPDATE "${entityName}" SET "deletedAt" = NULL WHERE id = ?`).run(id);
|
|
186
|
+
});
|
|
170
187
|
}) as any,
|
|
171
188
|
select: (...cols: string[]) => createQueryBuilder(this._ctx, entityName, cols),
|
|
172
|
-
count: () => {
|
|
173
|
-
const row = this.
|
|
189
|
+
count: () => this._m(`${entityName}.count`, () => {
|
|
190
|
+
const row = this._stmt(`SELECT COUNT(*) as count FROM "${entityName}"${this._softDeletes ? ' WHERE "deletedAt" IS NULL' : ''}`).get() as any;
|
|
174
191
|
return row?.count ?? 0;
|
|
175
|
-
},
|
|
192
|
+
}),
|
|
176
193
|
on: (event: ChangeEvent, callback: (row: any) => void | Promise<void>) => {
|
|
177
194
|
return this._registerListener(entityName, event, callback);
|
|
178
195
|
},
|
|
@@ -336,11 +353,11 @@ class _Database<Schemas extends SchemaMap> {
|
|
|
336
353
|
*/
|
|
337
354
|
private _processChanges(): void {
|
|
338
355
|
// Fast path: check if anything changed at all (single scalar, index-only)
|
|
339
|
-
const head = this.
|
|
356
|
+
const head = this._stmt('SELECT MAX(id) as m FROM "_changes"').get() as any;
|
|
340
357
|
const maxId: number = head?.m ?? 0;
|
|
341
358
|
if (maxId <= this._changeWatermark) return;
|
|
342
359
|
|
|
343
|
-
const changes = this.
|
|
360
|
+
const changes = this._stmt(
|
|
344
361
|
'SELECT id, tbl, op, row_id FROM "_changes" WHERE id > ? ORDER BY id'
|
|
345
362
|
).all(this._changeWatermark) as { id: number; tbl: string; op: string; row_id: number }[];
|
|
346
363
|
|
|
@@ -371,7 +388,7 @@ class _Database<Schemas extends SchemaMap> {
|
|
|
371
388
|
}
|
|
372
389
|
|
|
373
390
|
// Clean up consumed changes
|
|
374
|
-
this.
|
|
391
|
+
this._stmt('DELETE FROM "_changes" WHERE id <= ?').run(this._changeWatermark);
|
|
375
392
|
}
|
|
376
393
|
|
|
377
394
|
// =========================================================================
|
|
@@ -379,12 +396,13 @@ class _Database<Schemas extends SchemaMap> {
|
|
|
379
396
|
// =========================================================================
|
|
380
397
|
|
|
381
398
|
public transaction<T>(callback: () => T): T {
|
|
382
|
-
return this.db.transaction(callback)();
|
|
399
|
+
return this._m('transaction', () => this.db.transaction(callback)());
|
|
383
400
|
}
|
|
384
401
|
|
|
385
|
-
/** Close the database: stops polling and releases the SQLite handle. */
|
|
402
|
+
/** Close the database: stops polling, clears cache, and releases the SQLite handle. */
|
|
386
403
|
public close(): void {
|
|
387
404
|
this._stopPolling();
|
|
405
|
+
this._stmtCache.clear();
|
|
388
406
|
this.db.close();
|
|
389
407
|
}
|
|
390
408
|
|
|
@@ -396,14 +414,13 @@ class _Database<Schemas extends SchemaMap> {
|
|
|
396
414
|
public query<T extends Record<string, any> = Record<string, any>>(
|
|
397
415
|
callback: (ctx: { [K in keyof Schemas]: ProxyColumns<InferSchema<Schemas[K]>> }) => ProxyQueryResult
|
|
398
416
|
): T[] {
|
|
399
|
-
return executeProxyQuery(
|
|
417
|
+
return this._m('query(proxy)', () => executeProxyQuery(
|
|
400
418
|
this.schemas,
|
|
401
419
|
callback as any,
|
|
402
420
|
(sql: string, params: any[]) => {
|
|
403
|
-
|
|
404
|
-
return this.db.query(sql).all(...params) as T[];
|
|
421
|
+
return this._stmt(sql).all(...params) as T[];
|
|
405
422
|
},
|
|
406
|
-
);
|
|
423
|
+
));
|
|
407
424
|
}
|
|
408
425
|
|
|
409
426
|
// =========================================================================
|
|
@@ -412,14 +429,12 @@ class _Database<Schemas extends SchemaMap> {
|
|
|
412
429
|
|
|
413
430
|
/** Execute a raw SQL query and return results. */
|
|
414
431
|
public raw<T = any>(sql: string, ...params: any[]): T[] {
|
|
415
|
-
|
|
416
|
-
return this.db.query(sql).all(...params) as T[];
|
|
432
|
+
return this._m(`raw: ${sql.slice(0, 60)}`, () => this._stmt(sql).all(...params) as T[]);
|
|
417
433
|
}
|
|
418
434
|
|
|
419
435
|
/** Execute a raw SQL statement (INSERT/UPDATE/DELETE) without returning rows. */
|
|
420
436
|
public exec(sql: string, ...params: any[]): void {
|
|
421
|
-
|
|
422
|
-
this.db.run(sql, ...params);
|
|
437
|
+
this._m(`exec: ${sql.slice(0, 60)}`, () => this.db.run(sql, ...params));
|
|
423
438
|
}
|
|
424
439
|
|
|
425
440
|
// =========================================================================
|
|
@@ -445,11 +460,13 @@ class _Database<Schemas extends SchemaMap> {
|
|
|
445
460
|
* Each key is a table name, value is an array of raw row objects.
|
|
446
461
|
*/
|
|
447
462
|
public dump(): Record<string, any[]> {
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
463
|
+
return this._m('dump', () => {
|
|
464
|
+
const result: Record<string, any[]> = {};
|
|
465
|
+
for (const tableName of Object.keys(this.schemas)) {
|
|
466
|
+
result[tableName] = this.db.query(`SELECT * FROM "${tableName}"`).all();
|
|
467
|
+
}
|
|
468
|
+
return result;
|
|
469
|
+
});
|
|
453
470
|
}
|
|
454
471
|
|
|
455
472
|
/**
|
|
@@ -457,28 +474,29 @@ class _Database<Schemas extends SchemaMap> {
|
|
|
457
474
|
* Use `{ append: true }` to insert without truncating.
|
|
458
475
|
*/
|
|
459
476
|
public load(data: Record<string, any[]>, options?: { append?: boolean }): void {
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
const
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
const
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
477
|
+
this._m(`load(${Object.keys(data).join(',')})`, () => {
|
|
478
|
+
const txn = this.db.transaction(() => {
|
|
479
|
+
for (const [tableName, rows] of Object.entries(data)) {
|
|
480
|
+
if (!this.schemas[tableName]) continue;
|
|
481
|
+
if (!options?.append) {
|
|
482
|
+
this.db.run(`DELETE FROM "${tableName}"`);
|
|
483
|
+
}
|
|
484
|
+
for (const row of rows) {
|
|
485
|
+
const cols = Object.keys(row).filter(k => k !== 'id');
|
|
486
|
+
const placeholders = cols.map(() => '?').join(', ');
|
|
487
|
+
const values = cols.map(c => {
|
|
488
|
+
const v = row[c];
|
|
489
|
+
if (v !== null && v !== undefined && typeof v === 'object' && !(v instanceof Buffer)) {
|
|
490
|
+
return JSON.stringify(v);
|
|
491
|
+
}
|
|
492
|
+
return v;
|
|
493
|
+
});
|
|
494
|
+
this.db.query(`INSERT INTO "${tableName}" (${cols.map(c => `"${c}"`).join(', ')}) VALUES (${placeholders})`).run(...values);
|
|
495
|
+
}
|
|
478
496
|
}
|
|
479
|
-
}
|
|
497
|
+
});
|
|
498
|
+
txn();
|
|
480
499
|
});
|
|
481
|
-
txn();
|
|
482
500
|
}
|
|
483
501
|
|
|
484
502
|
/**
|
|
@@ -498,45 +516,45 @@ class _Database<Schemas extends SchemaMap> {
|
|
|
498
516
|
* Returns a diff object per table: { added, removed, typeChanged }.
|
|
499
517
|
*/
|
|
500
518
|
public diff(): Record<string, { added: string[]; removed: string[]; typeChanged: { column: string; expected: string; actual: string }[] }> {
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
const
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
519
|
+
return this._m('diff', () => {
|
|
520
|
+
const result: Record<string, { added: string[]; removed: string[]; typeChanged: { column: string; expected: string; actual: string }[] }> = {};
|
|
521
|
+
const systemCols = new Set(['id', 'createdAt', 'updatedAt', 'deletedAt']);
|
|
522
|
+
|
|
523
|
+
for (const [tableName, schema] of Object.entries(this.schemas)) {
|
|
524
|
+
const schemaFields = getStorableFields(schema);
|
|
525
|
+
const schemaColMap = new Map(schemaFields.map(f => [f.name, zodTypeToSqlType(f.type)]));
|
|
526
|
+
|
|
527
|
+
const liveColumns = this.columns(tableName);
|
|
528
|
+
const liveColMap = new Map(liveColumns.map(c => [c.name, c.type]));
|
|
529
|
+
|
|
530
|
+
const added: string[] = [];
|
|
531
|
+
const removed: string[] = [];
|
|
532
|
+
const typeChanged: { column: string; expected: string; actual: string }[] = [];
|
|
533
|
+
|
|
534
|
+
for (const [col, expectedType] of schemaColMap) {
|
|
535
|
+
if (!liveColMap.has(col)) {
|
|
536
|
+
added.push(col);
|
|
537
|
+
} else {
|
|
538
|
+
const actualType = liveColMap.get(col)!;
|
|
539
|
+
if (actualType !== expectedType) {
|
|
540
|
+
typeChanged.push({ column: col, expected: expectedType, actual: actualType });
|
|
541
|
+
}
|
|
523
542
|
}
|
|
524
543
|
}
|
|
525
|
-
}
|
|
526
544
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
545
|
+
for (const col of liveColMap.keys()) {
|
|
546
|
+
if (!systemCols.has(col) && !schemaColMap.has(col)) {
|
|
547
|
+
removed.push(col);
|
|
548
|
+
}
|
|
531
549
|
}
|
|
532
|
-
}
|
|
533
550
|
|
|
534
|
-
|
|
535
|
-
|
|
551
|
+
if (added.length > 0 || removed.length > 0 || typeChanged.length > 0) {
|
|
552
|
+
result[tableName] = { added, removed, typeChanged };
|
|
553
|
+
}
|
|
536
554
|
}
|
|
537
|
-
}
|
|
538
555
|
|
|
539
|
-
|
|
556
|
+
return result;
|
|
557
|
+
});
|
|
540
558
|
}
|
|
541
559
|
}
|
|
542
560
|
|
package/src/query.ts
CHANGED
|
@@ -39,10 +39,11 @@ export function createQueryBuilder(ctx: DatabaseContext, entityName: string, ini
|
|
|
39
39
|
const schema = ctx.schemas[entityName]!;
|
|
40
40
|
|
|
41
41
|
const executor = (sql: string, params: any[], raw: boolean): any[] => {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
return ctx._m(`SQL: ${sql.slice(0, 60)}`, () => {
|
|
43
|
+
const rows = ctx._stmt(sql).all(...params);
|
|
44
|
+
if (raw) return rows;
|
|
45
|
+
return rows.map((row: any) => ctx.attachMethods(entityName, transformFromStorage(row, schema)));
|
|
46
|
+
});
|
|
46
47
|
};
|
|
47
48
|
|
|
48
49
|
const singleExecutor = (sql: string, params: any[], raw: boolean): any | null => {
|
|
@@ -102,7 +103,7 @@ export function createQueryBuilder(ctx: DatabaseContext, entityName: string, ini
|
|
|
102
103
|
if (belongsTo) {
|
|
103
104
|
const fk = belongsTo.foreignKey;
|
|
104
105
|
const placeholders = parentIds.map(() => '?').join(', ');
|
|
105
|
-
const childRows = ctx.
|
|
106
|
+
const childRows = ctx._stmt(
|
|
106
107
|
`SELECT * FROM ${hasMany.to} WHERE ${fk} IN (${placeholders})`
|
|
107
108
|
).all(...parentIds) as any[];
|
|
108
109
|
|