uql-orm 0.29.0 → 0.31.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.
Files changed (40) hide show
  1. package/README.md +11 -3
  2. package/dist/browser/querier/httpQuerier.d.ts +5 -5
  3. package/dist/browser/uql-browser.min.js.map +2 -2
  4. package/dist/dialect/abstractSqlDialect.d.ts +6 -4
  5. package/dist/dialect/abstractSqlDialect.js +29 -29
  6. package/dist/dialect/mysqlLikeSqlDialect.d.ts +3 -1
  7. package/dist/dialect/mysqlLikeSqlDialect.js +9 -0
  8. package/dist/dialect/vectorSqlDialect.js +8 -1
  9. package/dist/mongo/mongoDialect.d.ts +3 -3
  10. package/dist/mongo/mongoDialect.js +17 -9
  11. package/dist/mongo/mongodbQuerier.d.ts +4 -4
  12. package/dist/pglite/index.d.ts +3 -0
  13. package/dist/pglite/index.js +3 -0
  14. package/dist/pglite/pgliteDialect.d.ts +14 -0
  15. package/dist/pglite/pgliteDialect.js +14 -0
  16. package/dist/pglite/pgliteQuerier.d.ts +36 -0
  17. package/dist/pglite/pgliteQuerier.js +30 -0
  18. package/dist/pglite/pgliteQuerierPool.d.ts +36 -0
  19. package/dist/pglite/pgliteQuerierPool.js +36 -0
  20. package/dist/querier/abstractQuerier.d.ts +19 -8
  21. package/dist/querier/abstractQuerier.js +61 -25
  22. package/dist/querier/abstractQuerierPool.d.ts +7 -7
  23. package/dist/querier/abstractSharedHandleQuerierPool.d.ts +42 -0
  24. package/dist/querier/abstractSharedHandleQuerierPool.js +48 -0
  25. package/dist/querier/abstractSqlQuerier.d.ts +6 -4
  26. package/dist/querier/abstractSqlQuerier.js +31 -9
  27. package/dist/sqlite/localSqliteQuerierPool.d.ts +8 -11
  28. package/dist/sqlite/localSqliteQuerierPool.js +8 -15
  29. package/dist/turso/tursoLocalQuerierPool.d.ts +5 -11
  30. package/dist/turso/tursoLocalQuerierPool.js +4 -14
  31. package/dist/type/entity.d.ts +41 -16
  32. package/dist/type/querier.d.ts +14 -8
  33. package/dist/type/query.d.ts +27 -14
  34. package/dist/type/queryAggregate.d.ts +67 -24
  35. package/dist/type/queryWhere.d.ts +8 -5
  36. package/dist/type/universalQuerier.d.ts +15 -13
  37. package/dist/type/utility.d.ts +7 -0
  38. package/dist/util/dialect.util.d.ts +26 -4
  39. package/dist/util/dialect.util.js +42 -0
  40. package/package.json +13 -4
@@ -0,0 +1,30 @@
1
+ import { AbstractSqlQuerier } from '../querier/index.js';
2
+ /**
3
+ * Querier for PGlite, Postgres compiled to WASM and run in this process.
4
+ *
5
+ * @remarks Extends {@link AbstractSqlQuerier} rather than `AbstractPgQuerier`, whose `internalStream`
6
+ * hands a `pg-query-stream` object to `query()`: PGlite has no cursor API, so streaming falls back to
7
+ * the base class buffering the whole result. `BEGIN`/`COMMIT` are plain statements on the single
8
+ * connection, leaving transactions to the base class.
9
+ */
10
+ export class PgliteQuerier extends AbstractSqlQuerier {
11
+ db;
12
+ extra;
13
+ constructor(db, dialect, extra) {
14
+ super(dialect, extra);
15
+ this.db = db;
16
+ this.extra = extra;
17
+ }
18
+ async internalAll(query, values) {
19
+ const res = await this.db.query(query, values);
20
+ return res.rows;
21
+ }
22
+ async internalRun(query, values) {
23
+ const res = await this.db.query(query, values);
24
+ // `affectedRows`, not `rowCount`: PGlite derives the former from the command tag of a write only,
25
+ // where the latter also counts a `SELECT`'s rows and is absent altogether from a DDL tag.
26
+ return this.buildUpdateResult({ rows: res.rows, changes: res.affectedRows ?? 0 });
27
+ }
28
+ /** The handle belongs to the pool, which hands out one querier per unit of work over it. */
29
+ async internalRelease() { }
30
+ }
@@ -0,0 +1,36 @@
1
+ import type { PGliteOptions } from '@electric-sql/pglite';
2
+ import { AbstractSharedHandleQuerierPool } from '../querier/abstractSharedHandleQuerierPool.js';
3
+ import type { ExtraOptions } from '../type/index.js';
4
+ import { PgliteDialect } from './pgliteDialect.js';
5
+ import { type PgliteDatabase, PgliteQuerier } from './pgliteQuerier.js';
6
+ /**
7
+ * The driver's own options, minus the `dataDir` this pool takes as its first argument.
8
+ *
9
+ * @remarks Imported rather than restated so extensions and the filesystem hooks keep their real types:
10
+ * `extensions: { vector }` from `@electric-sql/pglite-pgvector` is how a vector column becomes usable,
11
+ * mirroring `LocalSqlitePoolOptions.extensions` for `sqlite-vec`. Type-only, like the `pg` imports in
12
+ * `abstractPgQuerierPool.ts`, so nothing here reaches a runtime without the peer installed.
13
+ */
14
+ export type PglitePoolOptions = Omit<PGliteOptions, 'dataDir'>;
15
+ /**
16
+ * Pool for PGlite, Postgres compiled to WASM and run in this process. No server, no container.
17
+ *
18
+ * PGlite is single connection, so the shared-handle lifecycle is {@link AbstractSharedHandleQuerierPool}'s.
19
+ * Where PGlite differs from the two SQLite-family pools there is that it does not refuse a second
20
+ * `BEGIN`: a querier that opens a transaction while another already has one silently joins it, and that
21
+ * one's `ROLLBACK` then discards both queriers' writes. Nothing reports it, so a unit of work that needs
22
+ * a transaction of its own needs its own pool, and therefore its own database.
23
+ *
24
+ * @remarks Transactions are plain `BEGIN`/`COMMIT` statements rather than `db.transaction()`, whose
25
+ * callback holds PGlite's transaction mutex and would block every other querier's reads until commit.
26
+ * The cost is that PGlite cannot see the transaction, so it flushes to the filesystem after each
27
+ * statement within one: pass `relaxedDurability: true` on a persistent `dataDir` to skip waiting on
28
+ * those flushes.
29
+ */
30
+ export declare class PgliteQuerierPool extends AbstractSharedHandleQuerierPool<PgliteDatabase, PgliteQuerier, PgliteDialect> {
31
+ readonly dataDir: string;
32
+ readonly opts?: PglitePoolOptions | undefined;
33
+ constructor(dataDir?: string, opts?: PglitePoolOptions | undefined, extra?: ExtraOptions);
34
+ protected openDb(): Promise<PgliteDatabase>;
35
+ protected buildQuerier(db: PgliteDatabase): PgliteQuerier;
36
+ }
@@ -0,0 +1,36 @@
1
+ import { AbstractSharedHandleQuerierPool } from '../querier/abstractSharedHandleQuerierPool.js';
2
+ import { PgliteDialect } from './pgliteDialect.js';
3
+ import { PgliteQuerier } from './pgliteQuerier.js';
4
+ /**
5
+ * Pool for PGlite, Postgres compiled to WASM and run in this process. No server, no container.
6
+ *
7
+ * PGlite is single connection, so the shared-handle lifecycle is {@link AbstractSharedHandleQuerierPool}'s.
8
+ * Where PGlite differs from the two SQLite-family pools there is that it does not refuse a second
9
+ * `BEGIN`: a querier that opens a transaction while another already has one silently joins it, and that
10
+ * one's `ROLLBACK` then discards both queriers' writes. Nothing reports it, so a unit of work that needs
11
+ * a transaction of its own needs its own pool, and therefore its own database.
12
+ *
13
+ * @remarks Transactions are plain `BEGIN`/`COMMIT` statements rather than `db.transaction()`, whose
14
+ * callback holds PGlite's transaction mutex and would block every other querier's reads until commit.
15
+ * The cost is that PGlite cannot see the transaction, so it flushes to the filesystem after each
16
+ * statement within one: pass `relaxedDurability: true` on a persistent `dataDir` to skip waiting on
17
+ * those flushes.
18
+ */
19
+ export class PgliteQuerierPool extends AbstractSharedHandleQuerierPool {
20
+ dataDir;
21
+ opts;
22
+ constructor(dataDir = 'memory://', opts, extra) {
23
+ super(new PgliteDialect({ namingStrategy: extra?.namingStrategy }), extra);
24
+ this.dataDir = dataDir;
25
+ this.opts = opts;
26
+ }
27
+ async openDb() {
28
+ const { PGlite } = await import('@electric-sql/pglite');
29
+ // The declared return type is what checks {@link PgliteDatabase} against the real driver, so no
30
+ // cast is needed here or anywhere below it.
31
+ return PGlite.create(this.dataDir, this.opts);
32
+ }
33
+ buildQuerier(db) {
34
+ return new PgliteQuerier(db, this.dialect, this.extra);
35
+ }
36
+ }
@@ -1,4 +1,4 @@
1
- import type { ExtraOptions, IdValue, Querier, Query, QueryAggMap, QueryAggregate, QueryAggregateResult, QueryConflictPaths, QueryGroupMap, QueryOne, QueryOptions, QueryPopulate, QuerySearch, QueryUpdateResult, RawRow, RelationKey, TransactionOptions, Type, UpdatePayload } from '../type/index.js';
1
+ import type { EntityData, ExtraOptions, IdValue, Querier, Query, QueryAggMap, QueryAggregate, QueryAggregateResult, QueryConflictPaths, QueryGroupMap, QueryOne, QueryOptions, QueryPopulate, QuerySearch, QueryUpdateResult, RawRow, RelationKey, TransactionOptions, Type, UpdatePayload } from '../type/index.js';
2
2
  import { LoggerWrapper } from '../util/index.js';
3
3
  /**
4
4
  * Base class for all database queriers.
@@ -85,16 +85,16 @@ export declare abstract class AbstractQuerier implements Querier {
85
85
  */
86
86
  aggregate<E extends object, const G extends QueryGroupMap<E>, const A extends QueryAggMap<E>>(entity: Type<E>, q: QueryAggregate<E, G, A>, opts?: QueryOptions): Promise<QueryAggregateResult<E, G, A>[]>;
87
87
  protected abstract internalAggregate<E extends object, G extends QueryGroupMap<E>, A extends QueryAggMap<E>>(entity: Type<E>, q: QueryAggregate<E, G, A>, opts?: QueryOptions): Promise<QueryAggregateResult<E, G, A>[]>;
88
- insertOne<E extends object>(entity: Type<E>, payload: E): Promise<IdValue<E> | undefined>;
89
- insertMany<E extends object>(entity: Type<E>, payload: E[]): Promise<IdValue<E>[]>;
90
- protected abstract internalInsertMany<E extends object>(entity: Type<E>, payload: E[]): Promise<IdValue<E>[]>;
88
+ insertOne<E extends object>(entity: Type<E>, payload: EntityData<E>): Promise<IdValue<E> | undefined>;
89
+ insertMany<E extends object>(entity: Type<E>, payload: EntityData<E>[]): Promise<IdValue<E>[]>;
90
+ protected abstract internalInsertMany<E extends object>(entity: Type<E>, payload: EntityData<E>[]): Promise<IdValue<E>[]>;
91
91
  updateOneById<E extends object>(entity: Type<E>, id: IdValue<E>, payload: UpdatePayload<E>, opts?: QueryOptions): Promise<number>;
92
92
  updateMany<E extends object>(entity: Type<E>, q: QuerySearch<E>, payload: UpdatePayload<E>, opts?: QueryOptions): Promise<number>;
93
93
  protected abstract internalUpdateMany<E extends object>(entity: Type<E>, q: QuerySearch<E>, payload: UpdatePayload<E>, opts?: QueryOptions): Promise<number>;
94
94
  restoreOneById<E extends object>(entity: Type<E>, id: IdValue<E>): Promise<number>;
95
95
  restoreMany<E extends object>(entity: Type<E>, q: QuerySearch<E>): Promise<number>;
96
- abstract upsertOne<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: E): Promise<QueryUpdateResult>;
97
- abstract upsertMany<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: E[]): Promise<QueryUpdateResult>;
96
+ abstract upsertOne<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: EntityData<E>): Promise<QueryUpdateResult>;
97
+ abstract upsertMany<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: EntityData<E>[]): Promise<QueryUpdateResult>;
98
98
  deleteOneById<E extends object>(entity: Type<E>, id: IdValue<E>, opts?: QueryOptions): Promise<number>;
99
99
  /**
100
100
  * Delete records matching the query. Soft-deletes when the entity has a soft-delete field (unless
@@ -104,9 +104,18 @@ export declare abstract class AbstractQuerier implements Querier {
104
104
  deleteMany<E extends object>(q: QuerySearch<E> & {
105
105
  $entity: Type<E>;
106
106
  }, opts?: QueryOptions): Promise<number>;
107
+ /**
108
+ * The rows a delete is about to take, loaded only when a hook or listener is there to receive
109
+ * them: the round trip is pure overhead for the (common) delete nobody is watching, and
110
+ * `internalDeleteMany` has its own fast path that never reads the rows at all.
111
+ *
112
+ * `undefined` means nobody was watching, which is not the same as the empty array meaning nothing
113
+ * matched - the caller deletes by `q` for the first and skips the statement entirely for the second.
114
+ */
115
+ private findDoomed;
107
116
  protected abstract internalDeleteMany<E extends object>(entity: Type<E>, q: QuerySearch<E>, opts?: QueryOptions): Promise<number>;
108
- saveOne<E extends object>(entity: Type<E>, payload: E): Promise<IdValue<E>>;
109
- saveMany<E extends object>(entity: Type<E>, payload: E[]): Promise<IdValue<E>[]>;
117
+ saveOne<E extends object>(entity: Type<E>, payload: EntityData<E>): Promise<IdValue<E>>;
118
+ saveMany<E extends object>(entity: Type<E>, payload: EntityData<E>[]): Promise<IdValue<E>[]>;
110
119
  protected fillToManyRelations<E>(entity: Type<E>, payload: E[], populate?: QueryPopulate<E>): Promise<void>;
111
120
  private fillToManyThroughRelation;
112
121
  private fillToManyOneToMany;
@@ -137,6 +146,8 @@ export declare abstract class AbstractQuerier implements Querier {
137
146
  * one afterwards.
138
147
  */
139
148
  transaction<T>(callback: () => Promise<T>, opts?: TransactionOptions): Promise<T>;
149
+ /** Whether anything at all - a global listener or the entity itself - handles `event`. */
150
+ private hasHook;
140
151
  /**
141
152
  * Emit a lifecycle hook event for the given entity.
142
153
  * Fires global listeners first, then entity-level hooks.
@@ -1,6 +1,20 @@
1
1
  import { getMeta } from '../entity/index.js';
2
- import { asSelectMap, augmentWhere, clone, filterPersistableRelationKeys, forEachRequestedRelation, getKeys, getRelationRequestSummary, LoggerWrapper, parseRelationAtKey, parseRelationQueryValue, runHooks, } from '../util/index.js';
2
+ import { asSelectMap, augmentWhere, clone, filterPersistableRelationKeys, forEachRequestedRelation, getKeys, getRelationRequestSummary, LoggerWrapper, parseRelationAtKey, parseRelationQueryValue, runHooks, withoutSoftDeleteFilter, } from '../util/index.js';
3
3
  import { enrichError } from './queryError.js';
4
+ /**
5
+ * Rejects a nullish primary key before it reaches a statement. The by-id methods reduce to
6
+ * `{ $where: id }`, and a nullish `$where` is *no filter*, so an unchecked one addresses the whole
7
+ * table. An entity declares its id optional, which puts `undefined` inside `IdValue<E>`, and the
8
+ * HTTP layer reaches these methods with parsed JSON regardless, so the guard belongs at runtime.
9
+ *
10
+ * Its callers are all `async` so this surfaces as a rejection on every one of them: a guard that
11
+ * threw synchronously from some and rejected from others would escape a caller's `.catch()`.
12
+ */
13
+ function assertIdValue(entity, id) {
14
+ if (id === undefined || id === null) {
15
+ throw new TypeError(`'${entity.name}' was addressed by id, but the id is ${String(id)}`);
16
+ }
17
+ }
4
18
  /**
5
19
  * Base class for all database queriers.
6
20
  * It provides a standardized way to execute tasks serially to prevent race conditions on database connections.
@@ -65,6 +79,7 @@ export class AbstractQuerier {
65
79
  return [$entity, query, maybeQueryOrOpts];
66
80
  }
67
81
  async findOneById(entity, id, q = {}, opts) {
82
+ assertIdValue(entity, id);
68
83
  return this.findOne(entity, { ...q, $where: augmentWhere(getMeta(entity), q.$where, id) }, opts);
69
84
  }
70
85
  async findOne(entityOrQuery, maybeQueryOrOpts, maybeOpts) {
@@ -115,7 +130,8 @@ export class AbstractQuerier {
115
130
  await this.emitHook(entity, 'afterInsert', payload);
116
131
  return ids;
117
132
  }
118
- updateOneById(entity, id, payload, opts) {
133
+ async updateOneById(entity, id, payload, opts) {
134
+ assertIdValue(entity, id);
119
135
  return this.updateMany(entity, { $where: id }, payload, opts);
120
136
  }
121
137
  async updateMany(entity, q, payload, opts) {
@@ -124,7 +140,8 @@ export class AbstractQuerier {
124
140
  await this.emitHook(entity, 'afterUpdate', [payload]);
125
141
  return changes;
126
142
  }
127
- restoreOneById(entity, id) {
143
+ async restoreOneById(entity, id) {
144
+ assertIdValue(entity, id);
128
145
  return this.restoreMany(entity, { $where: id });
129
146
  }
130
147
  async restoreMany(entity, q) {
@@ -137,16 +154,41 @@ export class AbstractQuerier {
137
154
  filters: { softDelete: false },
138
155
  });
139
156
  }
140
- deleteOneById(entity, id, opts) {
157
+ async deleteOneById(entity, id, opts) {
158
+ assertIdValue(entity, id);
141
159
  return this.deleteMany(entity, { $where: id }, opts);
142
160
  }
143
161
  async deleteMany(entityOrQuery, qOrOpts, maybeOpts) {
144
162
  const [entity, q, opts] = this.resolveEntityQuery(entityOrQuery, qOrOpts, maybeOpts);
145
- await this.emitHook(entity, 'beforeDelete', []);
146
- const changes = await this.internalDeleteMany(entity, q, opts);
147
- await this.emitHook(entity, 'afterDelete', []);
163
+ const doomed = await this.findDoomed(entity, q, opts);
164
+ if (doomed?.length === 0) {
165
+ return 0;
166
+ }
167
+ // Where a snapshot was taken, the statement names those rows rather than resolving `q` a second
168
+ // time. Two reads of one `$limit` with no total order are free to disagree, which would fire the
169
+ // hooks for one row and delete another; naming them also spares the second read.
170
+ const target = doomed ? { $where: doomed.map((it) => it[getMeta(entity).id]) } : q;
171
+ await this.emitHook(entity, 'beforeDelete', doomed ?? []);
172
+ const changes = await this.internalDeleteMany(entity, target, opts);
173
+ await this.emitHook(entity, 'afterDelete', doomed ?? []);
148
174
  return changes;
149
175
  }
176
+ /**
177
+ * The rows a delete is about to take, loaded only when a hook or listener is there to receive
178
+ * them: the round trip is pure overhead for the (common) delete nobody is watching, and
179
+ * `internalDeleteMany` has its own fast path that never reads the rows at all.
180
+ *
181
+ * `undefined` means nobody was watching, which is not the same as the empty array meaning nothing
182
+ * matched - the caller deletes by `q` for the first and skips the statement entirely for the second.
183
+ */
184
+ async findDoomed(entity, q, opts) {
185
+ if (!this.hasHook(entity, 'beforeDelete') && !this.hasHook(entity, 'afterDelete')) {
186
+ return undefined;
187
+ }
188
+ // A hard delete takes already-soft-deleted rows too, so reading them back has to see them.
189
+ const findOpts = opts?.hardDelete ? { ...opts, filters: withoutSoftDeleteFilter(opts.filters) } : opts;
190
+ return this.internalFindMany(entity, q, findOpts);
191
+ }
150
192
  async saveOne(entity, payload) {
151
193
  const [id] = await this.saveMany(entity, [payload]);
152
194
  return id;
@@ -400,32 +442,26 @@ export class AbstractQuerier {
400
442
  throw err;
401
443
  }
402
444
  }
445
+ /** Whether anything at all - a global listener or the entity itself - handles `event`. */
446
+ hasHook(entity, event) {
447
+ return (this.extra?.listeners?.some((listener) => listener[event]) || (getMeta(entity).hooks?.[event]?.length ?? 0) > 0);
448
+ }
403
449
  /**
404
450
  * Emit a lifecycle hook event for the given entity.
405
451
  * Fires global listeners first, then entity-level hooks.
406
452
  */
407
453
  async emitHook(entity, event, payloads) {
408
- const listeners = this.extra?.listeners;
409
- const meta = getMeta(entity);
410
- const registrations = meta.hooks?.[event];
411
- // Fast bail-out: skip if no listeners and no entity hooks
412
- if (!listeners?.length && !registrations?.length)
454
+ if (!this.hasHook(entity, event))
413
455
  return;
414
- // Fire global listeners first
415
- if (listeners?.length) {
416
- for (const listener of listeners) {
417
- const fn = listener[event];
418
- if (fn) {
419
- const result = fn({ entity, querier: this, payloads, event });
420
- if (result instanceof Promise)
421
- await result;
422
- }
456
+ for (const listener of this.extra?.listeners ?? []) {
457
+ const fn = listener[event];
458
+ if (fn) {
459
+ const result = fn({ entity, querier: this, payloads, event });
460
+ if (result instanceof Promise)
461
+ await result;
423
462
  }
424
463
  }
425
- // Fire entity-level hooks
426
- if (registrations?.length) {
427
- await runHooks(entity, event, payloads, { querier: this });
428
- }
464
+ await runHooks(entity, event, payloads, { querier: this });
429
465
  }
430
466
  /**
431
467
  * Runs `task` after everything already queued on this querier, one at a time.
@@ -1,5 +1,5 @@
1
1
  import type { AbstractDialect } from '../dialect/index.js';
2
- import type { ExtraOptions, IdValue, PoolRunOptions, Querier, QuerierPool, Query, QueryAggMap, QueryAggregate, QueryAggregateResult, QueryConflictPaths, QueryGroupMap, QueryOne, QueryOptions, QuerySearch, QueryUpdateResult, TransactionOptions, Type, UpdatePayload } from '../type/index.js';
2
+ import type { EntityData, ExtraOptions, IdValue, PoolRunOptions, Querier, QuerierPool, Query, QueryAggMap, QueryAggregate, QueryAggregateResult, QueryConflictPaths, QueryGroupMap, QueryOne, QueryOptions, QuerySearch, QueryUpdateResult, TransactionOptions, Type, UpdatePayload } from '../type/index.js';
3
3
  /**
4
4
  * Base pool: dialect id and behavior come only from the `dialect` instance (see {@link QuerierPool}).
5
5
  */
@@ -35,14 +35,14 @@ export declare abstract class AbstractQuerierPool<Q extends Querier, D extends A
35
35
  findManyAndCount<E extends object>(entity: Type<E>, q: Query<E>, opts?: QueryOptions): Promise<[E[], number]>;
36
36
  count<E extends object>(entity: Type<E>, q?: QuerySearch<E>, opts?: QueryOptions): Promise<number>;
37
37
  aggregate<E extends object, const G extends QueryGroupMap<E>, const A extends QueryAggMap<E>>(entity: Type<E>, q: QueryAggregate<E, G, A>, opts?: QueryOptions): Promise<QueryAggregateResult<E, G, A>[]>;
38
- insertOne<E extends object>(entity: Type<E>, payload: E): Promise<IdValue<E> | undefined>;
39
- insertMany<E extends object>(entity: Type<E>, payload: E[]): Promise<IdValue<E>[]>;
38
+ insertOne<E extends object>(entity: Type<E>, payload: EntityData<E>): Promise<IdValue<E> | undefined>;
39
+ insertMany<E extends object>(entity: Type<E>, payload: EntityData<E>[]): Promise<IdValue<E>[]>;
40
40
  updateOneById<E extends object>(entity: Type<E>, id: IdValue<E>, payload: UpdatePayload<E>, opts?: QueryOptions): Promise<number>;
41
41
  updateMany<E extends object>(entity: Type<E>, q: QuerySearch<E>, payload: UpdatePayload<E>, opts?: QueryOptions): Promise<number>;
42
- upsertOne<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: E): Promise<QueryUpdateResult>;
43
- upsertMany<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: E[]): Promise<QueryUpdateResult>;
44
- saveOne<E extends object>(entity: Type<E>, payload: E): Promise<IdValue<E>>;
45
- saveMany<E extends object>(entity: Type<E>, payload: E[]): Promise<IdValue<E>[]>;
42
+ upsertOne<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: EntityData<E>): Promise<QueryUpdateResult>;
43
+ upsertMany<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: EntityData<E>[]): Promise<QueryUpdateResult>;
44
+ saveOne<E extends object>(entity: Type<E>, payload: EntityData<E>): Promise<IdValue<E>>;
45
+ saveMany<E extends object>(entity: Type<E>, payload: EntityData<E>[]): Promise<IdValue<E>[]>;
46
46
  deleteOneById<E extends object>(entity: Type<E>, id: IdValue<E>, opts?: QueryOptions): Promise<number>;
47
47
  deleteMany<E extends object>(entity: Type<E>, q: QuerySearch<E>, opts?: QueryOptions): Promise<number>;
48
48
  restoreOneById<E extends object>(entity: Type<E>, id: IdValue<E>): Promise<number>;
@@ -0,0 +1,42 @@
1
+ import type { AbstractSqlDialect } from '../dialect/index.js';
2
+ import type { SqlQuerier } from '../type/index.js';
3
+ import { AbstractSqlQuerierPool } from './abstractSqlQuerierPool.js';
4
+ /**
5
+ * Base pool for an engine that gives one connection per database and keeps it open for the pool's
6
+ * lifetime: every local SQLite driver, the embedded Turso engine, and PGlite.
7
+ *
8
+ * The handle is shared, but each acquisition gets its own querier, so transaction state stays per unit
9
+ * of work. That state is not *isolated*, which is the one way these differ from a real pool: there is a
10
+ * single connection under every querier, so two of them cannot hold independent transactions, and a
11
+ * unit of work that needs one needs its own pool and therefore its own database.
12
+ *
13
+ * What a second `BEGIN` then does is the engine's, not this class's: SQLite and the embedded Turso
14
+ * engine both refuse it ("cannot start a transaction within a transaction"), while PGlite accepts it
15
+ * into the transaction already open - see {@link PgliteQuerierPool}, which is why that one is worth
16
+ * saying out loud.
17
+ *
18
+ * Subclasses supply only how to open the handle and how to wrap it, the way {@link AbstractPgQuerierPool}
19
+ * takes `buildQuerier` alone. The lazy open and the close were written out once per pool before, along
20
+ * with three partial copies of the paragraph above.
21
+ *
22
+ * @remarks Deliberately not re-exported from `querier/index.ts`, which the root entry point re-exports:
23
+ * only the three driver entries need this, and each imports it by path, as `postgres/abstractPgQuerier.ts`
24
+ * is imported.
25
+ */
26
+ export declare abstract class AbstractSharedHandleQuerierPool<DB extends {
27
+ close(): unknown;
28
+ }, Q extends SqlQuerier, D extends AbstractSqlDialect> extends AbstractSqlQuerierPool<Q, D> {
29
+ /**
30
+ * The open, not the handle: `db ??= await openDb()` reads before the await and assigns after, so
31
+ * callers arriving while the first open is in flight each start one of their own. The extra handles
32
+ * are then unreachable and never closed, and on an in-memory database they are separate databases,
33
+ * so a querier built on one writes where nothing else will ever read.
34
+ */
35
+ private opening?;
36
+ /** Opens the one connection. Called on the first acquisition, and again after an {@link end}. */
37
+ protected abstract openDb(): Promise<DB>;
38
+ /** Wraps the shared handle in a querier: the only thing that varies between these pools. */
39
+ protected abstract buildQuerier(db: DB): Q;
40
+ getQuerier(): Promise<Q>;
41
+ end(): Promise<void>;
42
+ }
@@ -0,0 +1,48 @@
1
+ import { AbstractSqlQuerierPool } from './abstractSqlQuerierPool.js';
2
+ /**
3
+ * Base pool for an engine that gives one connection per database and keeps it open for the pool's
4
+ * lifetime: every local SQLite driver, the embedded Turso engine, and PGlite.
5
+ *
6
+ * The handle is shared, but each acquisition gets its own querier, so transaction state stays per unit
7
+ * of work. That state is not *isolated*, which is the one way these differ from a real pool: there is a
8
+ * single connection under every querier, so two of them cannot hold independent transactions, and a
9
+ * unit of work that needs one needs its own pool and therefore its own database.
10
+ *
11
+ * What a second `BEGIN` then does is the engine's, not this class's: SQLite and the embedded Turso
12
+ * engine both refuse it ("cannot start a transaction within a transaction"), while PGlite accepts it
13
+ * into the transaction already open - see {@link PgliteQuerierPool}, which is why that one is worth
14
+ * saying out loud.
15
+ *
16
+ * Subclasses supply only how to open the handle and how to wrap it, the way {@link AbstractPgQuerierPool}
17
+ * takes `buildQuerier` alone. The lazy open and the close were written out once per pool before, along
18
+ * with three partial copies of the paragraph above.
19
+ *
20
+ * @remarks Deliberately not re-exported from `querier/index.ts`, which the root entry point re-exports:
21
+ * only the three driver entries need this, and each imports it by path, as `postgres/abstractPgQuerier.ts`
22
+ * is imported.
23
+ */
24
+ export class AbstractSharedHandleQuerierPool extends AbstractSqlQuerierPool {
25
+ /**
26
+ * The open, not the handle: `db ??= await openDb()` reads before the await and assigns after, so
27
+ * callers arriving while the first open is in flight each start one of their own. The extra handles
28
+ * are then unreachable and never closed, and on an in-memory database they are separate databases,
29
+ * so a querier built on one writes where nothing else will ever read.
30
+ */
31
+ opening;
32
+ async getQuerier() {
33
+ // Cleared on failure, so a driver that could not start once is retried rather than refused forever.
34
+ this.opening ??= this.openDb().catch((err) => {
35
+ this.opening = undefined;
36
+ throw err;
37
+ });
38
+ return this.buildQuerier(await this.opening);
39
+ }
40
+ async end() {
41
+ const opening = this.opening;
42
+ this.opening = undefined;
43
+ // An open still in flight is awaited rather than abandoned: closing is what releases its file or port.
44
+ // One that failed leaves nothing to close, and `getQuerier` already reported it to its own caller.
45
+ const db = await opening?.catch(() => undefined);
46
+ await db?.close();
47
+ }
48
+ }
@@ -1,5 +1,5 @@
1
1
  import type { AbstractSqlDialect } from '../dialect/index.js';
2
- import type { ExtraOptions, IdValue, Query, QueryAggMap, QueryAggregate, QueryAggregateResult, QueryConflictPaths, QueryGroupMap, QueryOptions, QuerySearch, QueryUpdateResult, SqlQuerier, TransactionOptions, Type, UpdatePayload } from '../type/index.js';
2
+ import type { EntityData, ExtraOptions, IdValue, Query, QueryAggMap, QueryAggregate, QueryAggregateResult, QueryConflictPaths, QueryGroupMap, QueryOptions, QuerySearch, QueryUpdateResult, SqlQuerier, TransactionOptions, Type, UpdatePayload } from '../type/index.js';
3
3
  import type { BuildUpdateResultPayload } from '../util/sql.util.js';
4
4
  import { AbstractQuerier } from './abstractQuerier.js';
5
5
  export declare abstract class AbstractSqlQuerier extends AbstractQuerier implements SqlQuerier {
@@ -64,10 +64,12 @@ export declare abstract class AbstractSqlQuerier extends AbstractQuerier impleme
64
64
  private hydrateFields;
65
65
  protected internalCount<E extends object>(entity: Type<E>, q?: QuerySearch<E>, opts?: QueryOptions): Promise<number>;
66
66
  protected internalAggregate<E extends object, G extends QueryGroupMap<E>, A extends QueryAggMap<E>>(entity: Type<E>, q: QueryAggregate<E, G, A>, opts?: QueryOptions): Promise<QueryAggregateResult<E, G, A>[]>;
67
- internalInsertMany<E extends object>(entity: Type<E>, payload: E[]): Promise<IdValue<E>[]>;
67
+ internalInsertMany<E extends object>(entity: Type<E>, payload: EntityData<E>[]): Promise<IdValue<E>[]>;
68
68
  internalUpdateMany<E extends object>(entity: Type<E>, q: QuerySearch<E>, payload: UpdatePayload<E>, opts?: QueryOptions): Promise<number>;
69
- upsertOne<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: E): Promise<QueryUpdateResult>;
70
- upsertMany<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: E[]): Promise<QueryUpdateResult>;
69
+ /** The ids matching `q`, in `q`'s own order and page, so a write can name the rows it settled on. */
70
+ private settleIds;
71
+ upsertOne<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: EntityData<E>): Promise<QueryUpdateResult>;
72
+ upsertMany<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: EntityData<E>[]): Promise<QueryUpdateResult>;
71
73
  protected internalDeleteMany<E extends object>(entity: Type<E>, q: QuerySearch<E>, opts?: QueryOptions): Promise<number>;
72
74
  get hasOpenTransaction(): boolean;
73
75
  beginTransaction(opts?: TransactionOptions): Promise<void>;
@@ -3,6 +3,14 @@ import { getMeta } from '../entity/index.js';
3
3
  import { buildUpdateResult, cascadesOnDelete, clone, getInsertFieldKeys, getRelationRequestSummary, isAutoIncrement, obtainAttrsPaths, throwNoPendingTransaction, throwPendingTransaction, unflatObject, unflatObjects, withoutSoftDeleteFilter, } from '../util/index.js';
4
4
  import { AbstractQuerier } from './abstractQuerier.js';
5
5
  import { enrichError } from './queryError.js';
6
+ /**
7
+ * Whether `q` picks a specific slice of the matching rows rather than all of them. A write that
8
+ * does has to settle those rows and name them: `ORDER BY`/`LIMIT` on an UPDATE or DELETE is MySQL's
9
+ * alone.
10
+ */
11
+ function isPaged(q) {
12
+ return q.$sort !== undefined || q.$limit !== undefined || q.$skip !== undefined;
13
+ }
6
14
  export class AbstractSqlQuerier extends AbstractQuerier {
7
15
  dialect;
8
16
  extra;
@@ -219,12 +227,30 @@ export class AbstractSqlQuerier extends AbstractQuerier {
219
227
  }
220
228
  async internalUpdateMany(entity, q, payload, opts) {
221
229
  payload = clone(payload);
230
+ // Settled first for the reason `internalDeleteMany` settles: `ORDER BY`/`LIMIT` on an UPDATE is
231
+ // MySQL's alone, so a paged update has to name the rows it picked.
232
+ let target = q;
233
+ if (isPaged(q)) {
234
+ const ids = await this.settleIds(entity, q, opts);
235
+ if (!ids.length) {
236
+ return 0;
237
+ }
238
+ target = { $where: ids };
239
+ }
222
240
  const ctx = this.dialect.createContext();
223
- this.dialect.update(ctx, entity, q, payload, opts);
241
+ this.dialect.update(ctx, entity, target, payload, opts);
224
242
  const { changes = 0 } = await this.run(ctx.sql, ctx.values);
225
- await this.updateRelations(entity, q, payload, opts);
243
+ await this.updateRelations(entity, target, payload, opts);
226
244
  return changes;
227
245
  }
246
+ /** The ids matching `q`, in `q`'s own order and page, so a write can name the rows it settled on. */
247
+ async settleIds(entity, q, opts) {
248
+ const meta = getMeta(entity);
249
+ const ctx = this.dialect.createContext();
250
+ this.dialect.find(ctx, entity, { ...q, $select: { [meta.id]: true } }, opts);
251
+ const founds = await this.all(ctx.sql, ctx.values);
252
+ return founds.map((it) => it[meta.id]);
253
+ }
228
254
  async upsertOne(entity, conflictPaths, payload) {
229
255
  return this.upsertMany(entity, conflictPaths, [payload]);
230
256
  }
@@ -252,8 +278,7 @@ export class AbstractSqlQuerier extends AbstractQuerier {
252
278
  // parents' ids to find their children, and no engine but MySQL accepts `ORDER BY`/`LIMIT` on a
253
279
  // DELETE, so a paged delete has to name the rows it settled on. A plain predicate needs neither,
254
280
  // and there the round trip buys nothing: the statement can say what the caller already said.
255
- const hasPagination = q.$sort !== undefined || q.$limit !== undefined || q.$skip !== undefined;
256
- if (!hasPagination && !cascadesOnDelete(meta)) {
281
+ if (!isPaged(q) && !cascadesOnDelete(meta)) {
257
282
  const ctx = this.dialect.createContext();
258
283
  this.dialect.delete(ctx, entity, q, opts);
259
284
  const { changes = 0 } = await this.run(ctx.sql, ctx.values);
@@ -261,13 +286,10 @@ export class AbstractSqlQuerier extends AbstractQuerier {
261
286
  }
262
287
  // A hard delete also targets already-soft-deleted rows, so drop the soft-delete filter when finding ids.
263
288
  const findOpts = opts?.hardDelete ? { ...opts, filters: withoutSoftDeleteFilter(opts.filters) } : opts;
264
- const findCtx = this.dialect.createContext();
265
- this.dialect.find(findCtx, entity, { ...q, $select: { [meta.id]: true } }, findOpts);
266
- const founds = await this.all(findCtx.sql, findCtx.values);
267
- if (!founds.length) {
289
+ const ids = await this.settleIds(entity, q, findOpts);
290
+ if (!ids.length) {
268
291
  return 0;
269
292
  }
270
- const ids = founds.map((it) => it[meta.id]);
271
293
  // Children first: they hold the foreign key, so deleting the parent ahead of them is rejected
272
294
  // outright by any schema that declares the constraint without `ON DELETE CASCADE`.
273
295
  await this.deleteRelations(entity, ids, opts);
@@ -1,4 +1,4 @@
1
- import { AbstractSqlQuerierPool } from '../querier/index.js';
1
+ import { AbstractSharedHandleQuerierPool } from '../querier/abstractSharedHandleQuerierPool.js';
2
2
  import type { ExtraOptions } from '../type/index.js';
3
3
  import { SqliteDialect } from './sqliteDialect.js';
4
4
  import { type SqliteDatabase, SqliteQuerier } from './sqliteQuerier.js';
@@ -12,20 +12,17 @@ export type LocalSqlitePoolOptions = {
12
12
  extensions?: readonly string[];
13
13
  };
14
14
  /**
15
- * Pool for a SQLite database opened in this process, whichever driver provides it.
15
+ * Pool for a SQLite database opened in this process, whichever driver provides it. SQLite gives one
16
+ * connection per file, so the shared-handle lifecycle is {@link AbstractSharedHandleQuerierPool}'s.
16
17
  *
17
- * The handle is shared - SQLite gives one connection per file - but each acquisition gets its own
18
- * querier, so transaction state stays per unit of work. Subclasses supply only {@link createDb}: the
19
- * lifecycle, and loading the extensions on the way up, are the same for `better-sqlite3`, `bun:sqlite`
20
- * and `node:sqlite`, and were written out once per pool before.
18
+ * Subclasses supply only {@link createDb}: loading the extensions on the way up is the same for
19
+ * `better-sqlite3`, `bun:sqlite` and `node:sqlite`, and was written out once per pool before.
21
20
  */
22
- export declare abstract class AbstractLocalSqliteQuerierPool<O extends LocalSqlitePoolOptions> extends AbstractSqlQuerierPool<SqliteQuerier, SqliteDialect> {
21
+ export declare abstract class AbstractLocalSqliteQuerierPool<O extends LocalSqlitePoolOptions> extends AbstractSharedHandleQuerierPool<SqliteDatabase, SqliteQuerier, SqliteDialect> {
23
22
  readonly opts?: O | undefined;
24
- private db?;
25
23
  constructor(opts?: O | undefined, extra?: ExtraOptions);
26
24
  /** Opens the driver's database. Extensions are loaded by the caller, not here. */
27
25
  protected abstract createDb(): Promise<SqliteDatabase>;
28
- getQuerier(): Promise<SqliteQuerier>;
29
- private openDb;
30
- end(): Promise<void>;
26
+ protected openDb(): Promise<SqliteDatabase>;
27
+ protected buildQuerier(db: SqliteDatabase): SqliteQuerier;
31
28
  }
@@ -1,25 +1,19 @@
1
- import { AbstractSqlQuerierPool } from '../querier/index.js';
1
+ import { AbstractSharedHandleQuerierPool } from '../querier/abstractSharedHandleQuerierPool.js';
2
2
  import { SqliteDialect } from './sqliteDialect.js';
3
3
  import { SqliteQuerier } from './sqliteQuerier.js';
4
4
  /**
5
- * Pool for a SQLite database opened in this process, whichever driver provides it.
5
+ * Pool for a SQLite database opened in this process, whichever driver provides it. SQLite gives one
6
+ * connection per file, so the shared-handle lifecycle is {@link AbstractSharedHandleQuerierPool}'s.
6
7
  *
7
- * The handle is shared - SQLite gives one connection per file - but each acquisition gets its own
8
- * querier, so transaction state stays per unit of work. Subclasses supply only {@link createDb}: the
9
- * lifecycle, and loading the extensions on the way up, are the same for `better-sqlite3`, `bun:sqlite`
10
- * and `node:sqlite`, and were written out once per pool before.
8
+ * Subclasses supply only {@link createDb}: loading the extensions on the way up is the same for
9
+ * `better-sqlite3`, `bun:sqlite` and `node:sqlite`, and was written out once per pool before.
11
10
  */
12
- export class AbstractLocalSqliteQuerierPool extends AbstractSqlQuerierPool {
11
+ export class AbstractLocalSqliteQuerierPool extends AbstractSharedHandleQuerierPool {
13
12
  opts;
14
- db;
15
13
  constructor(opts, extra) {
16
14
  super(new SqliteDialect({ namingStrategy: extra?.namingStrategy }), extra);
17
15
  this.opts = opts;
18
16
  }
19
- async getQuerier() {
20
- this.db ??= await this.openDb();
21
- return new SqliteQuerier(this.db, this.dialect, this.extra);
22
- }
23
17
  async openDb() {
24
18
  const db = await this.createDb();
25
19
  for (const extension of this.opts?.extensions ?? []) {
@@ -27,8 +21,7 @@ export class AbstractLocalSqliteQuerierPool extends AbstractSqlQuerierPool {
27
21
  }
28
22
  return db;
29
23
  }
30
- async end() {
31
- await this.db?.close();
32
- this.db = undefined;
24
+ buildQuerier(db) {
25
+ return new SqliteQuerier(db, this.dialect, this.extra);
33
26
  }
34
27
  }