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
@@ -34,7 +34,6 @@ exports.apiBuilder = apiBuilder;
34
34
  exports.apiListSavedQueries = apiListSavedQueries;
35
35
  exports.apiCreateSavedQuery = apiCreateSavedQuery;
36
36
  exports.apiDeleteSavedQuery = apiDeleteSavedQuery;
37
- exports.isReadOnlyStatement = isReadOnlyStatement;
38
37
  const node_child_process_1 = require("node:child_process");
39
38
  const node_crypto_1 = require("node:crypto");
40
39
  const node_fs_1 = require("node:fs");
@@ -238,13 +237,12 @@ function checkRateLimit(limiter, token) {
238
237
  return { allowed: true, resetAt: entry.resetAt };
239
238
  }
240
239
  function constantTimeEqual(a, b) {
241
- if (a.length !== b.length)
242
- return false;
243
- let result = 0;
244
- for (let i = 0; i < a.length; i++) {
245
- result |= a.charCodeAt(i) ^ b.charCodeAt(i);
246
- }
247
- return result === 0;
240
+ // Hash both inputs to fixed-length 32-byte SHA-256 digests before comparing.
241
+ // This makes the comparison constant-length (timingSafeEqual never throws on a
242
+ // length mismatch) and leaks neither length nor content via timing.
243
+ const ah = (0, node_crypto_1.createHash)('sha256').update(a).digest();
244
+ const bh = (0, node_crypto_1.createHash)('sha256').update(b).digest();
245
+ return (0, node_crypto_1.timingSafeEqual)(ah, bh);
248
246
  }
249
247
  /**
250
248
  * Build a helpful "unknown table" error that lists the available tables so the
@@ -550,35 +548,6 @@ function clampInt(value, fallback, min, max) {
550
548
  return fallback;
551
549
  return Math.min(Math.max(n, min), max);
552
550
  }
553
- /**
554
- * Accept only SELECT or WITH (CTE) statements. Reject any statement that
555
- * contains a semicolon followed by non-whitespace (prevents statement
556
- * stacking), and require the first non-comment keyword to be SELECT or WITH.
557
- *
558
- * This is a first-line filter — the transaction's READ ONLY mode is the
559
- * second line of defense. Both must fail before a destructive statement
560
- * could run.
561
- */
562
- function isReadOnlyStatement(sql) {
563
- const stripped = stripSqlComments(sql).trim();
564
- if (!stripped)
565
- return false;
566
- // Disallow statement stacking. A single trailing `;` is fine.
567
- const withoutTrailingSemi = stripped.replace(/;+\s*$/, '');
568
- if (withoutTrailingSemi.includes(';'))
569
- return false;
570
- const firstWord = withoutTrailingSemi.slice(0, 6).toUpperCase();
571
- if (firstWord.startsWith('SELECT'))
572
- return true;
573
- if (firstWord.startsWith('WITH'))
574
- return true;
575
- return false;
576
- }
577
- function stripSqlComments(sql) {
578
- // Strip -- line comments and /* block comments */. Not a full SQL parser,
579
- // but sufficient to catch the common bypass attempts.
580
- return sql.replace(/--[^\n]*/g, '').replace(/\/\*[\s\S]*?\*\//g, '');
581
- }
582
551
  function serializeRow(row) {
583
552
  const out = {};
584
553
  for (const [k, v] of Object.entries(row)) {
@@ -29,6 +29,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
29
29
  exports.TurbineClient = exports.TransactionClient = void 0;
30
30
  exports.withRetry = withRetry;
31
31
  const pg_1 = __importDefault(require("pg"));
32
+ const dialect_js_1 = require("./dialect.js");
32
33
  const errors_js_1 = require("./errors.js");
33
34
  const observe_js_1 = require("./observe.js");
34
35
  const pipeline_js_1 = require("./pipeline.js");
@@ -89,11 +90,14 @@ class TransactionClient {
89
90
  queryOptions;
90
91
  tableCache = new Map();
91
92
  savepointCounter = 0;
93
+ /** Active SQL dialect — owns savepoint keywords and raw-SQL placeholders. */
94
+ dialect;
92
95
  constructor(client, schema, middlewares, queryOptions) {
93
96
  this.client = client;
94
97
  this.schema = schema;
95
98
  this.middlewares = middlewares;
96
99
  this.queryOptions = queryOptions;
100
+ this.dialect = queryOptions?.dialect ?? dialect_js_1.postgresDialect;
97
101
  // Auto-create typed table accessors for all tables in the schema
98
102
  for (const tableName of Object.keys(schema.tables)) {
99
103
  const camelName = tableName.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
@@ -127,14 +131,14 @@ class TransactionClient {
127
131
  */
128
132
  async $transaction(fn) {
129
133
  const savepointName = `sp_${++this.savepointCounter}`;
130
- await this.client.query(`SAVEPOINT ${savepointName}`);
134
+ await this.client.query(this.dialect.savepointStatement(savepointName));
131
135
  try {
132
136
  const result = await fn(this);
133
- await this.client.query(`RELEASE SAVEPOINT ${savepointName}`);
137
+ await this.client.query(this.dialect.releaseSavepointStatement(savepointName));
134
138
  return result;
135
139
  }
136
140
  catch (err) {
137
- await this.client.query(`ROLLBACK TO SAVEPOINT ${savepointName}`);
141
+ await this.client.query(this.dialect.rollbackToSavepointStatement(savepointName));
138
142
  throw err;
139
143
  }
140
144
  }
@@ -146,7 +150,7 @@ class TransactionClient {
146
150
  strings.forEach((str, i) => {
147
151
  sql += str;
148
152
  if (i < values.length) {
149
- sql += `$${i + 1}`;
153
+ sql += this.dialect.paramPlaceholder(i + 1);
150
154
  }
151
155
  });
152
156
  try {
@@ -200,6 +204,8 @@ class TurbineClient {
200
204
  schema;
201
205
  static int8ParserRegistered = false;
202
206
  logging;
207
+ /** Active SQL dialect — owns transaction keywords, set_config, raw-SQL placeholders, capability flags. */
208
+ dialect;
203
209
  tableCache = new Map();
204
210
  middlewares = [];
205
211
  queryListeners = new Set();
@@ -245,6 +251,7 @@ class TurbineClient {
245
251
  TurbineClient.int8ParserRegistered = true;
246
252
  }
247
253
  this.logging = config.logging ?? false;
254
+ this.dialect = config.dialect ?? dialect_js_1.postgresDialect;
248
255
  this.schema = schema;
249
256
  // Respect env var kill switch
250
257
  const envDisablePrepared = typeof process !== 'undefined' && process.env?.TURBINE_DISABLE_PREPARED === '1';
@@ -331,12 +338,14 @@ class TurbineClient {
331
338
  // Middleware — intercept all queries
332
339
  // -------------------------------------------------------------------------
333
340
  /**
334
- * Register a middleware function that runs before/after every query.
341
+ * Register a middleware function that runs around every query.
335
342
  *
336
- * Middleware can inspect and log query parameters, modify results after execution,
337
- * and measure timing. Note: query SQL is generated before middleware runs, so
338
- * modifying params.args in middleware will NOT affect the executed SQL.
339
- * To intercept queries before SQL generation, use the raw() method instead.
343
+ * Middleware can inspect and log query parameters, measure timing, and
344
+ * transform the result returned by `next()`. Note: query SQL is generated
345
+ * BEFORE middleware runs — `params.args` is a read-only snapshot, and
346
+ * mutating it does NOT change the executed SQL. Cross-cutting filters
347
+ * (e.g. soft deletes) belong in the query itself: pass an explicit
348
+ * `where: { deletedAt: null }` or wrap the table accessor in a small helper.
340
349
  *
341
350
  * @example
342
351
  * ```ts
@@ -348,16 +357,13 @@ class TurbineClient {
348
357
  * return result;
349
358
  * });
350
359
  *
351
- * // Soft-delete middleware
360
+ * // Result transformation middleware — redact a field on the way out
352
361
  * db.$use(async (params, next) => {
353
- * if (params.action === 'findMany' || params.action === 'findUnique') {
354
- * params.args.where = { ...params.args.where, deletedAt: null };
355
- * }
356
- * if (params.action === 'delete') {
357
- * params.action = 'update';
358
- * params.args = { where: params.args.where, data: { deletedAt: new Date() } };
362
+ * const result = await next(params);
363
+ * if (params.model === 'users' && Array.isArray(result)) {
364
+ * for (const row of result as { email?: string }[]) row.email = '[redacted]';
359
365
  * }
360
- * return next(params);
366
+ * return result;
361
367
  * });
362
368
  * ```
363
369
  */
@@ -474,7 +480,7 @@ class TurbineClient {
474
480
  strings.forEach((str, i) => {
475
481
  sql += str;
476
482
  if (i < values.length) {
477
- sql += `$${i + 1}`;
483
+ sql += this.dialect.paramPlaceholder(i + 1);
478
484
  }
479
485
  });
480
486
  if (this.logging) {
@@ -519,7 +525,7 @@ class TurbineClient {
519
525
  * ```
520
526
  */
521
527
  sql(strings, ...values) {
522
- const { sql, params } = (0, typed_sql_js_1.buildTypedSql)(strings, values);
528
+ const { sql, params } = (0, typed_sql_js_1.buildTypedSql)(strings, values, this.dialect);
523
529
  return new typed_sql_js_1.TypedSqlQuery(this.pool, sql, params, this.logging);
524
530
  }
525
531
  // -------------------------------------------------------------------------
@@ -539,13 +545,13 @@ class TurbineClient {
539
545
  async transaction(fn) {
540
546
  const client = await this.pool.connect();
541
547
  try {
542
- await client.query('BEGIN');
548
+ await client.query(this.dialect.beginStatement());
543
549
  const result = await fn(client);
544
- await client.query('COMMIT');
550
+ await client.query(this.dialect.commitStatement());
545
551
  return result;
546
552
  }
547
553
  catch (err) {
548
- await client.query('ROLLBACK');
554
+ await client.query(this.dialect.rollbackStatement());
549
555
  throw err;
550
556
  }
551
557
  finally {
@@ -596,14 +602,10 @@ class TurbineClient {
596
602
  };
597
603
  let timedOut = false;
598
604
  try {
599
- // BEGIN with optional isolation level
600
- let beginSQL = 'BEGIN';
601
- if (options?.isolationLevel) {
602
- const level = ISOLATION_LEVELS[options.isolationLevel];
603
- if (level)
604
- beginSQL += ` ISOLATION LEVEL ${level}`;
605
- }
606
- await client.query(beginSQL);
605
+ // BEGIN with optional isolation level — the dialect owns the keyword and
606
+ // BEGIN+isolation composition (Postgres appends ` ISOLATION LEVEL …`).
607
+ const isolationSql = options?.isolationLevel ? ISOLATION_LEVELS[options.isolationLevel] : undefined;
608
+ await client.query(this.dialect.beginStatement(isolationSql));
607
609
  // Apply transaction-local session context (RLS / multi-tenant GUCs).
608
610
  // Order matters: BEGIN -> isolation level (above) -> set_config loop ->
609
611
  // user fn. Any error here propagates to the catch below and rolls back
@@ -611,12 +613,16 @@ class TurbineClient {
611
613
  // is_local=true) — the parameterizable, transaction-scoped equivalent of
612
614
  // SET LOCAL — so both name and value are BOUND params, never interpolated.
613
615
  if (options?.sessionContext) {
616
+ if (!this.dialect.supportsRLS) {
617
+ throw new errors_js_1.UnsupportedFeatureError('sessionContext (RLS session GUCs)', this.dialect.name, 'set_config-based row-level-security context requires PostgreSQL.');
618
+ }
614
619
  for (const [name, value] of Object.entries(options.sessionContext)) {
615
620
  if (!GUC_NAME_REGEX.test(name)) {
616
621
  throw new errors_js_1.ValidationError(`[turbine] Invalid session-context GUC name "${name}" — must match ` +
617
622
  '/^[A-Za-z_][A-Za-z0-9_]*(\\.[A-Za-z_][A-Za-z0-9_]*)?$/ (optionally namespaced, e.g. "app.current_tenant")');
618
623
  }
619
- await client.query('SELECT set_config($1, $2, true)', [name, String(value)]);
624
+ const cfg = this.dialect.buildSetSessionConfig(name, String(value));
625
+ await client.query(cfg.sql, cfg.params);
620
626
  }
621
627
  }
622
628
  // Create the transaction client with typed table accessors
@@ -662,7 +668,7 @@ class TurbineClient {
662
668
  else {
663
669
  result = await fn(tx);
664
670
  }
665
- await client.query('COMMIT');
671
+ await client.query(this.dialect.commitStatement());
666
672
  if (this.logging) {
667
673
  console.log('[turbine] Transaction committed');
668
674
  }
@@ -675,7 +681,7 @@ class TurbineClient {
675
681
  // when its socket was closed).
676
682
  if (!timedOut && !released) {
677
683
  try {
678
- await client.query('ROLLBACK');
684
+ await client.query(this.dialect.rollbackStatement());
679
685
  }
680
686
  catch {
681
687
  // Best-effort rollback — the connection may have died mid-query.
@@ -742,6 +748,9 @@ class TurbineClient {
742
748
  * ```
743
749
  */
744
750
  async $listen(channel, handler) {
751
+ if (!this.dialect.supportsListenNotify) {
752
+ throw new errors_js_1.UnsupportedFeatureError('$listen (LISTEN/NOTIFY realtime)', this.dialect.name, 'Realtime pub/sub requires PostgreSQL.');
753
+ }
745
754
  (0, realtime_js_1.validateChannel)(channel);
746
755
  const quoted = (0, utils_js_1.quoteIdent)(channel);
747
756
  if (this.logging) {
@@ -767,6 +776,9 @@ class TurbineClient {
767
776
  * ```
768
777
  */
769
778
  async $notify(channel, payload) {
779
+ if (!this.dialect.supportsListenNotify) {
780
+ throw new errors_js_1.UnsupportedFeatureError('$notify (LISTEN/NOTIFY realtime)', this.dialect.name, 'Realtime pub/sub requires PostgreSQL.');
781
+ }
770
782
  (0, realtime_js_1.validateChannel)(channel);
771
783
  if (this.logging) {
772
784
  console.log(`[turbine] NOTIFY ${channel}`);
@@ -6,6 +6,39 @@
6
6
  * PostgreSQL-native by default, but query generation now depends on this
7
7
  * contract for the SQL primitives that vary across MySQL and SQLite.
8
8
  */
9
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ var desc = Object.getOwnPropertyDescriptor(m, k);
12
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
13
+ desc = { enumerable: true, get: function() { return m[k]; } };
14
+ }
15
+ Object.defineProperty(o, k2, desc);
16
+ }) : (function(o, m, k, k2) {
17
+ if (k2 === undefined) k2 = k;
18
+ o[k2] = m[k];
19
+ }));
20
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
21
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
22
+ }) : function(o, v) {
23
+ o["default"] = v;
24
+ });
25
+ var __importStar = (this && this.__importStar) || (function () {
26
+ var ownKeys = function(o) {
27
+ ownKeys = Object.getOwnPropertyNames || function (o) {
28
+ var ar = [];
29
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
30
+ return ar;
31
+ };
32
+ return ownKeys(o);
33
+ };
34
+ return function (mod) {
35
+ if (mod && mod.__esModule) return mod;
36
+ var result = {};
37
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
38
+ __setModuleDefault(result, mod);
39
+ return result;
40
+ };
41
+ })();
9
42
  Object.defineProperty(exports, "__esModule", { value: true });
10
43
  exports.postgresDialect = void 0;
11
44
  const errors_js_1 = require("./errors.js");
@@ -13,11 +46,17 @@ const schema_js_1 = require("./schema.js");
13
46
  /** PostgreSQL implementation of the dialect contract. */
14
47
  exports.postgresDialect = {
15
48
  name: 'postgresql',
49
+ resultStrategy: 'returning',
16
50
  supportsReturning: true,
17
51
  supportsILike: true,
18
52
  jsonPathSupport: 'native',
19
53
  emptyJsonArrayLiteral: "'[]'::json",
20
54
  nullJsonLiteral: 'NULL',
55
+ aggSupportsInlineOrderBy: true,
56
+ supportsVector: true,
57
+ supportsListenNotify: true,
58
+ supportsRLS: true,
59
+ supportsAdvisoryLock: true,
21
60
  paramPlaceholder(index) {
22
61
  return `$${index}`;
23
62
  },
@@ -35,6 +74,9 @@ exports.postgresDialect = {
35
74
  const suffix = orderBy ? ` ${orderBy}` : '';
36
75
  return `COALESCE(json_agg(${jsonObjectExpr}${suffix}), ${this.emptyJsonArrayLiteral})`;
37
76
  },
77
+ wrapJsonSubresult(subquery, fallback) {
78
+ return `COALESCE((${subquery}), ${fallback})`;
79
+ },
38
80
  buildReturningClause(selection = '*') {
39
81
  return ` RETURNING ${selection}`;
40
82
  },
@@ -76,6 +118,15 @@ exports.postgresDialect = {
76
118
  typeToTypeScript(dialectType, nullable) {
77
119
  return (0, schema_js_1.pgTypeToTs)(dialectType, nullable);
78
120
  },
121
+ castAggregate(expr, target) {
122
+ return `${expr}::${target}`;
123
+ },
124
+ buildInClause(expr, paramRef, negated) {
125
+ return negated ? `${expr} != ALL(${paramRef})` : `${expr} = ANY(${paramRef})`;
126
+ },
127
+ inClauseParam(values) {
128
+ return values;
129
+ },
79
130
  arrayType(baseType) {
80
131
  return (0, schema_js_1.pgArrayType)(baseType);
81
132
  },
@@ -131,4 +182,69 @@ exports.postgresDialect = {
131
182
  buildMigrationDeleteApplied(table) {
132
183
  return `DELETE FROM ${table} WHERE name = ${this.paramPlaceholder(1)}`;
133
184
  },
185
+ beginStatement(isolationLevel) {
186
+ return isolationLevel ? `BEGIN ISOLATION LEVEL ${isolationLevel}` : 'BEGIN';
187
+ },
188
+ commitStatement() {
189
+ return 'COMMIT';
190
+ },
191
+ rollbackStatement() {
192
+ return 'ROLLBACK';
193
+ },
194
+ savepointStatement(name) {
195
+ return `SAVEPOINT ${name}`;
196
+ },
197
+ releaseSavepointStatement(name) {
198
+ return `RELEASE SAVEPOINT ${name}`;
199
+ },
200
+ rollbackToSavepointStatement(name) {
201
+ return `ROLLBACK TO SAVEPOINT ${name}`;
202
+ },
203
+ buildSetSessionConfig(name, value) {
204
+ // set_config(name, value, is_local=true) — the parameterizable,
205
+ // transaction-local equivalent of `SET LOCAL` (which rejects bind params).
206
+ return {
207
+ sql: `SELECT set_config(${this.paramPlaceholder(1)}, ${this.paramPlaceholder(2)}, true)`,
208
+ params: [name, value],
209
+ };
210
+ },
211
+ async *openStream(connection, sql, params, batchSize) {
212
+ // Cursors require a single connection inside a transaction. Identical SQL
213
+ // sequence to the historical inline implementation: BEGIN → DECLARE … NO
214
+ // SCROLL CURSOR FOR → FETCH n (loop) → CLOSE → COMMIT; ROLLBACK on error.
215
+ const cursorName = `turbine_cursor_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
216
+ const quotedCursor = this.quoteIdentifier(cursorName);
217
+ await connection.query(this.beginStatement());
218
+ try {
219
+ await connection.query(`DECLARE ${quotedCursor} NO SCROLL CURSOR FOR ${sql}`, params);
220
+ while (true) {
221
+ const batch = await connection.query(`FETCH ${batchSize} FROM ${quotedCursor}`);
222
+ if (batch.rows.length === 0)
223
+ break;
224
+ yield batch.rows;
225
+ if (batch.rows.length < batchSize)
226
+ break;
227
+ }
228
+ await connection.query(`CLOSE ${quotedCursor}`);
229
+ await connection.query(this.commitStatement());
230
+ }
231
+ catch (err) {
232
+ try {
233
+ await connection.query(this.rollbackStatement());
234
+ }
235
+ catch {
236
+ // Connection may already be broken — ignore rollback error.
237
+ }
238
+ throw err;
239
+ }
240
+ },
241
+ // Postgres introspection wraps the information_schema / pg_catalog reader in
242
+ // introspect.ts. A dynamic import keeps the static graph acyclic (introspect.ts
243
+ // imports postgresDialect) and calls the raw catalog reader (never the router).
244
+ introspector: {
245
+ async introspect(options) {
246
+ const { introspectPostgresCatalog } = await Promise.resolve().then(() => __importStar(require('./introspect.js')));
247
+ return introspectPostgresCatalog(options);
248
+ },
249
+ },
134
250
  };
@@ -6,7 +6,7 @@
6
6
  * All Turbine errors extend TurbineError which includes a `code` property.
7
7
  */
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
- exports.OptimisticLockError = exports.PipelineError = exports.ExclusionConstraintError = exports.CheckConstraintError = exports.SerializationFailureError = exports.DeadlockError = exports.NotNullViolationError = exports.ForeignKeyError = exports.UniqueConstraintError = exports.CircularRelationError = exports.MigrationError = exports.RelationError = exports.ConnectionError = exports.ValidationError = exports.TimeoutError = exports.NotFoundError = exports.TurbineError = exports.TurbineErrorCode = void 0;
9
+ exports.UnsupportedFeatureError = exports.OptimisticLockError = exports.PipelineError = exports.ExclusionConstraintError = exports.CheckConstraintError = exports.SerializationFailureError = exports.DeadlockError = exports.NotNullViolationError = exports.ForeignKeyError = exports.UniqueConstraintError = exports.CircularRelationError = exports.MigrationError = exports.RelationError = exports.ConnectionError = exports.ValidationError = exports.TimeoutError = exports.NotFoundError = exports.TurbineError = exports.TurbineErrorCode = void 0;
10
10
  exports.setErrorMessageMode = setErrorMessageMode;
11
11
  exports.getErrorMessageMode = getErrorMessageMode;
12
12
  exports.wrapPgError = wrapPgError;
@@ -28,6 +28,7 @@ exports.TurbineErrorCode = {
28
28
  PIPELINE: 'TURBINE_E014',
29
29
  OPTIMISTIC_LOCK: 'TURBINE_E015',
30
30
  EXCLUSION_VIOLATION: 'TURBINE_E016',
31
+ UNSUPPORTED_FEATURE: 'TURBINE_E017',
31
32
  };
32
33
  /** Base error class for all Turbine errors */
33
34
  class TurbineError extends Error {
@@ -458,6 +459,23 @@ class OptimisticLockError extends TurbineError {
458
459
  }
459
460
  }
460
461
  exports.OptimisticLockError = OptimisticLockError;
462
+ /**
463
+ * Thrown when a Postgres-only feature (pgvector distance ops, LISTEN/NOTIFY
464
+ * realtime, RLS session GUCs, advisory-lock migration locking, ...) is invoked
465
+ * on a dialect/engine whose capability flag reports it unsupported. Surfaces a
466
+ * clear `unsupported on <engine>` message instead of generating broken SQL.
467
+ */
468
+ class UnsupportedFeatureError extends TurbineError {
469
+ feature;
470
+ dialect;
471
+ constructor(feature, dialect, hint) {
472
+ super(exports.TurbineErrorCode.UNSUPPORTED_FEATURE, `[turbine] ${feature} is unsupported on "${dialect}".${hint ? ` ${hint}` : ''}`);
473
+ this.name = 'UnsupportedFeatureError';
474
+ this.feature = feature;
475
+ this.dialect = dialect;
476
+ }
477
+ }
478
+ exports.UnsupportedFeatureError = UnsupportedFeatureError;
461
479
  /**
462
480
  * Parse column names out of a pg `detail` string like:
463
481
  * "Key (email)=(foo@bar) already exists."
@@ -363,6 +363,10 @@ function generateMetadata(schema) {
363
363
  lines.push(' },');
364
364
  lines.push('};');
365
365
  lines.push('');
366
+ // Back-compat lowercase alias. `SCHEMA` is the canonical export, but docs and
367
+ // users frequently import `schema`; emit both so either name resolves.
368
+ lines.push('export const schema = SCHEMA;');
369
+ lines.push('');
366
370
  return lines.join('\n');
367
371
  }
368
372
  // ---------------------------------------------------------------------------
package/dist/cjs/index.js CHANGED
@@ -34,8 +34,8 @@
34
34
  * ```
35
35
  */
36
36
  Object.defineProperty(exports, "__esModule", { value: true });
37
- exports.column = exports.ColumnBuilder = exports.applyManyToManyRelations = exports.snakeToPascal = exports.snakeToCamel = exports.singularize = exports.pgTypeToTs = exports.pgArrayType = exports.normalizeKeyColumns = exports.isDateType = exports.camelToSnake = exports.validateChannel = exports.QueryInterface = exports.pipelineSupported = exports.executePipeline = exports.hasRelationFields = exports.executeNestedUpdate = exports.executeNestedCreate = exports.introspect = exports.generate = exports.wrapPgError = exports.ValidationError = exports.UniqueConstraintError = exports.TurbineErrorCode = exports.TurbineError = exports.TimeoutError = exports.setErrorMessageMode = exports.SerializationFailureError = exports.RelationError = exports.PipelineError = exports.OptimisticLockError = exports.NotNullViolationError = exports.NotFoundError = exports.MigrationError = exports.getErrorMessageMode = exports.ForeignKeyError = exports.ExclusionConstraintError = exports.DeadlockError = exports.ConnectionError = exports.CircularRelationError = exports.CheckConstraintError = exports.postgresDialect = exports.withRetry = exports.TurbineClient = exports.TransactionClient = exports.yugabytedb = exports.timescale = exports.postgresql = exports.cockroachdb = exports.alloydb = void 0;
38
- exports.TypedSqlQuery = exports.buildTypedSql = exports.turbineHttp = exports.schemaToSQLString = exports.schemaToSQL = exports.schemaPush = exports.schemaDiff = exports.table = exports.defineSchema = void 0;
37
+ exports.ColumnBuilder = exports.applyManyToManyRelations = exports.snakeToPascal = exports.snakeToCamel = exports.singularize = exports.pgTypeToTs = exports.pgArrayType = exports.normalizeKeyColumns = exports.isDateType = exports.camelToSnake = exports.validateChannel = exports.QueryInterface = exports.pipelineSupported = exports.executePipeline = exports.hasRelationFields = exports.executeNestedUpdate = exports.executeNestedCreate = exports.introspect = exports.generate = exports.wrapPgError = exports.ValidationError = exports.UnsupportedFeatureError = exports.UniqueConstraintError = exports.TurbineErrorCode = exports.TurbineError = exports.TimeoutError = exports.setErrorMessageMode = exports.SerializationFailureError = exports.RelationError = exports.PipelineError = exports.OptimisticLockError = exports.NotNullViolationError = exports.NotFoundError = exports.MigrationError = exports.getErrorMessageMode = exports.ForeignKeyError = exports.ExclusionConstraintError = exports.DeadlockError = exports.ConnectionError = exports.CircularRelationError = exports.CheckConstraintError = exports.postgresDialect = exports.withRetry = exports.TurbineClient = exports.TransactionClient = exports.yugabytedb = exports.timescale = exports.postgresql = exports.cockroachdb = exports.alloydb = void 0;
38
+ exports.TypedSqlQuery = exports.buildTypedSql = exports.turbineHttp = exports.schemaToSQLString = exports.schemaToSQL = exports.schemaPush = exports.schemaDiff = exports.table = exports.defineSchema = exports.column = void 0;
39
39
  var index_js_1 = require("./adapters/index.js");
40
40
  Object.defineProperty(exports, "alloydb", { enumerable: true, get: function () { return index_js_1.alloydb; } });
41
41
  Object.defineProperty(exports, "cockroachdb", { enumerable: true, get: function () { return index_js_1.cockroachdb; } });
@@ -70,6 +70,7 @@ Object.defineProperty(exports, "TimeoutError", { enumerable: true, get: function
70
70
  Object.defineProperty(exports, "TurbineError", { enumerable: true, get: function () { return errors_js_1.TurbineError; } });
71
71
  Object.defineProperty(exports, "TurbineErrorCode", { enumerable: true, get: function () { return errors_js_1.TurbineErrorCode; } });
72
72
  Object.defineProperty(exports, "UniqueConstraintError", { enumerable: true, get: function () { return errors_js_1.UniqueConstraintError; } });
73
+ Object.defineProperty(exports, "UnsupportedFeatureError", { enumerable: true, get: function () { return errors_js_1.UnsupportedFeatureError; } });
73
74
  Object.defineProperty(exports, "ValidationError", { enumerable: true, get: function () { return errors_js_1.ValidationError; } });
74
75
  Object.defineProperty(exports, "wrapPgError", { enumerable: true, get: function () { return errors_js_1.wrapPgError; } });
75
76
  // Code generation
@@ -13,6 +13,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
13
13
  };
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.introspect = introspect;
16
+ exports.introspectPostgresCatalog = introspectPostgresCatalog;
16
17
  const pg_1 = __importDefault(require("pg"));
17
18
  const dialect_js_1 = require("./dialect.js");
18
19
  const schema_js_1 = require("./schema.js");
@@ -100,7 +101,26 @@ const SQL_ENUMS = `
100
101
  // ---------------------------------------------------------------------------
101
102
  // Main introspection function
102
103
  // ---------------------------------------------------------------------------
104
+ /**
105
+ * Introspect a database into {@link SchemaMetadata}, routing through the active
106
+ * dialect's {@link Dialect.introspector} so each engine can override the catalog
107
+ * SQL. PostgreSQL is driven by {@link introspectPostgresCatalog}.
108
+ */
103
109
  async function introspect(options) {
110
+ const dialect = options.dialect ?? dialect_js_1.postgresDialect;
111
+ const introspector = dialect.introspector;
112
+ if (introspector) {
113
+ return introspector.introspect(options);
114
+ }
115
+ // Dialects without an introspector fall back to the Postgres catalog reader.
116
+ return introspectPostgresCatalog(options);
117
+ }
118
+ /**
119
+ * PostgreSQL catalog introspector: reads information_schema + pg_catalog and
120
+ * produces {@link SchemaMetadata}. This is the implementation wrapped by
121
+ * `postgresDialect.introspector`; call {@link introspect} for dialect routing.
122
+ */
123
+ async function introspectPostgresCatalog(options) {
104
124
  const schema = options.schema ?? 'public';
105
125
  const dialect = dialect_js_1.postgresDialect;
106
126
  const pool = new pg_1.default.Pool({