turbine-orm 0.19.1 → 0.21.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 (53) hide show
  1. package/README.md +156 -24
  2. package/dist/adapters/cockroachdb.js +4 -9
  3. package/dist/cjs/adapters/cockroachdb.js +4 -9
  4. package/dist/cjs/cli/index.js +23 -12
  5. package/dist/cjs/cli/observe-ui.js +12 -2
  6. package/dist/cjs/cli/observe.js +6 -7
  7. package/dist/cjs/cli/studio-ui.generated.js +1 -1
  8. package/dist/cjs/cli/studio.js +6 -37
  9. package/dist/cjs/client.js +45 -33
  10. package/dist/cjs/dialect.js +116 -0
  11. package/dist/cjs/errors.js +19 -1
  12. package/dist/cjs/generate.js +4 -0
  13. package/dist/cjs/index.js +3 -2
  14. package/dist/cjs/introspect.js +20 -0
  15. package/dist/cjs/mssql.js +1338 -0
  16. package/dist/cjs/mysql.js +1052 -0
  17. package/dist/cjs/query/builder.js +408 -122
  18. package/dist/cjs/query/utils.js +1 -0
  19. package/dist/cjs/sqlite.js +849 -0
  20. package/dist/cjs/typed-sql.js +9 -7
  21. package/dist/cli/index.js +23 -12
  22. package/dist/cli/migrate.d.ts +2 -2
  23. package/dist/cli/observe-ui.d.ts +1 -1
  24. package/dist/cli/observe-ui.js +12 -2
  25. package/dist/cli/observe.js +7 -8
  26. package/dist/cli/studio-ui.generated.js +1 -1
  27. package/dist/cli/studio.d.ts +0 -10
  28. package/dist/cli/studio.js +7 -37
  29. package/dist/client.d.ts +35 -14
  30. package/dist/client.js +46 -34
  31. package/dist/dialect.d.ts +258 -1
  32. package/dist/dialect.js +83 -0
  33. package/dist/errors.d.ts +12 -0
  34. package/dist/errors.js +17 -0
  35. package/dist/generate.js +4 -0
  36. package/dist/index.d.ts +4 -4
  37. package/dist/index.js +1 -1
  38. package/dist/introspect.d.ts +18 -0
  39. package/dist/introspect.js +19 -0
  40. package/dist/mssql.d.ts +233 -0
  41. package/dist/mssql.js +1298 -0
  42. package/dist/mysql.d.ts +174 -0
  43. package/dist/mysql.js +1012 -0
  44. package/dist/query/builder.d.ts +105 -6
  45. package/dist/query/builder.js +409 -123
  46. package/dist/query/index.d.ts +2 -2
  47. package/dist/query/types.d.ts +62 -12
  48. package/dist/query/utils.js +1 -0
  49. package/dist/sqlite.d.ts +144 -0
  50. package/dist/sqlite.js +842 -0
  51. package/dist/typed-sql.d.ts +7 -5
  52. package/dist/typed-sql.js +9 -7
  53. package/package.json +32 -3
package/dist/dialect.d.ts CHANGED
@@ -5,7 +5,8 @@
5
5
  * PostgreSQL-native by default, but query generation now depends on this
6
6
  * contract for the SQL primitives that vary across MySQL and SQLite.
7
7
  */
8
- import { type SchemaMetadata } from './schema.js';
8
+ import type { WithOptions } from './query/types.js';
9
+ import { type RelationDef, type SchemaMetadata, type TableMetadata } from './schema.js';
9
10
  export type DialectName = 'postgresql' | 'mysql' | 'sqlite' | (string & {});
10
11
  export interface InsertStatementInput {
11
12
  /** SQL-ready quoted table name. */
@@ -86,9 +87,136 @@ export interface CreateIndexStatementInput {
86
87
  /** SQL-ready quoted index columns. */
87
88
  columns: string[];
88
89
  }
90
+ /**
91
+ * How a dialect surfaces the row(s) produced by an INSERT/UPDATE/DELETE/upsert.
92
+ *
93
+ * - `'returning'` — a trailing `RETURNING *` clause returns the affected rows
94
+ * in the same statement (PostgreSQL, SQLite ≥ 3.35). The executor reads them
95
+ * directly from the statement result.
96
+ * - `'output'` — the statement itself emits the rows in a non-RETURNING shape
97
+ * (SQL Server `OUTPUT INSERTED.*`). Executed exactly like `'returning'`: the
98
+ * rows come back on the statement result.
99
+ * - `'reselect'` — the engine cannot return rows from a write (MySQL). The
100
+ * executor runs the write, then issues a follow-up `SELECT` (by primary
101
+ * key / unique / where predicate) to fetch the affected row(s). The build
102
+ * method supplies the {@link DeferredQuery} `reselect` plan that owns the
103
+ * statement ordering (write-then-select for create/update/upsert;
104
+ * select-then-write for delete, whose row is gone after the statement runs).
105
+ */
106
+ export type ResultStrategy = 'returning' | 'output' | 'reselect';
107
+ /**
108
+ * Minimal connection surface needed to drive a server-side stream (cursor /
109
+ * driver iterator). `pg.PoolClient` and `PgCompatPoolClient` both satisfy it.
110
+ * Declared locally so {@link Dialect.openStream} stays free of an import cycle
111
+ * back into `client.ts`.
112
+ */
113
+ export interface StreamableConnection {
114
+ query(text: string, values?: unknown[]): Promise<{
115
+ rows: Record<string, unknown>[];
116
+ }>;
117
+ }
118
+ /**
119
+ * Inputs for {@link Dialect.buildUpdateStatement} — full UPDATE assembly. Used by
120
+ * engines whose returning shape is injected MID-statement rather than as a trailing
121
+ * clause (SQL Server `OUTPUT INSERTED.*` lands between `SET …` and `WHERE …`).
122
+ */
123
+ export interface UpdateStatementInput {
124
+ /** SQL-ready quoted table name. */
125
+ table: string;
126
+ /** SQL-ready `col = expr` assignments. */
127
+ setClauses: string[];
128
+ /** SQL-ready WHERE fragment INCLUDING the leading ` WHERE ` (or '' for none). */
129
+ whereSql: string;
130
+ /** SQL-ready returning selection (default `*`). */
131
+ returning?: string;
132
+ }
133
+ /**
134
+ * Inputs for {@link Dialect.buildDeleteStatement} — full DELETE assembly. SQL Server
135
+ * injects `OUTPUT DELETED.*` between `DELETE FROM <t>` and `WHERE …`.
136
+ */
137
+ export interface DeleteStatementInput {
138
+ /** SQL-ready quoted table name. */
139
+ table: string;
140
+ /** SQL-ready WHERE fragment INCLUDING the leading ` WHERE ` (or '' for none). */
141
+ whereSql: string;
142
+ /** SQL-ready returning selection (default `*`). */
143
+ returning?: string;
144
+ }
145
+ /**
146
+ * Inputs for {@link Dialect.buildLimitOffset} — the trailing pagination clause of an
147
+ * outer SELECT. PostgreSQL/MySQL/SQLite use `LIMIT x [OFFSET y]`; SQL Server has no
148
+ * `LIMIT` and uses `[ORDER BY …] OFFSET y ROWS [FETCH NEXT x ROWS ONLY]` (which
149
+ * requires an ORDER BY — a stable default is injected when {@link hasOrderBy} is false).
150
+ */
151
+ export interface LimitOffsetInput {
152
+ /** SQL-ready placeholder/literal for the row LIMIT, or undefined for no limit. */
153
+ limitPlaceholder?: string;
154
+ /** SQL-ready placeholder/literal for the OFFSET, or undefined for no offset. */
155
+ offsetPlaceholder?: string;
156
+ /** Whether the outer SELECT already carries an ORDER BY (so none must be injected). */
157
+ hasOrderBy: boolean;
158
+ }
159
+ /**
160
+ * Everything an engine needs to OVERRIDE nested-relation subquery generation, for
161
+ * dialects whose JSON-aggregation shape is fundamentally different from PostgreSQL's
162
+ * `json_agg(json_build_object(...))` (SQL Server's `FOR JSON PATH` expresses the
163
+ * object shape through the child SELECT's column ALIASES rather than an explicit
164
+ * `JSON_OBJECT`, so it cannot be assembled from {@link Dialect.buildJsonObject} /
165
+ * {@link Dialect.buildJsonArrayAgg} primitives — see {@link Dialect.buildRelationSubquery}).
166
+ *
167
+ * The query builder pre-resolves the parts that are engine-independent (alias,
168
+ * select/omit-resolved columns, recursion + param threading) and hands them to the
169
+ * dialect. **Param-push ordering contract:** the override MUST push values to
170
+ * {@link params} in the same order the builder's collect path expects so the SQL
171
+ * cache and pipeline batching stay in sync —
172
+ * - to-many with `limit`/`orderBy` (the "wrap" path) and manyToMany:
173
+ * `buildWhere(...)` → push `limit` → `recurse(...)` for each nested relation;
174
+ * - everything else (to-one, unordered/unlimited to-many): `recurse(...)` for each
175
+ * nested relation → `buildWhere(...)` (no limit param).
176
+ */
177
+ export interface RelationSubqueryContext {
178
+ /** The relation being expanded (its `type`, `to`, `foreignKey`/`referenceKey`, `through`). */
179
+ relDef: RelationDef;
180
+ /** The `with` spec for this relation: `true`, or a {@link WithOptions} object. */
181
+ spec: true | WithOptions;
182
+ /** Shared parameter array — push BOUND values here in the order described above. */
183
+ params: unknown[];
184
+ /** Parent alias or table name (RAW identifier, not quoted) to correlate against. */
185
+ parentRef: string;
186
+ /** Pre-allocated unique alias for this relation's target rows (e.g. `t0`). */
187
+ alias: string;
188
+ /** Target table name (snake_case). */
189
+ targetTable: string;
190
+ /** Target table metadata. */
191
+ targetMeta: TableMetadata;
192
+ /** Resolved target columns (snake_case) honoring `select` / `omit`. */
193
+ targetColumns: string[];
194
+ /** Current recursion depth (for nested {@link recurse} calls, pass `depth + 1`). */
195
+ depth: number;
196
+ /** Breadcrumb path of relation/table names for circular-relation errors. */
197
+ path: string[];
198
+ /** Quote a RAW identifier through the active dialect. */
199
+ quote(name: string): string;
200
+ /**
201
+ * Build a WHERE fragment for `spec.where` against `whereAlias`, PUSHING its params
202
+ * to {@link params}. Returns '' when the spec has no `where`. Call exactly once.
203
+ */
204
+ buildWhere(whereAlias: string): string;
205
+ /**
206
+ * Recurse to build a nested relation's subquery (PUSHING its params to
207
+ * {@link params}). Uses the shared alias counter, so nested aliases never collide.
208
+ */
209
+ recurse(relDef: RelationDef, spec: true | WithOptions, parentRef: string, depth: number, path: string[]): string;
210
+ }
89
211
  export interface Dialect {
90
212
  /** Dialect identifier. */
91
213
  readonly name: DialectName;
214
+ /**
215
+ * How write statements surface their affected rows. PostgreSQL uses
216
+ * `'returning'`; the executor branches on this so non-RETURNING engines
217
+ * (MySQL `'reselect'`, SQL Server `'output'`) can still return rows.
218
+ */
219
+ readonly resultStrategy: ResultStrategy;
92
220
  /** Parameter placeholder for the Nth value, using a 1-indexed public count. */
93
221
  paramPlaceholder(index: number): string;
94
222
  /** Quote a SQL identifier (table, column, cursor, alias). */
@@ -103,8 +231,42 @@ export interface Dialect {
103
231
  buildJsonObject(pairs: [key: string, expr: string][]): string;
104
232
  /** Build a JSON array aggregation expression with a dialect-specific empty-array fallback. */
105
233
  buildJsonArrayAgg(jsonObjectExpr: string, orderBy?: string): string;
234
+ /**
235
+ * Whether the array-aggregate (`buildJsonArrayAgg`) can take an inline
236
+ * `ORDER BY` argument. PostgreSQL's `json_agg(... ORDER BY ...)` can, so this
237
+ * is `true`. Engines whose array aggregate has no ORDER BY argument
238
+ * (MySQL `JSON_ARRAYAGG`, SQLite `json_group_array`) set this `false` to force
239
+ * the inner-subquery rewrite for every ordered to-many relation.
240
+ */
241
+ readonly aggSupportsInlineOrderBy: boolean;
242
+ /**
243
+ * Render `LIMIT` / `OFFSET` as inline integer literals instead of bound
244
+ * parameters. MySQL sets this `true`: mysql2's binary (prepared) protocol sends
245
+ * JS numbers as `DOUBLE`, which MySQL's `LIMIT`/`OFFSET` reject ("Incorrect
246
+ * arguments to mysqld_stmt_execute"). The values are Turbine-validated
247
+ * non-negative integers (never user strings), so inlining is injection-safe.
248
+ * PostgreSQL / SQLite / SQL Server leave this falsy and keep pagination
249
+ * parameterized, so their generated SQL stays byte-identical.
250
+ */
251
+ readonly inlineLimitOffset?: boolean;
252
+ /**
253
+ * Wrap a correlated nested-relation subquery (and its empty/null fallback) for
254
+ * embedding as a JSON value in a parent `json_build_object`. PostgreSQL emits
255
+ * `COALESCE((subquery), fallback)`. SQLite must additionally `json(...)`-wrap
256
+ * the subquery (its `json_group_array` double-encodes nested objects);
257
+ * SQL Server uses `ISNULL((... FOR JSON PATH), '[]')`.
258
+ */
259
+ wrapJsonSubresult(subquery: string, fallback: string): string;
106
260
  /** Whether INSERT/UPDATE/DELETE support RETURNING rows. */
107
261
  readonly supportsReturning: boolean;
262
+ /** Whether this dialect/engine supports pgvector distance ops (KNN / distance WHERE). */
263
+ readonly supportsVector: boolean;
264
+ /** Whether this dialect/engine supports LISTEN/NOTIFY realtime pub/sub. */
265
+ readonly supportsListenNotify: boolean;
266
+ /** Whether this dialect/engine supports row-level-security session GUCs (set_config). */
267
+ readonly supportsRLS: boolean;
268
+ /** Whether this dialect/engine supports advisory-lock-style migration locking. */
269
+ readonly supportsAdvisoryLock: boolean;
108
270
  /** Build a dialect-specific RETURNING clause. Return an empty string when unsupported. */
109
271
  buildReturningClause(selection?: string): string;
110
272
  /** Build a single-row INSERT statement. Inputs are SQL-ready quoted fragments. */
@@ -127,6 +289,32 @@ export interface Dialect {
127
289
  buildCorrelation(leftRef: string, leftColumns: string | string[], rightRef: string, rightColumns: string | string[]): string;
128
290
  /** Optional type mapping hook for code generation/introspection. */
129
291
  typeToTypeScript?(dialectType: string, nullable: boolean): string;
292
+ /**
293
+ * Cast an aggregate result expression to an integer or float SQL type.
294
+ * PostgreSQL uses the postfix casts `expr::int` / `expr::float`; portable
295
+ * engines (e.g. SQLite, which has no `::` cast operator) emit
296
+ * `CAST(expr AS INTEGER/REAL)`. Optional: when a dialect omits it, the query
297
+ * builder falls back to the PostgreSQL postfix cast, so dialects that predate
298
+ * this hook keep emitting byte-identical SQL.
299
+ */
300
+ castAggregate?(expr: string, target: 'int' | 'float'): string;
301
+ /**
302
+ * Build a membership (`IN` / `NOT IN`) predicate from a column/expression and
303
+ * a single bound parameter reference. PostgreSQL binds the whole list as one
304
+ * array param (`expr = ANY($n)` / `expr != ALL($n)`), so the placeholder count
305
+ * stays independent of the list length and the SQL cache remains valid.
306
+ * Engines without array parameters (SQLite) override this together with
307
+ * {@link inClauseParam} to use a length-independent single-placeholder form
308
+ * (e.g. `expr IN (SELECT value FROM json_each(?))`). Optional: the query
309
+ * builder falls back to the PostgreSQL `ANY`/`ALL` form when absent.
310
+ */
311
+ buildInClause?(expr: string, paramRef: string, negated: boolean): string;
312
+ /**
313
+ * The single bound value for an `IN` list (paired with {@link buildInClause}).
314
+ * PostgreSQL passes the array through unchanged; SQLite serializes it to a
315
+ * JSON string consumed by `json_each`. Optional: defaults to the array itself.
316
+ */
317
+ inClauseParam?(values: unknown[]): unknown;
130
318
  /** Optional array-cast hook for bulk insert implementations. */
131
319
  arrayType?(baseType: string): string;
132
320
  /** Map a schema-builder column type to dialect DDL. */
@@ -149,6 +337,68 @@ export interface Dialect {
149
337
  buildMigrationInsertApplied(table: string): string;
150
338
  /** Build the query that deletes a rolled-back migration record. */
151
339
  buildMigrationDeleteApplied(table: string): string;
340
+ /** `BEGIN`, optionally with a SQL-ready isolation-level suffix. */
341
+ beginStatement(isolationLevel?: string): string;
342
+ /** Commit the current transaction. */
343
+ commitStatement(): string;
344
+ /** Roll back the current transaction. */
345
+ rollbackStatement(): string;
346
+ /** Establish a savepoint with the given (SQL-ready) name. */
347
+ savepointStatement(name: string): string;
348
+ /** Release the named savepoint. */
349
+ releaseSavepointStatement(name: string): string;
350
+ /** Roll back to the named savepoint. */
351
+ rollbackToSavepointStatement(name: string): string;
352
+ /**
353
+ * Build a transaction-local session-config (GUC) assignment for RLS /
354
+ * multi-tenant context. Both name and value are bound parameters.
355
+ */
356
+ buildSetSessionConfig(name: string, value: string): BuiltStatement;
357
+ /**
358
+ * Drive a server-side stream of result rows on a single connection, yielding
359
+ * row batches of up to `batchSize`. PostgreSQL uses a `DECLARE CURSOR` /
360
+ * `FETCH` / `CLOSE` loop inside a transaction; other engines use their
361
+ * driver's native streaming iterator.
362
+ */
363
+ openStream(connection: StreamableConnection, sql: string, params: unknown[], batchSize: number): AsyncGenerator<Record<string, unknown>[], void, undefined>;
364
+ /**
365
+ * Schema introspector for this engine. `introspect()` routes through it so
366
+ * engines can override the catalog SQL. PostgreSQL wraps the
367
+ * information_schema / pg_catalog reader in `introspect.ts`.
368
+ */
369
+ readonly introspector?: DialectIntrospector;
370
+ /**
371
+ * Build the trailing pagination clause for an OUTER SELECT. Optional: when a
372
+ * dialect omits it, the query builder emits the PostgreSQL form
373
+ * (` LIMIT <ph>` and/or ` OFFSET <ph>`), so PG/MySQL/SQLite stay byte-identical.
374
+ * SQL Server has no `LIMIT`, so it implements this to emit
375
+ * `[ORDER BY (SELECT NULL)] OFFSET <off> ROWS [FETCH NEXT <lim> ROWS ONLY]`.
376
+ */
377
+ buildLimitOffset?(input: LimitOffsetInput): string;
378
+ /**
379
+ * Assemble a full UPDATE statement. Optional: omitted by engines whose returning
380
+ * shape is a trailing clause (PG/SQLite `RETURNING`, MySQL none) — the builder
381
+ * falls back to `UPDATE <t> SET <set> <where><returningClause>`. SQL Server
382
+ * implements this to inject `OUTPUT INSERTED.*` between `SET …` and `WHERE …`.
383
+ */
384
+ buildUpdateStatement?(input: UpdateStatementInput): string;
385
+ /**
386
+ * Assemble a full DELETE statement. Optional: see {@link buildUpdateStatement}.
387
+ * SQL Server injects `OUTPUT DELETED.*` between `DELETE FROM <t>` and `WHERE …`.
388
+ */
389
+ buildDeleteStatement?(input: DeleteStatementInput): string;
390
+ /**
391
+ * OVERRIDE nested-relation subquery generation entirely. Optional: when a dialect
392
+ * omits it, the builder uses its native `json_agg(json_build_object(...))` path
393
+ * (PG) routed through {@link buildJsonObject} / {@link buildJsonArrayAgg} /
394
+ * {@link wrapJsonSubresult} — so MySQL and SQLite, which only swap those
395
+ * primitives, produce identical output and never define this hook. SQL Server
396
+ * defines it to emit `FOR JSON PATH` correlated subqueries (its JSON aggregate
397
+ * is expressed through the child SELECT's column aliases, which does not map onto
398
+ * the primitive hooks). See {@link RelationSubqueryContext} for the
399
+ * param-push-ordering contract the override must honor.
400
+ */
401
+ buildRelationSubquery?(ctx: RelationSubqueryContext): string;
152
402
  }
153
403
  export interface DialectIntrospector {
154
404
  introspect(options: IntrospectOptions): Promise<SchemaMetadata>;
@@ -159,6 +409,13 @@ export interface IntrospectOptions {
159
409
  include?: string[];
160
410
  exclude?: string[];
161
411
  }
412
+ /**
413
+ * @deprecated Migration locking is owned by the {@link DatabaseAdapter} seam
414
+ * (`src/adapters/index.ts` — `acquireLock`/`releaseLock`/`statementTimeout`),
415
+ * which is the single canonical locking seam. This interface was never
416
+ * implemented or wired and is retained only for type back-compat; new engines
417
+ * MUST provide a `DatabaseAdapter` instead. Will be removed in a future major.
418
+ */
162
419
  export interface DialectMigrator {
163
420
  acquireLock(lockId: number): Promise<boolean>;
164
421
  releaseLock(lockId: number): Promise<void>;
package/dist/dialect.js CHANGED
@@ -10,11 +10,17 @@ import { pgArrayType, pgTypeToTs } from './schema.js';
10
10
  /** PostgreSQL implementation of the dialect contract. */
11
11
  export const postgresDialect = {
12
12
  name: 'postgresql',
13
+ resultStrategy: 'returning',
13
14
  supportsReturning: true,
14
15
  supportsILike: true,
15
16
  jsonPathSupport: 'native',
16
17
  emptyJsonArrayLiteral: "'[]'::json",
17
18
  nullJsonLiteral: 'NULL',
19
+ aggSupportsInlineOrderBy: true,
20
+ supportsVector: true,
21
+ supportsListenNotify: true,
22
+ supportsRLS: true,
23
+ supportsAdvisoryLock: true,
18
24
  paramPlaceholder(index) {
19
25
  return `$${index}`;
20
26
  },
@@ -32,6 +38,9 @@ export const postgresDialect = {
32
38
  const suffix = orderBy ? ` ${orderBy}` : '';
33
39
  return `COALESCE(json_agg(${jsonObjectExpr}${suffix}), ${this.emptyJsonArrayLiteral})`;
34
40
  },
41
+ wrapJsonSubresult(subquery, fallback) {
42
+ return `COALESCE((${subquery}), ${fallback})`;
43
+ },
35
44
  buildReturningClause(selection = '*') {
36
45
  return ` RETURNING ${selection}`;
37
46
  },
@@ -73,6 +82,15 @@ export const postgresDialect = {
73
82
  typeToTypeScript(dialectType, nullable) {
74
83
  return pgTypeToTs(dialectType, nullable);
75
84
  },
85
+ castAggregate(expr, target) {
86
+ return `${expr}::${target}`;
87
+ },
88
+ buildInClause(expr, paramRef, negated) {
89
+ return negated ? `${expr} != ALL(${paramRef})` : `${expr} = ANY(${paramRef})`;
90
+ },
91
+ inClauseParam(values) {
92
+ return values;
93
+ },
76
94
  arrayType(baseType) {
77
95
  return pgArrayType(baseType);
78
96
  },
@@ -128,5 +146,70 @@ export const postgresDialect = {
128
146
  buildMigrationDeleteApplied(table) {
129
147
  return `DELETE FROM ${table} WHERE name = ${this.paramPlaceholder(1)}`;
130
148
  },
149
+ beginStatement(isolationLevel) {
150
+ return isolationLevel ? `BEGIN ISOLATION LEVEL ${isolationLevel}` : 'BEGIN';
151
+ },
152
+ commitStatement() {
153
+ return 'COMMIT';
154
+ },
155
+ rollbackStatement() {
156
+ return 'ROLLBACK';
157
+ },
158
+ savepointStatement(name) {
159
+ return `SAVEPOINT ${name}`;
160
+ },
161
+ releaseSavepointStatement(name) {
162
+ return `RELEASE SAVEPOINT ${name}`;
163
+ },
164
+ rollbackToSavepointStatement(name) {
165
+ return `ROLLBACK TO SAVEPOINT ${name}`;
166
+ },
167
+ buildSetSessionConfig(name, value) {
168
+ // set_config(name, value, is_local=true) — the parameterizable,
169
+ // transaction-local equivalent of `SET LOCAL` (which rejects bind params).
170
+ return {
171
+ sql: `SELECT set_config(${this.paramPlaceholder(1)}, ${this.paramPlaceholder(2)}, true)`,
172
+ params: [name, value],
173
+ };
174
+ },
175
+ async *openStream(connection, sql, params, batchSize) {
176
+ // Cursors require a single connection inside a transaction. Identical SQL
177
+ // sequence to the historical inline implementation: BEGIN → DECLARE … NO
178
+ // SCROLL CURSOR FOR → FETCH n (loop) → CLOSE → COMMIT; ROLLBACK on error.
179
+ const cursorName = `turbine_cursor_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
180
+ const quotedCursor = this.quoteIdentifier(cursorName);
181
+ await connection.query(this.beginStatement());
182
+ try {
183
+ await connection.query(`DECLARE ${quotedCursor} NO SCROLL CURSOR FOR ${sql}`, params);
184
+ while (true) {
185
+ const batch = await connection.query(`FETCH ${batchSize} FROM ${quotedCursor}`);
186
+ if (batch.rows.length === 0)
187
+ break;
188
+ yield batch.rows;
189
+ if (batch.rows.length < batchSize)
190
+ break;
191
+ }
192
+ await connection.query(`CLOSE ${quotedCursor}`);
193
+ await connection.query(this.commitStatement());
194
+ }
195
+ catch (err) {
196
+ try {
197
+ await connection.query(this.rollbackStatement());
198
+ }
199
+ catch {
200
+ // Connection may already be broken — ignore rollback error.
201
+ }
202
+ throw err;
203
+ }
204
+ },
205
+ // Postgres introspection wraps the information_schema / pg_catalog reader in
206
+ // introspect.ts. A dynamic import keeps the static graph acyclic (introspect.ts
207
+ // imports postgresDialect) and calls the raw catalog reader (never the router).
208
+ introspector: {
209
+ async introspect(options) {
210
+ const { introspectPostgresCatalog } = await import('./introspect.js');
211
+ return introspectPostgresCatalog(options);
212
+ },
213
+ },
131
214
  };
132
215
  //# sourceMappingURL=dialect.js.map
package/dist/errors.d.ts CHANGED
@@ -22,6 +22,7 @@ export declare const TurbineErrorCode: {
22
22
  readonly PIPELINE: "TURBINE_E014";
23
23
  readonly OPTIMISTIC_LOCK: "TURBINE_E015";
24
24
  readonly EXCLUSION_VIOLATION: "TURBINE_E016";
25
+ readonly UNSUPPORTED_FEATURE: "TURBINE_E017";
25
26
  };
26
27
  export type TurbineErrorCode = (typeof TurbineErrorCode)[keyof typeof TurbineErrorCode];
27
28
  /** Base error class for all Turbine errors */
@@ -273,6 +274,17 @@ export declare class OptimisticLockError extends TurbineError {
273
274
  expectedVersion: unknown;
274
275
  });
275
276
  }
277
+ /**
278
+ * Thrown when a Postgres-only feature (pgvector distance ops, LISTEN/NOTIFY
279
+ * realtime, RLS session GUCs, advisory-lock migration locking, ...) is invoked
280
+ * on a dialect/engine whose capability flag reports it unsupported. Surfaces a
281
+ * clear `unsupported on <engine>` message instead of generating broken SQL.
282
+ */
283
+ export declare class UnsupportedFeatureError extends TurbineError {
284
+ readonly feature: string;
285
+ readonly dialect: string;
286
+ constructor(feature: string, dialect: string, hint?: string);
287
+ }
276
288
  /**
277
289
  * Translate a pg driver error into a typed Turbine error.
278
290
  * If the error doesn't match a known constraint code, returns it unchanged.
package/dist/errors.js CHANGED
@@ -22,6 +22,7 @@ export const TurbineErrorCode = {
22
22
  PIPELINE: 'TURBINE_E014',
23
23
  OPTIMISTIC_LOCK: 'TURBINE_E015',
24
24
  EXCLUSION_VIOLATION: 'TURBINE_E016',
25
+ UNSUPPORTED_FEATURE: 'TURBINE_E017',
25
26
  };
26
27
  /** Base error class for all Turbine errors */
27
28
  export class TurbineError extends Error {
@@ -435,6 +436,22 @@ export class OptimisticLockError extends TurbineError {
435
436
  this.expectedVersion = opts.expectedVersion;
436
437
  }
437
438
  }
439
+ /**
440
+ * Thrown when a Postgres-only feature (pgvector distance ops, LISTEN/NOTIFY
441
+ * realtime, RLS session GUCs, advisory-lock migration locking, ...) is invoked
442
+ * on a dialect/engine whose capability flag reports it unsupported. Surfaces a
443
+ * clear `unsupported on <engine>` message instead of generating broken SQL.
444
+ */
445
+ export class UnsupportedFeatureError extends TurbineError {
446
+ feature;
447
+ dialect;
448
+ constructor(feature, dialect, hint) {
449
+ super(TurbineErrorCode.UNSUPPORTED_FEATURE, `[turbine] ${feature} is unsupported on "${dialect}".${hint ? ` ${hint}` : ''}`);
450
+ this.name = 'UnsupportedFeatureError';
451
+ this.feature = feature;
452
+ this.dialect = dialect;
453
+ }
454
+ }
438
455
  /**
439
456
  * Parse column names out of a pg `detail` string like:
440
457
  * "Key (email)=(foo@bar) already exists."
package/dist/generate.js CHANGED
@@ -359,6 +359,10 @@ function generateMetadata(schema) {
359
359
  lines.push(' },');
360
360
  lines.push('};');
361
361
  lines.push('');
362
+ // Back-compat lowercase alias. `SCHEMA` is the canonical export, but docs and
363
+ // users frequently import `schema`; emit both so either name resolves.
364
+ lines.push('export const schema = SCHEMA;');
365
+ lines.push('');
362
366
  return lines.join('\n');
363
367
  }
364
368
  // ---------------------------------------------------------------------------
package/dist/index.d.ts CHANGED
@@ -34,16 +34,16 @@
34
34
  */
35
35
  export type { DatabaseAdapter, IntrospectionOverrides } from './adapters/index.js';
36
36
  export { alloydb, cockroachdb, postgresql, timescale, yugabytedb } from './adapters/index.js';
37
- export { type Middleware, type MiddlewareNext, type MiddlewareParams, type PgCompatPool, type PgCompatPoolClient, type PgCompatQueryResult, type RetryOptions, TransactionClient, type TransactionOptions, TurbineClient, type TurbineConfig, withRetry, } from './client.js';
38
- export type { BuiltStatement, BulkInsertStatementInput, ColumnDefinitionInput, ColumnTypeInput, CreateIndexStatementInput, CreateTableStatementInput, Dialect, DialectIntrospector, DialectMigrator, DialectName, InsertStatementInput, IntrospectOptions as DialectIntrospectOptions, UpsertStatementInput, } from './dialect.js';
37
+ export { type Middleware, type MiddlewareNext, type MiddlewareParams, type PgCompatPool, type PgCompatPoolClient, type PgCompatQueryResult, type RetryOptions, TransactionClient, type TransactionOptions, TurbineClient, type TurbineConfig, type TurbineDriver, withRetry, } from './client.js';
38
+ export type { BuiltStatement, BulkInsertStatementInput, ColumnDefinitionInput, ColumnTypeInput, CreateIndexStatementInput, CreateTableStatementInput, Dialect, DialectIntrospector, DialectMigrator, DialectName, InsertStatementInput, IntrospectOptions as DialectIntrospectOptions, ResultStrategy, StreamableConnection, UpsertStatementInput, } from './dialect.js';
39
39
  export { postgresDialect } from './dialect.js';
40
- export { CheckConstraintError, CircularRelationError, ConnectionError, DeadlockError, type ErrorMessageMode, ExclusionConstraintError, ForeignKeyError, getErrorMessageMode, MigrationError, NotFoundError, NotNullViolationError, OptimisticLockError, PipelineError, type PipelineResultSlot, RelationError, SerializationFailureError, setErrorMessageMode, TimeoutError, TurbineError, TurbineErrorCode, UniqueConstraintError, ValidationError, wrapPgError, } from './errors.js';
40
+ export { CheckConstraintError, CircularRelationError, ConnectionError, DeadlockError, type ErrorMessageMode, ExclusionConstraintError, ForeignKeyError, getErrorMessageMode, MigrationError, NotFoundError, NotNullViolationError, OptimisticLockError, PipelineError, type PipelineResultSlot, RelationError, SerializationFailureError, setErrorMessageMode, TimeoutError, TurbineError, TurbineErrorCode, UniqueConstraintError, UnsupportedFeatureError, ValidationError, wrapPgError, } from './errors.js';
41
41
  export { type GenerateOptions, generate } from './generate.js';
42
42
  export { type IntrospectOptions, introspect } from './introspect.js';
43
43
  export { executeNestedCreate, executeNestedUpdate, hasRelationFields, type NestedWriteContext, } from './nested-write.js';
44
44
  export type { ObserveConfig, ObserveHandle } from './observe.js';
45
45
  export { executePipeline, type PipelineOptions, type PipelineResults, pipelineSupported } from './pipeline.js';
46
- export { type AggregateArgs, type AggregateResult, type ArrayFilter, type ConnectOrCreateOp, type CountArgs, type CreateArgs, type CreateManyArgs, type DeferredQuery, type DeleteArgs, type DeleteManyArgs, type FieldResult, type FindManyArgs, type FindManyStreamArgs, type FindUniqueArgs, type GroupByArgs, type HavingClause, type JsonFilter, type MiddlewareFn, type NestedCreateOp, type NestedUpdateOp, type OmitResult, type OrderByClause, type OrderDirection, type QueryEvent, type QueryEventListener, QueryInterface, type QueryResult, type RelationDescriptor, type RelationFilter, type SelectResult, type TextSearchFilter, type TypedWithClause, type UpdateArgs, type UpdateInput, type UpdateManyArgs, type UpdateOperatorInput, type UpsertArgs, type VectorDistanceFilter, type VectorFilter, type VectorMetric, type VectorOrderBy, type VectorOrderByDistance, type WhereClause, type WhereOperator, type WhereValue, type WithClause, type WithOptions, type WithResult, } from './query/index.js';
46
+ export { type AggregateArgs, type AggregateResult, type ArrayFilter, type ConnectOrCreateOp, type CountArgs, type CreateArgs, type CreateDataInput, type CreateManyArgs, type DeferredQuery, type DeleteArgs, type DeleteManyArgs, type FieldResult, type FindManyArgs, type FindManyStreamArgs, type FindUniqueArgs, type GroupByArgs, type HavingClause, type JsonFilter, type MiddlewareFn, type NestedCreateOp, type NestedUpdateOp, type NestedUpdateOpItem, type NestedUpsertOpItem, type OmitResult, type OrderByClause, type OrderDirection, type QueryEvent, type QueryEventListener, QueryInterface, type QueryResult, type RelationDescriptor, type RelationFilter, type SelectResult, type TextSearchFilter, type TypedWithClause, type UpdateArgs, type UpdateDataInput, type UpdateInput, type UpdateManyArgs, type UpdateOperatorInput, type UpsertArgs, type VectorDistanceFilter, type VectorFilter, type VectorMetric, type VectorOrderBy, type VectorOrderByDistance, type WhereClause, type WhereOperator, type WhereValue, type WithClause, type WithOptions, type WithResult, } from './query/index.js';
47
47
  export { type ActiveSubscription, type NotificationHandler, type Subscription, validateChannel } from './realtime.js';
48
48
  export type { ColumnMetadata, IndexMetadata, RelationDef, SchemaMetadata, TableMetadata, } from './schema.js';
49
49
  export { camelToSnake, isDateType, normalizeKeyColumns, pgArrayType, pgTypeToTs, singularize, snakeToCamel, snakeToPascal, } from './schema.js';
package/dist/index.js CHANGED
@@ -37,7 +37,7 @@ export { alloydb, cockroachdb, postgresql, timescale, yugabytedb } from './adapt
37
37
  export { TransactionClient, TurbineClient, withRetry, } from './client.js';
38
38
  export { postgresDialect } from './dialect.js';
39
39
  // Error types
40
- export { CheckConstraintError, CircularRelationError, ConnectionError, DeadlockError, ExclusionConstraintError, ForeignKeyError, getErrorMessageMode, MigrationError, NotFoundError, NotNullViolationError, OptimisticLockError, PipelineError, RelationError, SerializationFailureError, setErrorMessageMode, TimeoutError, TurbineError, TurbineErrorCode, UniqueConstraintError, ValidationError, wrapPgError, } from './errors.js';
40
+ export { CheckConstraintError, CircularRelationError, ConnectionError, DeadlockError, ExclusionConstraintError, ForeignKeyError, getErrorMessageMode, MigrationError, NotFoundError, NotNullViolationError, OptimisticLockError, PipelineError, RelationError, SerializationFailureError, setErrorMessageMode, TimeoutError, TurbineError, TurbineErrorCode, UniqueConstraintError, UnsupportedFeatureError, ValidationError, wrapPgError, } from './errors.js';
41
41
  // Code generation
42
42
  export { generate } from './generate.js';
43
43
  // Introspection
@@ -7,6 +7,7 @@
7
7
  *
8
8
  * This is the foundation of `npx turbine generate`.
9
9
  */
10
+ import { type Dialect } from './dialect.js';
10
11
  import { type SchemaMetadata } from './schema.js';
11
12
  export interface IntrospectOptions {
12
13
  /** Postgres connection string */
@@ -17,6 +18,23 @@ export interface IntrospectOptions {
17
18
  include?: string[];
18
19
  /** Tables to exclude (default: none). Applied after include. */
19
20
  exclude?: string[];
21
+ /**
22
+ * Dialect whose {@link Dialect.introspector} drives the catalog reads.
23
+ * Defaults to {@link postgresDialect}. Engines plug their own introspector
24
+ * here so `introspect()` works across databases.
25
+ */
26
+ dialect?: Dialect;
20
27
  }
28
+ /**
29
+ * Introspect a database into {@link SchemaMetadata}, routing through the active
30
+ * dialect's {@link Dialect.introspector} so each engine can override the catalog
31
+ * SQL. PostgreSQL is driven by {@link introspectPostgresCatalog}.
32
+ */
21
33
  export declare function introspect(options: IntrospectOptions): Promise<SchemaMetadata>;
34
+ /**
35
+ * PostgreSQL catalog introspector: reads information_schema + pg_catalog and
36
+ * produces {@link SchemaMetadata}. This is the implementation wrapped by
37
+ * `postgresDialect.introspector`; call {@link introspect} for dialect routing.
38
+ */
39
+ export declare function introspectPostgresCatalog(options: IntrospectOptions): Promise<SchemaMetadata>;
22
40
  //# sourceMappingURL=introspect.d.ts.map
@@ -94,7 +94,26 @@ const SQL_ENUMS = `
94
94
  // ---------------------------------------------------------------------------
95
95
  // Main introspection function
96
96
  // ---------------------------------------------------------------------------
97
+ /**
98
+ * Introspect a database into {@link SchemaMetadata}, routing through the active
99
+ * dialect's {@link Dialect.introspector} so each engine can override the catalog
100
+ * SQL. PostgreSQL is driven by {@link introspectPostgresCatalog}.
101
+ */
97
102
  export async function introspect(options) {
103
+ const dialect = options.dialect ?? postgresDialect;
104
+ const introspector = dialect.introspector;
105
+ if (introspector) {
106
+ return introspector.introspect(options);
107
+ }
108
+ // Dialects without an introspector fall back to the Postgres catalog reader.
109
+ return introspectPostgresCatalog(options);
110
+ }
111
+ /**
112
+ * PostgreSQL catalog introspector: reads information_schema + pg_catalog and
113
+ * produces {@link SchemaMetadata}. This is the implementation wrapped by
114
+ * `postgresDialect.introspector`; call {@link introspect} for dialect routing.
115
+ */
116
+ export async function introspectPostgresCatalog(options) {
98
117
  const schema = options.schema ?? 'public';
99
118
  const dialect = postgresDialect;
100
119
  const pool = new pg.Pool({