sqlite-zod-orm 3.2.1 → 3.3.1
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 +44 -154
- package/dist/index.js +32 -88
- package/package.json +1 -1
- package/src/database.ts +44 -105
- package/src/query-builder.ts +10 -10
- package/src/types.ts +0 -5
package/README.md
CHANGED
|
@@ -207,66 +207,11 @@ const db = new Database(':memory:', schemas, {
|
|
|
207
207
|
|
|
208
208
|
---
|
|
209
209
|
|
|
210
|
-
## Reactivity —
|
|
210
|
+
## Reactivity — `select().subscribe()`
|
|
211
211
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
| System | Detects | Scope | Overhead | Best for |
|
|
215
|
-
|---|---|---|---|---|
|
|
216
|
-
| **CRUD Events** | insert, update, delete | In-process, per table | Zero (synchronous) | Side effects, caching, logs |
|
|
217
|
-
| **Smart Polling** | insert, delete, update* | Any query result | Lightweight fingerprint check | Live UI, dashboards |
|
|
218
|
-
| **Change Tracking** | insert, update, delete | Per table or global | Trigger-based WAL | Cross-process sync, audit |
|
|
219
|
-
|
|
220
|
-
\* Smart polling detects UPDATEs automatically when `changeTracking` is enabled. Without it, only inserts and deletes are detected.
|
|
221
|
-
|
|
222
|
-
---
|
|
223
|
-
|
|
224
|
-
### 1. CRUD Events — `db.table.subscribe(event, callback)`
|
|
225
|
-
|
|
226
|
-
Synchronous callbacks fired immediately after each CRUD operation. Zero overhead; the callback runs inline.
|
|
212
|
+
One API to watch any query for changes. Detects **all** mutations (inserts, updates, deletes) with zero disk overhead.
|
|
227
213
|
|
|
228
214
|
```typescript
|
|
229
|
-
// Listen for new users
|
|
230
|
-
db.users.subscribe('insert', (user) => {
|
|
231
|
-
console.log('New user:', user.name); // fires on every db.users.insert(...)
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
// Listen for updates
|
|
235
|
-
db.users.subscribe('update', (user) => {
|
|
236
|
-
console.log('Updated:', user.name, '→', user.role);
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
// Listen for deletes
|
|
240
|
-
db.users.subscribe('delete', (user) => {
|
|
241
|
-
console.log('Deleted:', user.name);
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
// Stop listening
|
|
245
|
-
db.users.unsubscribe('update', myCallback);
|
|
246
|
-
```
|
|
247
|
-
|
|
248
|
-
**Use cases:**
|
|
249
|
-
- Invalidating a cache after writes
|
|
250
|
-
- Logging / audit trail
|
|
251
|
-
- Sending notifications
|
|
252
|
-
- Keeping derived data in sync (e.g., a counter table)
|
|
253
|
-
|
|
254
|
-
The database also extends Node's `EventEmitter`, so you can use `db.on()`:
|
|
255
|
-
|
|
256
|
-
```typescript
|
|
257
|
-
db.on('insert', (tableName, entity) => {
|
|
258
|
-
console.log(`New row in ${tableName}:`, entity.id);
|
|
259
|
-
});
|
|
260
|
-
```
|
|
261
|
-
|
|
262
|
-
---
|
|
263
|
-
|
|
264
|
-
### 2. Smart Polling — `select().subscribe(callback, options)`
|
|
265
|
-
|
|
266
|
-
Query-level polling that watches *any query result* for changes. Instead of re-fetching all rows every tick, it runs a **lightweight fingerprint query** (`SELECT COUNT(*), MAX(id)`) with the same WHERE clause. The full query only re-executes when the fingerprint changes.
|
|
267
|
-
|
|
268
|
-
```typescript
|
|
269
|
-
// Watch for admin list changes, poll every second
|
|
270
215
|
const unsub = db.users.select()
|
|
271
216
|
.where({ role: 'admin' })
|
|
272
217
|
.orderBy('name', 'asc')
|
|
@@ -283,113 +228,63 @@ unsub();
|
|
|
283
228
|
| Option | Default | Description |
|
|
284
229
|
|---|---|---|
|
|
285
230
|
| `interval` | `500` | Polling interval in milliseconds |
|
|
286
|
-
| `immediate` | `true` |
|
|
231
|
+
| `immediate` | `true` | Fire callback immediately with current data |
|
|
287
232
|
|
|
288
|
-
|
|
233
|
+
### How it works
|
|
289
234
|
|
|
290
235
|
```
|
|
291
|
-
|
|
292
|
-
│ Every {interval}ms:
|
|
293
|
-
│
|
|
294
|
-
│ 1.
|
|
295
|
-
│
|
|
296
|
-
│
|
|
297
|
-
│
|
|
298
|
-
│
|
|
299
|
-
│
|
|
300
|
-
│
|
|
301
|
-
|
|
236
|
+
┌──────────────────────────────────────────────────┐
|
|
237
|
+
│ Every {interval}ms: │
|
|
238
|
+
│ │
|
|
239
|
+
│ 1. Check revision (in-memory + data_version) │
|
|
240
|
+
│ 2. Run: SELECT COUNT(*), MAX(id) │
|
|
241
|
+
│ FROM users WHERE role = 'admin' │
|
|
242
|
+
│ │
|
|
243
|
+
│ 3. Combine into fingerprint: "count:max:rev:dv" │
|
|
244
|
+
│ │
|
|
245
|
+
│ 4. If fingerprint changed → re-run full query │
|
|
246
|
+
│ and call your callback │
|
|
247
|
+
└──────────────────────────────────────────────────┘
|
|
302
248
|
```
|
|
303
249
|
|
|
304
|
-
**
|
|
250
|
+
Two signals combine to detect **all** changes from **any** source:
|
|
305
251
|
|
|
306
|
-
|
|
|
252
|
+
| Signal | Catches | How |
|
|
307
253
|
|---|---|---|
|
|
308
|
-
|
|
|
309
|
-
|
|
|
310
|
-
| UPDATE | ❌ (fingerprint unchanged) | ✅ (change sequence bumps) |
|
|
311
|
-
|
|
312
|
-
> **Tip:** Enable `changeTracking: true` if you need `.subscribe()` to react to UPDATEs.
|
|
313
|
-
> The overhead is minimal — one trigger per table that appends to a `_changes` log.
|
|
314
|
-
|
|
315
|
-
**Use cases:**
|
|
316
|
-
- Live dashboards (poll every 1-5s)
|
|
317
|
-
- Real-time chat message lists
|
|
318
|
-
- Auto-refreshing data tables
|
|
319
|
-
- Watching filtered subsets of data
|
|
320
|
-
|
|
321
|
-
---
|
|
322
|
-
|
|
323
|
-
### 3. Change Tracking — `changeTracking: true`
|
|
254
|
+
| **In-memory revision** | Same-process writes | Bumped by CRUD methods |
|
|
255
|
+
| **PRAGMA data_version** | Cross-process writes | SQLite bumps it on external commits |
|
|
324
256
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
});
|
|
331
|
-
```
|
|
257
|
+
| Operation | Detected | Source |
|
|
258
|
+
|---|---|---|
|
|
259
|
+
| INSERT | ✅ | Same or other process |
|
|
260
|
+
| DELETE | ✅ | Same or other process |
|
|
261
|
+
| UPDATE | ✅ | Same or other process |
|
|
332
262
|
|
|
333
|
-
|
|
334
|
-
- A `_changes` table: `(id, table_name, row_id, action, changed_at)`
|
|
335
|
-
- An index on `(table_name, id)` for fast lookups
|
|
336
|
-
- Triggers on each table for INSERT, UPDATE, and DELETE
|
|
263
|
+
No triggers. No `_changes` table. Zero disk overhead. WAL mode is enabled by default for concurrent read/write.
|
|
337
264
|
|
|
338
|
-
|
|
265
|
+
### Multi-process example
|
|
339
266
|
|
|
340
267
|
```typescript
|
|
341
|
-
//
|
|
342
|
-
const
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
//
|
|
350
|
-
//
|
|
268
|
+
// Process A — watches for new/edited messages
|
|
269
|
+
const unsub = db.messages.select()
|
|
270
|
+
.orderBy('id', 'asc')
|
|
271
|
+
.subscribe((messages) => {
|
|
272
|
+
console.log('Messages:', messages);
|
|
273
|
+
}, { interval: 200 });
|
|
274
|
+
|
|
275
|
+
// Process B — writes to the same DB file (different process)
|
|
276
|
+
// sqlite3 chat.db "INSERT INTO messages (text, author) VALUES ('hello', 'Bob')"
|
|
277
|
+
// → Process A's callback fires with updated message list!
|
|
351
278
|
```
|
|
352
279
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
```typescript
|
|
356
|
-
let lastSeq = 0;
|
|
357
|
-
|
|
358
|
-
setInterval(() => {
|
|
359
|
-
const changes = db.getChangesSince(lastSeq);
|
|
360
|
-
if (changes.length > 0) {
|
|
361
|
-
lastSeq = changes[changes.length - 1].id;
|
|
362
|
-
for (const change of changes) {
|
|
363
|
-
console.log(`${change.action} on ${change.table_name} row ${change.row_id}`);
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
}, 1000);
|
|
367
|
-
```
|
|
280
|
+
Run `bun examples/messages-demo.ts` for a full working demo.
|
|
368
281
|
|
|
369
282
|
**Use cases:**
|
|
370
|
-
-
|
|
371
|
-
-
|
|
372
|
-
-
|
|
373
|
-
-
|
|
374
|
-
-
|
|
375
|
-
|
|
376
|
-
---
|
|
377
|
-
|
|
378
|
-
### Choosing the Right System
|
|
379
|
-
|
|
380
|
-
```
|
|
381
|
-
Do you need to react to your own writes?
|
|
382
|
-
→ CRUD Events (db.table.subscribe)
|
|
383
|
-
|
|
384
|
-
Do you need to watch a query result set?
|
|
385
|
-
→ Smart Polling (select().subscribe)
|
|
386
|
-
→ Enable changeTracking if you need UPDATE detection
|
|
387
|
-
|
|
388
|
-
Do you need cross-process sync or audit?
|
|
389
|
-
→ Change Tracking (changeTracking: true + getChangesSince)
|
|
390
|
-
```
|
|
391
|
-
|
|
392
|
-
All three systems can be used together. `changeTracking` enhances smart polling automatically — no code changes needed.
|
|
283
|
+
- Live dashboards (poll every 1-5s)
|
|
284
|
+
- Real-time chat / message lists
|
|
285
|
+
- Auto-refreshing data tables
|
|
286
|
+
- Watching filtered subsets of data
|
|
287
|
+
- Cross-process data synchronization
|
|
393
288
|
|
|
394
289
|
---
|
|
395
290
|
|
|
@@ -424,12 +319,7 @@ bun test # 91 tests
|
|
|
424
319
|
| `entity.update(data)` | Update entity in-place |
|
|
425
320
|
| `entity.delete()` | Delete entity |
|
|
426
321
|
| **Reactivity** | |
|
|
427
|
-
| `
|
|
428
|
-
| `db.table.unsubscribe(event, cb)` | Remove CRUD event listener |
|
|
429
|
-
| `db.on(event, cb)` | EventEmitter: listen across all tables |
|
|
430
|
-
| `select().subscribe(cb, opts?)` | Smart polling (fingerprint-based) |
|
|
431
|
-
| `db.getChangeSeq(table?)` | Current change sequence number |
|
|
432
|
-
| `db.getChangesSince(seq, table?)` | Changes since sequence (change tracking) |
|
|
322
|
+
| `select().subscribe(cb, opts?)` | Watch any query for changes (all mutations) |
|
|
433
323
|
|
|
434
324
|
## License
|
|
435
325
|
|
package/dist/index.js
CHANGED
|
@@ -12,7 +12,6 @@ var __export = (target, all) => {
|
|
|
12
12
|
|
|
13
13
|
// src/database.ts
|
|
14
14
|
import { Database as SqliteDatabase } from "bun:sqlite";
|
|
15
|
-
import { EventEmitter } from "events";
|
|
16
15
|
|
|
17
16
|
// src/ast.ts
|
|
18
17
|
var wrapNode = (val) => val !== null && typeof val === "object" && ("type" in val) ? val : { type: "literal", value: val };
|
|
@@ -174,14 +173,14 @@ class QueryBuilder {
|
|
|
174
173
|
singleExecutor;
|
|
175
174
|
joinResolver;
|
|
176
175
|
conditionResolver;
|
|
177
|
-
|
|
178
|
-
constructor(tableName, executor, singleExecutor, joinResolver, conditionResolver,
|
|
176
|
+
revisionGetter;
|
|
177
|
+
constructor(tableName, executor, singleExecutor, joinResolver, conditionResolver, revisionGetter) {
|
|
179
178
|
this.tableName = tableName;
|
|
180
179
|
this.executor = executor;
|
|
181
180
|
this.singleExecutor = singleExecutor;
|
|
182
181
|
this.joinResolver = joinResolver ?? null;
|
|
183
182
|
this.conditionResolver = conditionResolver ?? null;
|
|
184
|
-
this.
|
|
183
|
+
this.revisionGetter = revisionGetter ?? null;
|
|
185
184
|
this.iqo = {
|
|
186
185
|
selects: [],
|
|
187
186
|
wheres: [],
|
|
@@ -335,8 +334,8 @@ class QueryBuilder {
|
|
|
335
334
|
try {
|
|
336
335
|
const fpRows = this.executor(fingerprintSQL.sql, fingerprintSQL.params, true);
|
|
337
336
|
const fpRow = fpRows[0];
|
|
338
|
-
const
|
|
339
|
-
const currentFingerprint = `${fpRow?._cnt ?? 0}:${fpRow?._max ?? 0}:${
|
|
337
|
+
const rev = this.revisionGetter?.() ?? "0";
|
|
338
|
+
const currentFingerprint = `${fpRow?._cnt ?? 0}:${fpRow?._max ?? 0}:${rev}`;
|
|
340
339
|
if (currentFingerprint !== lastFingerprint) {
|
|
341
340
|
lastFingerprint = currentFingerprint;
|
|
342
341
|
const rows = this.all();
|
|
@@ -4652,26 +4651,23 @@ function transformFromStorage(row, schema) {
|
|
|
4652
4651
|
}
|
|
4653
4652
|
|
|
4654
4653
|
// src/database.ts
|
|
4655
|
-
class _Database
|
|
4654
|
+
class _Database {
|
|
4656
4655
|
db;
|
|
4657
4656
|
schemas;
|
|
4658
4657
|
relationships;
|
|
4659
|
-
subscriptions;
|
|
4660
4658
|
options;
|
|
4659
|
+
_revisions = {};
|
|
4661
4660
|
constructor(dbFile, schemas, options = {}) {
|
|
4662
|
-
super();
|
|
4663
4661
|
this.db = new SqliteDatabase(dbFile);
|
|
4662
|
+
this.db.run("PRAGMA journal_mode = WAL");
|
|
4664
4663
|
this.db.run("PRAGMA foreign_keys = ON");
|
|
4665
4664
|
this.schemas = schemas;
|
|
4666
4665
|
this.options = options;
|
|
4667
|
-
this.subscriptions = { insert: {}, update: {}, delete: {} };
|
|
4668
4666
|
this.relationships = options.relations ? parseRelationsConfig(options.relations, schemas) : [];
|
|
4669
4667
|
this.initializeTables();
|
|
4670
4668
|
this.runMigrations();
|
|
4671
4669
|
if (options.indexes)
|
|
4672
4670
|
this.createIndexes(options.indexes);
|
|
4673
|
-
if (options.changeTracking)
|
|
4674
|
-
this.setupChangeTracking();
|
|
4675
4671
|
for (const entityName of Object.keys(schemas)) {
|
|
4676
4672
|
const key = entityName;
|
|
4677
4673
|
const accessor = {
|
|
@@ -4683,8 +4679,6 @@ class _Database extends EventEmitter {
|
|
|
4683
4679
|
},
|
|
4684
4680
|
upsert: (conditions, data) => this.upsert(entityName, data, conditions),
|
|
4685
4681
|
delete: (id) => this.delete(entityName, id),
|
|
4686
|
-
subscribe: (event, callback) => this.subscribe(event, entityName, callback),
|
|
4687
|
-
unsubscribe: (event, callback) => this.unsubscribe(event, entityName, callback),
|
|
4688
4682
|
select: (...cols) => this._createQueryBuilder(entityName, cols),
|
|
4689
4683
|
_tableName: entityName
|
|
4690
4684
|
};
|
|
@@ -4706,66 +4700,34 @@ class _Database extends EventEmitter {
|
|
|
4706
4700
|
}
|
|
4707
4701
|
}
|
|
4708
4702
|
runMigrations() {
|
|
4709
|
-
this.db.run(`CREATE TABLE IF NOT EXISTS _schema_meta (
|
|
4710
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
4711
|
-
table_name TEXT NOT NULL,
|
|
4712
|
-
column_name TEXT NOT NULL,
|
|
4713
|
-
added_at TEXT DEFAULT (datetime('now')),
|
|
4714
|
-
UNIQUE(table_name, column_name)
|
|
4715
|
-
)`);
|
|
4716
4703
|
for (const [entityName, schema] of Object.entries(this.schemas)) {
|
|
4717
|
-
const
|
|
4704
|
+
const existingColumns = this.db.query(`PRAGMA table_info(${entityName})`).all();
|
|
4705
|
+
const existingNames = new Set(existingColumns.map((c) => c.name));
|
|
4718
4706
|
const storableFields = getStorableFields(schema);
|
|
4719
4707
|
for (const field of storableFields) {
|
|
4720
|
-
if (!
|
|
4721
|
-
|
|
4722
|
-
this.db.
|
|
4708
|
+
if (!existingNames.has(field.name)) {
|
|
4709
|
+
const sqlType = zodTypeToSqlType(field.type);
|
|
4710
|
+
this.db.run(`ALTER TABLE ${entityName} ADD COLUMN ${field.name} ${sqlType}`);
|
|
4723
4711
|
}
|
|
4724
4712
|
}
|
|
4725
4713
|
}
|
|
4726
4714
|
}
|
|
4727
|
-
createIndexes(
|
|
4728
|
-
for (const [tableName,
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
const columns = Array.isArray(indexDef) ? indexDef : [indexDef];
|
|
4734
|
-
const indexName = `idx_${tableName}_${columns.join("_")}`;
|
|
4735
|
-
this.db.run(`CREATE INDEX IF NOT EXISTS ${indexName} ON ${tableName} (${columns.join(", ")})`);
|
|
4715
|
+
createIndexes(indexes) {
|
|
4716
|
+
for (const [tableName, indexDefs] of Object.entries(indexes)) {
|
|
4717
|
+
for (const def of indexDefs) {
|
|
4718
|
+
const cols = Array.isArray(def) ? def : [def];
|
|
4719
|
+
const idxName = `idx_${tableName}_${cols.join("_")}`;
|
|
4720
|
+
this.db.run(`CREATE INDEX IF NOT EXISTS ${idxName} ON ${tableName} (${cols.join(", ")})`);
|
|
4736
4721
|
}
|
|
4737
4722
|
}
|
|
4738
4723
|
}
|
|
4739
|
-
|
|
4740
|
-
this.
|
|
4741
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
4742
|
-
table_name TEXT NOT NULL,
|
|
4743
|
-
row_id INTEGER NOT NULL,
|
|
4744
|
-
action TEXT NOT NULL CHECK(action IN ('INSERT', 'UPDATE', 'DELETE')),
|
|
4745
|
-
changed_at TEXT DEFAULT (datetime('now'))
|
|
4746
|
-
)`);
|
|
4747
|
-
this.db.run(`CREATE INDEX IF NOT EXISTS idx_changes_table ON _changes (table_name, id)`);
|
|
4748
|
-
for (const entityName of Object.keys(this.schemas)) {
|
|
4749
|
-
for (const action of ["insert", "update", "delete"]) {
|
|
4750
|
-
const ref = action === "delete" ? "OLD" : "NEW";
|
|
4751
|
-
this.db.run(`CREATE TRIGGER IF NOT EXISTS _trg_${entityName}_${action}
|
|
4752
|
-
AFTER ${action.toUpperCase()} ON ${entityName}
|
|
4753
|
-
BEGIN
|
|
4754
|
-
INSERT INTO _changes (table_name, row_id, action) VALUES ('${entityName}', ${ref}.id, '${action.toUpperCase()}');
|
|
4755
|
-
END`);
|
|
4756
|
-
}
|
|
4757
|
-
}
|
|
4724
|
+
_bumpRevision(entityName) {
|
|
4725
|
+
this._revisions[entityName] = (this._revisions[entityName] ?? 0) + 1;
|
|
4758
4726
|
}
|
|
4759
|
-
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
4763
|
-
const row = this.db.query(sql).get(...tableName ? [tableName] : []);
|
|
4764
|
-
return row?.seq ?? 0;
|
|
4765
|
-
}
|
|
4766
|
-
getChangesSince(sinceSeq, tableName) {
|
|
4767
|
-
const sql = tableName ? `SELECT * FROM _changes WHERE id > ? AND table_name = ? ORDER BY id ASC` : `SELECT * FROM _changes WHERE id > ? ORDER BY id ASC`;
|
|
4768
|
-
return this.db.query(sql).all(...tableName ? [sinceSeq, tableName] : [sinceSeq]);
|
|
4727
|
+
_getRevision(entityName) {
|
|
4728
|
+
const rev = this._revisions[entityName] ?? 0;
|
|
4729
|
+
const dataVersion = this.db.query("PRAGMA data_version").get()?.data_version ?? 0;
|
|
4730
|
+
return `${rev}:${dataVersion}`;
|
|
4769
4731
|
}
|
|
4770
4732
|
insert(entityName, data) {
|
|
4771
4733
|
const schema = this.schemas[entityName];
|
|
@@ -4777,8 +4739,7 @@ class _Database extends EventEmitter {
|
|
|
4777
4739
|
const newEntity = this._getById(entityName, result.lastInsertRowid);
|
|
4778
4740
|
if (!newEntity)
|
|
4779
4741
|
throw new Error("Failed to retrieve entity after insertion");
|
|
4780
|
-
this.
|
|
4781
|
-
this.subscriptions.insert[entityName]?.forEach((cb) => cb(newEntity));
|
|
4742
|
+
this._bumpRevision(entityName);
|
|
4782
4743
|
return newEntity;
|
|
4783
4744
|
}
|
|
4784
4745
|
_getById(entityName, id) {
|
|
@@ -4807,11 +4768,8 @@ class _Database extends EventEmitter {
|
|
|
4807
4768
|
return this._getById(entityName, id);
|
|
4808
4769
|
const setClause = Object.keys(transformed).map((key) => `${key} = ?`).join(", ");
|
|
4809
4770
|
this.db.query(`UPDATE ${entityName} SET ${setClause} WHERE id = ?`).run(...Object.values(transformed), id);
|
|
4771
|
+
this._bumpRevision(entityName);
|
|
4810
4772
|
const updatedEntity = this._getById(entityName, id);
|
|
4811
|
-
if (updatedEntity) {
|
|
4812
|
-
this.emit("update", entityName, updatedEntity);
|
|
4813
|
-
this.subscriptions.update[entityName]?.forEach((cb) => cb(updatedEntity));
|
|
4814
|
-
}
|
|
4815
4773
|
return updatedEntity;
|
|
4816
4774
|
}
|
|
4817
4775
|
_updateWhere(entityName, data, conditions) {
|
|
@@ -4827,12 +4785,8 @@ class _Database extends EventEmitter {
|
|
|
4827
4785
|
const setClause = setCols.map((key) => `${key} = ?`).join(", ");
|
|
4828
4786
|
const result = this.db.query(`UPDATE ${entityName} SET ${setClause} ${clause}`).run(...setCols.map((key) => transformed[key]), ...whereValues);
|
|
4829
4787
|
const affected = result.changes ?? 0;
|
|
4830
|
-
if (affected > 0
|
|
4831
|
-
|
|
4832
|
-
this.emit("update", entityName, entity);
|
|
4833
|
-
this.subscriptions.update[entityName]?.forEach((cb) => cb(entity));
|
|
4834
|
-
}
|
|
4835
|
-
}
|
|
4788
|
+
if (affected > 0)
|
|
4789
|
+
this._bumpRevision(entityName);
|
|
4836
4790
|
return affected;
|
|
4837
4791
|
}
|
|
4838
4792
|
_createUpdateBuilder(entityName, data) {
|
|
@@ -4862,8 +4816,7 @@ class _Database extends EventEmitter {
|
|
|
4862
4816
|
const entity = this._getById(entityName, id);
|
|
4863
4817
|
if (entity) {
|
|
4864
4818
|
this.db.query(`DELETE FROM ${entityName} WHERE id = ?`).run(id);
|
|
4865
|
-
this.
|
|
4866
|
-
this.subscriptions.delete[entityName]?.forEach((cb) => cb(entity));
|
|
4819
|
+
this._bumpRevision(entityName);
|
|
4867
4820
|
}
|
|
4868
4821
|
}
|
|
4869
4822
|
_attachMethods(entityName, entity) {
|
|
@@ -4960,15 +4913,6 @@ class _Database extends EventEmitter {
|
|
|
4960
4913
|
throw new Error(`Transaction failed: ${error.message}`);
|
|
4961
4914
|
}
|
|
4962
4915
|
}
|
|
4963
|
-
subscribe(event, entityName, callback) {
|
|
4964
|
-
this.subscriptions[event][entityName] = this.subscriptions[event][entityName] || [];
|
|
4965
|
-
this.subscriptions[event][entityName].push(callback);
|
|
4966
|
-
}
|
|
4967
|
-
unsubscribe(event, entityName, callback) {
|
|
4968
|
-
if (this.subscriptions[event][entityName]) {
|
|
4969
|
-
this.subscriptions[event][entityName] = this.subscriptions[event][entityName].filter((cb) => cb !== callback);
|
|
4970
|
-
}
|
|
4971
|
-
}
|
|
4972
4916
|
_createQueryBuilder(entityName, initialCols) {
|
|
4973
4917
|
const schema = this.schemas[entityName];
|
|
4974
4918
|
const executor = (sql, params, raw) => {
|
|
@@ -4990,8 +4934,8 @@ class _Database extends EventEmitter {
|
|
|
4990
4934
|
return { fk: "id", pk: reverse.foreignKey };
|
|
4991
4935
|
return null;
|
|
4992
4936
|
};
|
|
4993
|
-
const
|
|
4994
|
-
const builder = new QueryBuilder(entityName, executor, singleExecutor, joinResolver, null,
|
|
4937
|
+
const revisionGetter = () => this._getRevision(entityName);
|
|
4938
|
+
const builder = new QueryBuilder(entityName, executor, singleExecutor, joinResolver, null, revisionGetter);
|
|
4995
4939
|
if (initialCols.length > 0)
|
|
4996
4940
|
builder.select(...initialCols);
|
|
4997
4941
|
return builder;
|
package/package.json
CHANGED
package/src/database.ts
CHANGED
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
* query builders, and event handling.
|
|
6
6
|
*/
|
|
7
7
|
import { Database as SqliteDatabase } from 'bun:sqlite';
|
|
8
|
-
import { EventEmitter } from 'events';
|
|
9
8
|
import { z } from 'zod';
|
|
10
9
|
import { QueryBuilder } from './query-builder';
|
|
11
10
|
import { executeProxyQuery, type ProxyQueryResult } from './proxy-query';
|
|
@@ -25,25 +24,26 @@ import {
|
|
|
25
24
|
// Database Class
|
|
26
25
|
// =============================================================================
|
|
27
26
|
|
|
28
|
-
class _Database<Schemas extends SchemaMap>
|
|
27
|
+
class _Database<Schemas extends SchemaMap> {
|
|
29
28
|
private db: SqliteDatabase;
|
|
30
29
|
private schemas: Schemas;
|
|
31
30
|
private relationships: Relationship[];
|
|
32
|
-
private subscriptions: Record<'insert' | 'update' | 'delete', Record<string, ((data: any) => void)[]>>;
|
|
33
31
|
private options: DatabaseOptions;
|
|
34
32
|
|
|
33
|
+
/** In-memory revision counter per table — bumps on every write (insert/update/delete).
|
|
34
|
+
* Used by QueryBuilder.subscribe() fingerprint to detect ALL changes with zero overhead. */
|
|
35
|
+
private _revisions: Record<string, number> = {};
|
|
36
|
+
|
|
35
37
|
constructor(dbFile: string, schemas: Schemas, options: DatabaseOptions = {}) {
|
|
36
|
-
super();
|
|
37
38
|
this.db = new SqliteDatabase(dbFile);
|
|
39
|
+
this.db.run('PRAGMA journal_mode = WAL'); // WAL enables concurrent read + write
|
|
38
40
|
this.db.run('PRAGMA foreign_keys = ON');
|
|
39
41
|
this.schemas = schemas;
|
|
40
42
|
this.options = options;
|
|
41
|
-
this.subscriptions = { insert: {}, update: {}, delete: {} };
|
|
42
43
|
this.relationships = options.relations ? parseRelationsConfig(options.relations, schemas) : [];
|
|
43
44
|
this.initializeTables();
|
|
44
45
|
this.runMigrations();
|
|
45
46
|
if (options.indexes) this.createIndexes(options.indexes);
|
|
46
|
-
if (options.changeTracking) this.setupChangeTracking();
|
|
47
47
|
|
|
48
48
|
// Create typed entity accessors (db.users, db.posts, etc.)
|
|
49
49
|
for (const entityName of Object.keys(schemas)) {
|
|
@@ -56,8 +56,6 @@ class _Database<Schemas extends SchemaMap> extends EventEmitter {
|
|
|
56
56
|
},
|
|
57
57
|
upsert: (conditions, data) => this.upsert(entityName, data, conditions),
|
|
58
58
|
delete: (id) => this.delete(entityName, id),
|
|
59
|
-
subscribe: (event, callback) => this.subscribe(event, entityName, callback),
|
|
60
|
-
unsubscribe: (event, callback) => this.unsubscribe(event, entityName, callback),
|
|
61
59
|
select: (...cols: string[]) => this._createQueryBuilder(entityName, cols),
|
|
62
60
|
_tableName: entityName,
|
|
63
61
|
};
|
|
@@ -90,85 +88,53 @@ class _Database<Schemas extends SchemaMap> extends EventEmitter {
|
|
|
90
88
|
}
|
|
91
89
|
|
|
92
90
|
private runMigrations(): void {
|
|
93
|
-
this.db.run(`CREATE TABLE IF NOT EXISTS _schema_meta (
|
|
94
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
95
|
-
table_name TEXT NOT NULL,
|
|
96
|
-
column_name TEXT NOT NULL,
|
|
97
|
-
added_at TEXT DEFAULT (datetime('now')),
|
|
98
|
-
UNIQUE(table_name, column_name)
|
|
99
|
-
)`);
|
|
100
|
-
|
|
101
91
|
for (const [entityName, schema] of Object.entries(this.schemas)) {
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
);
|
|
105
|
-
const storableFields = getStorableFields(schema);
|
|
92
|
+
const existingColumns = this.db.query(`PRAGMA table_info(${entityName})`).all() as any[];
|
|
93
|
+
const existingNames = new Set(existingColumns.map(c => c.name));
|
|
106
94
|
|
|
95
|
+
const storableFields = getStorableFields(schema);
|
|
107
96
|
for (const field of storableFields) {
|
|
108
|
-
if (!
|
|
109
|
-
|
|
110
|
-
this.db.
|
|
97
|
+
if (!existingNames.has(field.name)) {
|
|
98
|
+
const sqlType = zodTypeToSqlType(field.type);
|
|
99
|
+
this.db.run(`ALTER TABLE ${entityName} ADD COLUMN ${field.name} ${sqlType}`);
|
|
111
100
|
}
|
|
112
101
|
}
|
|
113
102
|
}
|
|
114
103
|
}
|
|
115
104
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
if (!this.schemas[tableName]) throw new Error(`Cannot create index on unknown table '${tableName}'`);
|
|
123
|
-
const indexList = Array.isArray(indexes) ? indexes : [indexes];
|
|
124
|
-
for (const indexDef of indexList) {
|
|
125
|
-
const columns = Array.isArray(indexDef) ? indexDef : [indexDef];
|
|
126
|
-
const indexName = `idx_${tableName}_${columns.join('_')}`;
|
|
127
|
-
this.db.run(`CREATE INDEX IF NOT EXISTS ${indexName} ON ${tableName} (${columns.join(', ')})`);
|
|
105
|
+
private createIndexes(indexes: Record<string, (string | string[])[]>): void {
|
|
106
|
+
for (const [tableName, indexDefs] of Object.entries(indexes)) {
|
|
107
|
+
for (const def of indexDefs) {
|
|
108
|
+
const cols = Array.isArray(def) ? def : [def];
|
|
109
|
+
const idxName = `idx_${tableName}_${cols.join('_')}`;
|
|
110
|
+
this.db.run(`CREATE INDEX IF NOT EXISTS ${idxName} ON ${tableName} (${cols.join(', ')})`);
|
|
128
111
|
}
|
|
129
112
|
}
|
|
130
113
|
}
|
|
131
114
|
|
|
132
115
|
// ===========================================================================
|
|
133
|
-
//
|
|
116
|
+
// Revision Tracking (in-memory + cross-process)
|
|
134
117
|
// ===========================================================================
|
|
135
118
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
table_name TEXT NOT NULL,
|
|
140
|
-
row_id INTEGER NOT NULL,
|
|
141
|
-
action TEXT NOT NULL CHECK(action IN ('INSERT', 'UPDATE', 'DELETE')),
|
|
142
|
-
changed_at TEXT DEFAULT (datetime('now'))
|
|
143
|
-
)`);
|
|
144
|
-
this.db.run(`CREATE INDEX IF NOT EXISTS idx_changes_table ON _changes (table_name, id)`);
|
|
145
|
-
|
|
146
|
-
for (const entityName of Object.keys(this.schemas)) {
|
|
147
|
-
for (const action of ['insert', 'update', 'delete'] as const) {
|
|
148
|
-
const ref = action === 'delete' ? 'OLD' : 'NEW';
|
|
149
|
-
this.db.run(`CREATE TRIGGER IF NOT EXISTS _trg_${entityName}_${action}
|
|
150
|
-
AFTER ${action.toUpperCase()} ON ${entityName}
|
|
151
|
-
BEGIN
|
|
152
|
-
INSERT INTO _changes (table_name, row_id, action) VALUES ('${entityName}', ${ref}.id, '${action.toUpperCase()}');
|
|
153
|
-
END`);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
public getChangeSeq(tableName?: string): number {
|
|
159
|
-
if (!this.options.changeTracking) return -1;
|
|
160
|
-
const sql = tableName
|
|
161
|
-
? `SELECT MAX(id) as seq FROM _changes WHERE table_name = ?`
|
|
162
|
-
: `SELECT MAX(id) as seq FROM _changes`;
|
|
163
|
-
const row = this.db.query(sql).get(...(tableName ? [tableName] : [])) as any;
|
|
164
|
-
return row?.seq ?? 0;
|
|
119
|
+
/** Bump the revision counter for a table. Called on every write. */
|
|
120
|
+
private _bumpRevision(entityName: string): void {
|
|
121
|
+
this._revisions[entityName] = (this._revisions[entityName] ?? 0) + 1;
|
|
165
122
|
}
|
|
166
123
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
124
|
+
/**
|
|
125
|
+
* Get a composite revision string for a table.
|
|
126
|
+
*
|
|
127
|
+
* Combines two signals:
|
|
128
|
+
* - In-memory counter: catches writes from THIS process (our CRUD methods bump it)
|
|
129
|
+
* - PRAGMA data_version: catches writes from OTHER processes (SQLite bumps it
|
|
130
|
+
* whenever another connection commits, but NOT for the current connection)
|
|
131
|
+
*
|
|
132
|
+
* Together they detect ALL changes regardless of source, with zero disk overhead.
|
|
133
|
+
*/
|
|
134
|
+
public _getRevision(entityName: string): string {
|
|
135
|
+
const rev = this._revisions[entityName] ?? 0;
|
|
136
|
+
const dataVersion = (this.db.query('PRAGMA data_version').get() as any)?.data_version ?? 0;
|
|
137
|
+
return `${rev}:${dataVersion}`;
|
|
172
138
|
}
|
|
173
139
|
|
|
174
140
|
// ===========================================================================
|
|
@@ -189,8 +155,7 @@ class _Database<Schemas extends SchemaMap> extends EventEmitter {
|
|
|
189
155
|
const newEntity = this._getById(entityName, result.lastInsertRowid as number);
|
|
190
156
|
if (!newEntity) throw new Error('Failed to retrieve entity after insertion');
|
|
191
157
|
|
|
192
|
-
this.
|
|
193
|
-
this.subscriptions.insert[entityName]?.forEach(cb => cb(newEntity));
|
|
158
|
+
this._bumpRevision(entityName);
|
|
194
159
|
return newEntity;
|
|
195
160
|
}
|
|
196
161
|
|
|
@@ -227,11 +192,8 @@ class _Database<Schemas extends SchemaMap> extends EventEmitter {
|
|
|
227
192
|
const setClause = Object.keys(transformed).map(key => `${key} = ?`).join(', ');
|
|
228
193
|
this.db.query(`UPDATE ${entityName} SET ${setClause} WHERE id = ?`).run(...Object.values(transformed), id);
|
|
229
194
|
|
|
195
|
+
this._bumpRevision(entityName);
|
|
230
196
|
const updatedEntity = this._getById(entityName, id);
|
|
231
|
-
if (updatedEntity) {
|
|
232
|
-
this.emit('update', entityName, updatedEntity);
|
|
233
|
-
this.subscriptions.update[entityName]?.forEach(cb => cb(updatedEntity));
|
|
234
|
-
}
|
|
235
197
|
return updatedEntity;
|
|
236
198
|
}
|
|
237
199
|
|
|
@@ -252,12 +214,7 @@ class _Database<Schemas extends SchemaMap> extends EventEmitter {
|
|
|
252
214
|
);
|
|
253
215
|
|
|
254
216
|
const affected = (result as any).changes ?? 0;
|
|
255
|
-
if (affected > 0
|
|
256
|
-
for (const entity of this._findMany(entityName, conditions)) {
|
|
257
|
-
this.emit('update', entityName, entity);
|
|
258
|
-
this.subscriptions.update[entityName]?.forEach(cb => cb(entity));
|
|
259
|
-
}
|
|
260
|
-
}
|
|
217
|
+
if (affected > 0) this._bumpRevision(entityName);
|
|
261
218
|
return affected;
|
|
262
219
|
}
|
|
263
220
|
|
|
@@ -292,8 +249,7 @@ class _Database<Schemas extends SchemaMap> extends EventEmitter {
|
|
|
292
249
|
const entity = this._getById(entityName, id);
|
|
293
250
|
if (entity) {
|
|
294
251
|
this.db.query(`DELETE FROM ${entityName} WHERE id = ?`).run(id);
|
|
295
|
-
this.
|
|
296
|
-
this.subscriptions.delete[entityName]?.forEach(cb => cb(entity));
|
|
252
|
+
this._bumpRevision(entityName);
|
|
297
253
|
}
|
|
298
254
|
}
|
|
299
255
|
|
|
@@ -415,21 +371,6 @@ class _Database<Schemas extends SchemaMap> extends EventEmitter {
|
|
|
415
371
|
}
|
|
416
372
|
}
|
|
417
373
|
|
|
418
|
-
// ===========================================================================
|
|
419
|
-
// Events
|
|
420
|
-
// ===========================================================================
|
|
421
|
-
|
|
422
|
-
private subscribe(event: 'insert' | 'update' | 'delete', entityName: string, callback: (data: any) => void): void {
|
|
423
|
-
this.subscriptions[event][entityName] = this.subscriptions[event][entityName] || [];
|
|
424
|
-
this.subscriptions[event][entityName].push(callback);
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
private unsubscribe(event: 'insert' | 'update' | 'delete', entityName: string, callback: (data: any) => void): void {
|
|
428
|
-
if (this.subscriptions[event][entityName]) {
|
|
429
|
-
this.subscriptions[event][entityName] = this.subscriptions[event][entityName].filter(cb => cb !== callback);
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
|
|
433
374
|
// ===========================================================================
|
|
434
375
|
// Query Builders
|
|
435
376
|
// ===========================================================================
|
|
@@ -459,13 +400,11 @@ class _Database<Schemas extends SchemaMap> extends EventEmitter {
|
|
|
459
400
|
if (reverse) return { fk: 'id', pk: reverse.foreignKey };
|
|
460
401
|
return null;
|
|
461
402
|
};
|
|
462
|
-
// Provide change sequence getter when change tracking is enabled
|
|
463
|
-
// This allows .subscribe() to detect row UPDATEs (not just inserts/deletes)
|
|
464
|
-
const changeSeqGetter = this.options.changeTracking
|
|
465
|
-
? () => this.getChangeSeq(entityName)
|
|
466
|
-
: null;
|
|
467
403
|
|
|
468
|
-
|
|
404
|
+
// Pass revision getter — allows .subscribe() to detect ALL changes
|
|
405
|
+
const revisionGetter = () => this._getRevision(entityName);
|
|
406
|
+
|
|
407
|
+
const builder = new QueryBuilder(entityName, executor, singleExecutor, joinResolver, null, revisionGetter);
|
|
469
408
|
if (initialCols.length > 0) builder.select(...initialCols);
|
|
470
409
|
return builder;
|
|
471
410
|
}
|
package/src/query-builder.ts
CHANGED
|
@@ -167,7 +167,7 @@ export class QueryBuilder<T extends Record<string, any>> {
|
|
|
167
167
|
private singleExecutor: (sql: string, params: any[], raw: boolean) => any | null;
|
|
168
168
|
private joinResolver: ((fromTable: string, toTable: string) => { fk: string; pk: string } | null) | null;
|
|
169
169
|
private conditionResolver: ((conditions: Record<string, any>) => Record<string, any>) | null;
|
|
170
|
-
private
|
|
170
|
+
private revisionGetter: (() => string) | null;
|
|
171
171
|
|
|
172
172
|
constructor(
|
|
173
173
|
tableName: string,
|
|
@@ -175,14 +175,14 @@ export class QueryBuilder<T extends Record<string, any>> {
|
|
|
175
175
|
singleExecutor: (sql: string, params: any[], raw: boolean) => any | null,
|
|
176
176
|
joinResolver?: ((fromTable: string, toTable: string) => { fk: string; pk: string } | null) | null,
|
|
177
177
|
conditionResolver?: ((conditions: Record<string, any>) => Record<string, any>) | null,
|
|
178
|
-
|
|
178
|
+
revisionGetter?: (() => string) | null,
|
|
179
179
|
) {
|
|
180
180
|
this.tableName = tableName;
|
|
181
181
|
this.executor = executor;
|
|
182
182
|
this.singleExecutor = singleExecutor;
|
|
183
183
|
this.joinResolver = joinResolver ?? null;
|
|
184
184
|
this.conditionResolver = conditionResolver ?? null;
|
|
185
|
-
this.
|
|
185
|
+
this.revisionGetter = revisionGetter ?? null;
|
|
186
186
|
this.iqo = {
|
|
187
187
|
selects: [],
|
|
188
188
|
wheres: [],
|
|
@@ -423,9 +423,9 @@ export class QueryBuilder<T extends Record<string, any>> {
|
|
|
423
423
|
/**
|
|
424
424
|
* Subscribe to query result changes using smart interval-based polling.
|
|
425
425
|
*
|
|
426
|
-
*
|
|
427
|
-
*
|
|
428
|
-
*
|
|
426
|
+
* Uses a lightweight fingerprint (`COUNT(*), MAX(id)`) combined with an
|
|
427
|
+
* in-memory revision counter to detect ALL changes (inserts, updates, deletes)
|
|
428
|
+
* with zero disk overhead.
|
|
429
429
|
*
|
|
430
430
|
* ```ts
|
|
431
431
|
* const unsub = db.messages.select()
|
|
@@ -459,10 +459,10 @@ export class QueryBuilder<T extends Record<string, any>> {
|
|
|
459
459
|
// Run lightweight fingerprint check
|
|
460
460
|
const fpRows = this.executor(fingerprintSQL.sql, fingerprintSQL.params, true);
|
|
461
461
|
const fpRow = fpRows[0] as any;
|
|
462
|
-
// Include
|
|
463
|
-
// This
|
|
464
|
-
const
|
|
465
|
-
const currentFingerprint = `${fpRow?._cnt ?? 0}:${fpRow?._max ?? 0}:${
|
|
462
|
+
// Include revision in fingerprint (combines in-memory counter + PRAGMA data_version).
|
|
463
|
+
// This detects ALL changes: same-process and cross-process.
|
|
464
|
+
const rev = this.revisionGetter?.() ?? '0';
|
|
465
|
+
const currentFingerprint = `${fpRow?._cnt ?? 0}:${fpRow?._max ?? 0}:${rev}`;
|
|
466
466
|
|
|
467
467
|
if (currentFingerprint !== lastFingerprint) {
|
|
468
468
|
lastFingerprint = currentFingerprint;
|
package/src/types.ts
CHANGED
|
@@ -21,7 +21,6 @@ export type IndexDef = string | string[];
|
|
|
21
21
|
|
|
22
22
|
/** Options for the Database constructor */
|
|
23
23
|
export type DatabaseOptions<R extends RelationsConfig = RelationsConfig> = {
|
|
24
|
-
changeTracking?: boolean;
|
|
25
24
|
indexes?: Record<string, IndexDef[]>;
|
|
26
25
|
/**
|
|
27
26
|
* Declare relationships between tables.
|
|
@@ -143,8 +142,6 @@ export type NavEntityAccessor<
|
|
|
143
142
|
& ((data: Partial<Omit<z.input<S[Table & keyof S]>, 'id'>>) => UpdateBuilder<NavEntity<S, R, Table>>);
|
|
144
143
|
upsert: (conditions?: Partial<z.infer<S[Table & keyof S]>>, data?: Partial<z.infer<S[Table & keyof S]>>) => NavEntity<S, R, Table>;
|
|
145
144
|
delete: (id: number) => void;
|
|
146
|
-
subscribe: (event: 'insert' | 'update' | 'delete', callback: (data: NavEntity<S, R, Table>) => void) => void;
|
|
147
|
-
unsubscribe: (event: 'insert' | 'update' | 'delete', callback: (data: NavEntity<S, R, Table>) => void) => void;
|
|
148
145
|
select: (...cols: (keyof z.infer<S[Table & keyof S]> & string)[]) => QueryBuilder<NavEntity<S, R, Table>>;
|
|
149
146
|
_tableName: string;
|
|
150
147
|
readonly _schema?: S[Table & keyof S];
|
|
@@ -170,8 +167,6 @@ export type EntityAccessor<S extends z.ZodType<any>> = {
|
|
|
170
167
|
update: ((id: number, data: Partial<EntityData<S>>) => AugmentedEntity<S> | null) & ((data: Partial<EntityData<S>>) => UpdateBuilder<AugmentedEntity<S>>);
|
|
171
168
|
upsert: (conditions?: Partial<InferSchema<S>>, data?: Partial<InferSchema<S>>) => AugmentedEntity<S>;
|
|
172
169
|
delete: (id: number) => void;
|
|
173
|
-
subscribe: (event: 'insert' | 'update' | 'delete', callback: (data: AugmentedEntity<S>) => void) => void;
|
|
174
|
-
unsubscribe: (event: 'insert' | 'update' | 'delete', callback: (data: AugmentedEntity<S>) => void) => void;
|
|
175
170
|
select: (...cols: (keyof InferSchema<S> & string)[]) => QueryBuilder<AugmentedEntity<S>>;
|
|
176
171
|
_tableName: string;
|
|
177
172
|
readonly _schema?: S;
|