turbine-orm 0.22.0 → 0.23.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/dist/powdb.js CHANGED
@@ -201,18 +201,32 @@ function isDateColumn(col) {
201
201
  * Generate PowQL DDL (`type T { … }`) for every table in a schema. Used to
202
202
  * provision a PowDB database from a code-first `defineSchema`/`SchemaMetadata`
203
203
  * (PowDB has no migration runner yet). The primary key column is declared
204
- * `required unique`; non-nullable columns are `required`.
204
+ * `required unique`; non-nullable columns are `required`. A server-generated
205
+ * column ({@link ColumnMetadata.isGenerated}) that maps to PowQL `int` gets the
206
+ * `auto` modifier, so PowDB assigns a monotonic id on insert and Turbine stops
207
+ * synthesizing a client-side value for it.
205
208
  */
206
209
  export function powqlSchemaDDL(schema) {
207
210
  const stmts = [];
208
211
  for (const meta of Object.values(schema.tables)) {
209
212
  const pkSet = new Set(meta.primaryKey);
213
+ // PowDB's `unique` is single-column only — there is no composite-unique
214
+ // constraint (`add unique` takes one `.column`). So the per-field `unique`
215
+ // modifier is emitted only for a single-column PK; a composite PK (e.g. a
216
+ // m2m junction's `(source_id, target_id)`) marks its columns `required` but
217
+ // cannot enforce the tuple's uniqueness at the engine level.
218
+ const pkIsSingle = meta.primaryKey.length === 1;
210
219
  const fields = meta.columns.map((col) => {
211
220
  const mods = [];
212
221
  if (!col.nullable || pkSet.has(col.name))
213
222
  mods.push('required');
214
- if (pkSet.has(col.name))
223
+ if (pkSet.has(col.name) && pkIsSingle)
215
224
  mods.push('unique');
225
+ // `auto` = server-generated monotonic int. PowDB requires it be `int` and
226
+ // rejects it alongside a `default`; non-int generated columns fall back to
227
+ // a plain typed column (Turbine assigns the value client-side instead).
228
+ if (col.isGenerated && powqlColumnType(col) === 'int')
229
+ mods.push('auto');
216
230
  return ` ${mods.join(' ')}${mods.length ? ' ' : ''}${col.name}: ${powqlColumnType(col)}`;
217
231
  });
218
232
  stmts.push(`type ${meta.name} {\n${fields.join(',\n')}\n}`);
package/dist/powql.d.ts CHANGED
@@ -15,10 +15,20 @@
15
15
  * - `create`/`createMany`/`update`/`delete` use PowDB 0.7.0's trailing
16
16
  * `returning` keyword (`RETURNING *`, all columns) to surface affected rows
17
17
  * in one round-trip. `upsert` is the lone exception — its statement does not
18
- * accept `returning`, so it reselects the row by PK.
19
- * - The PK is generated client-side (UUID) when the column has a default.
20
- * - `with` (nested relations) degrades to **batched N+1 loaders** D
21
- * round-trips for depth D, not one query. manyToMany is Phase B.
18
+ * accept `returning`, so it reselects the row by PK (a composite-PK upsert
19
+ * reselects-or-writes inside one flat transaction).
20
+ * - The PK is server-assigned when the column is `isGenerated` (PowDB's `auto`
21
+ * int read back via `returning`); otherwise a defaulted **string** PK is
22
+ * generated client-side (UUID).
23
+ * - `with` (nested relations) uses **batched N+1 loaders** — D round-trips for
24
+ * depth D, not one query — including manyToMany (junction → targets).
25
+ * - **Relation filters** (`some`/`none`/`every`, all cardinalities incl. m2m)
26
+ * are resolved client-side to a literal `in (…)` list, never an IN-subquery:
27
+ * PowDB's executor caches a subquery's result by plan shape and would return
28
+ * a stale prior result for a later subquery of the same shape.
29
+ * - **Nested writes** (relation ops in `create`/`update` data) run through the
30
+ * shared nested-write engine as one flat top-level transaction (PowDB is
31
+ * single-writer, no savepoints).
22
32
  * - pgvector / JSON / array filters and cursor streaming throw
23
33
  * {@link UnsupportedFeatureError} (E017) — they have no PowDB equivalent.
24
34
  *
@@ -87,11 +97,25 @@ export declare class PowqlInterface<T extends object = Record<string, unknown>>
87
97
  /** `lhs [not] in ($1, $2, …)` — empty list collapses to a constant. */
88
98
  private buildInList;
89
99
  /**
90
- * Relation filter (`some`/`none`/`every`) via the verified IN-subquery form
91
- * `Outer filter .localKey in (Target filter <e> { .targetKey })`. `exists`
92
- * correlation is unreliable on PowDB, so it is never used.
100
+ * Pre-resolve every relation filter (`some`/`none`/`every`) in a where clause
101
+ * into a plain scalar `in`/`notIn` condition on the **local key**, by running
102
+ * the inner predicate as its own query and materializing the matching keys as
103
+ * a literal list. The compiled where is then relation-free and `buildWhere`
104
+ * emits only `in (<literal list>)`.
105
+ *
106
+ * Why not an IN-subquery (`.k in (Target filter <e> { .fk })`)? PowDB's
107
+ * executor caches a subquery's result by **plan shape, ignoring the literal**,
108
+ * so a second subquery of the same shape with a different value returns the
109
+ * first one's stale rows (reproduced live on the embedded engine; the
110
+ * single-statement literal `in (list)` form is always correct). Resolving
111
+ * client-side trades extra round-trips for correctness, and recurses — nested
112
+ * relation filters in the inner predicate resolve when the target query runs.
93
113
  */
94
- private buildRelationFilter;
114
+ private resolveRelationFilters;
115
+ /** Resolve one hasMany/hasOne/belongsTo filter to `{ localField: { in|notIn: [...] } }`. */
116
+ private resolveRelationCondition;
117
+ /** Resolve a manyToMany filter through the junction to `{ sourceRefField: { in|notIn: [...] } }`. */
118
+ private resolveManyToManyCondition;
95
119
  /** Resolve the set of columns to project, honouring `select` / `omit`. */
96
120
  private projectedColumns;
97
121
  /** `{ .c1, .c2, … }` projection clause. */
@@ -114,9 +138,23 @@ export declare class PowqlInterface<T extends object = Record<string, unknown>>
114
138
  findFirstOrThrow(args?: FindManyArgs<T>): Promise<T>;
115
139
  /** Load each requested relation for `parents` and attach it onto each row. */
116
140
  private loadRelations;
141
+ /**
142
+ * manyToMany nested read — a three-hop batched loader (no `json_agg`/join
143
+ * pushdown): (1) read the junction rows for all parents in `sourceKey in (…)`
144
+ * chunks, (2) read the target rows for the collected `targetKey`s, (3) stitch
145
+ * each parent → its junction rows → its targets in memory. Mirrors the
146
+ * single-key N+1 loaders; the junction's source/target columns must be single
147
+ * (composite junction keys would need PowQL tuple-`in`, which it lacks).
148
+ */
149
+ private loadManyToMany;
117
150
  /** Split `data` into scalar assignments; reject relation (nested-write) keys. */
118
151
  private scalarData;
119
- /** Fill in a client-generated UUID for a defaulted PK that wasn't supplied. */
152
+ /**
153
+ * Fill in a client-generated UUID for a defaulted **string** PK that wasn't
154
+ * supplied. A server-generated PK ({@link ColumnMetadata.isGenerated}, e.g. an
155
+ * `int` column with PowDB's `auto` modifier) is left untouched — PowDB assigns
156
+ * it and the trailing `returning` reads it back — as is any non-string PK.
157
+ */
120
158
  private applyPkDefault;
121
159
  create(args: CreateArgs<T>): Promise<T>;
122
160
  createMany(args: CreateManyArgs<T>): Promise<T[]>;
@@ -126,11 +164,25 @@ export declare class PowqlInterface<T extends object = Record<string, unknown>>
126
164
  }>;
127
165
  /** Compile `data` into PowQL update assignments, including atomic operators. */
128
166
  private buildUpdateAssignments;
167
+ private isTxScoped;
168
+ private nestedCreate;
169
+ private nestedUpdate;
170
+ /** Open a flat PowDB transaction on a pinned connection and run `fn` inside it. */
171
+ private runInImplicitTx;
172
+ /** Already inside a transaction: build a context whose table accessors reuse the pinned pool. */
173
+ private buildNestedCtx;
129
174
  delete(args: DeleteArgs<T>): Promise<T>;
130
175
  deleteMany(args: DeleteManyArgs<T>): Promise<{
131
176
  count: number;
132
177
  }>;
133
178
  upsert(args: UpsertArgs<T>): Promise<T>;
179
+ /**
180
+ * Composite-key upsert: PowQL's `upsert … on .col` only takes one conflict
181
+ * column, so reselect by the full composite PK and update-or-create inside one
182
+ * flat transaction (PowDB single-writer makes the read-then-write safe from
183
+ * concurrent writers; the transaction makes it atomic with the write).
184
+ */
185
+ private upsertComposite;
134
186
  count(args?: CountArgs<T>): Promise<number>;
135
187
  aggregate(args: AggregateArgs<T>): Promise<AggregateResult<T>>;
136
188
  groupBy(args: GroupByArgs<T>): Promise<Record<string, unknown>[]>;