turbine-orm 0.33.0 → 0.35.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 (47) hide show
  1. package/README.md +2 -2
  2. package/dist/cjs/client.js +26 -4
  3. package/dist/cjs/dialect.js +1 -0
  4. package/dist/cjs/errors.js +41 -1
  5. package/dist/cjs/index-advisor.js +0 -0
  6. package/dist/cjs/index.js +4 -2
  7. package/dist/cjs/mssql.js +5 -0
  8. package/dist/cjs/mysql.js +4 -0
  9. package/dist/cjs/optional-peer-import.cjs +28 -0
  10. package/dist/cjs/powdb-introspect.js +222 -0
  11. package/dist/cjs/powdb.js +592 -72
  12. package/dist/cjs/powql.js +998 -134
  13. package/dist/cjs/query/builder.js +72 -1
  14. package/dist/cjs/schema-builder.js +16 -0
  15. package/dist/cjs/schema-metadata.js +81 -10
  16. package/dist/cjs/sqlite.js +3 -0
  17. package/dist/client.d.ts +32 -5
  18. package/dist/client.js +26 -4
  19. package/dist/dialect.d.ts +13 -0
  20. package/dist/dialect.js +1 -0
  21. package/dist/errors.d.ts +36 -0
  22. package/dist/errors.js +39 -0
  23. package/dist/index-advisor.d.ts +15 -1
  24. package/dist/index-advisor.js +0 -0
  25. package/dist/index.d.ts +3 -3
  26. package/dist/index.js +2 -2
  27. package/dist/mssql.js +5 -0
  28. package/dist/mysql.js +4 -0
  29. package/dist/optional-peer-import.cjs +28 -0
  30. package/dist/optional-peer-import.d.cts +19 -0
  31. package/dist/powdb-introspect.d.ts +84 -0
  32. package/dist/powdb-introspect.js +219 -0
  33. package/dist/powdb.d.ts +361 -19
  34. package/dist/powdb.js +585 -72
  35. package/dist/powql.d.ts +245 -8
  36. package/dist/powql.js +1001 -137
  37. package/dist/query/builder.d.ts +36 -1
  38. package/dist/query/builder.js +72 -1
  39. package/dist/query/deferred.d.ts +6 -2
  40. package/dist/query/types.d.ts +49 -12
  41. package/dist/schema-builder.d.ts +46 -1
  42. package/dist/schema-builder.js +15 -0
  43. package/dist/schema-metadata.d.ts +13 -7
  44. package/dist/schema-metadata.js +82 -11
  45. package/dist/schema.d.ts +25 -0
  46. package/dist/sqlite.js +3 -0
  47. package/package.json +3 -3
package/README.md CHANGED
@@ -863,7 +863,7 @@ const db = await turbineMssql('mssql://sa:Passw0rd!@localhost:1433/app', SCHEMA)
863
863
  ```
864
864
 
865
865
  ```ts
866
- // PowDB — async; embedded (in-process) or networked. Schema is code-defined (no introspection).
866
+ // PowDB — async; embedded (in-process) or networked. Schema is code-defined.
867
867
  import { turbinePowDB } from 'turbine-orm/powdb';
868
868
  import { schema } from './schema.js'; // defineSchema({...})
869
869
 
@@ -891,7 +891,7 @@ Everything is honest about what ports and what doesn't. Features marked **PG-onl
891
891
 
892
892
  **Engine notes:** SQLite uses `RETURNING` (≥ 3.35) just like Postgres. MySQL has no `RETURNING`, so writes re-`SELECT` the affected row and **`createMany` returns `[]`** (the rows ARE inserted — re-query if you need them). SQL Server returns rows via `OUTPUT`/`MERGE`; `DISTINCT ON` is Postgres-only. Only Postgres streams via a true cursor (constant memory); the other engines' `findManyStream` materializes the result then yields it in batches. Optimistic locking throws `OptimisticLockError` on all engines (on MySQL the conflict is detected from the version-checked UPDATE's affected-row count). The `turbine` CLI (`generate`, `migrate`) is currently PostgreSQL-only — point the engine factories at a hand-written or programmatically introspected `SCHEMA`.
893
893
 
894
- **PowDB** speaks its own non-SQL query language (PowQL), so it sits outside the SQL matrix above. Writes use a trailing **`returning`** keyword (upsert reselects by PK). PKs are server-assigned `auto` ints **or** client UUIDs. Nested relations load client-side (N+1, including many-to-many via the junction — no `json_agg`). Nested writes cover hasMany/hasOne/belongsTo; many-to-many nested writes are not supported. Transactions are single-writer: concurrent `$transaction` calls queue FIFO (bounded by `transactionQueueTimeoutMs`); nested/re-entrant transactions throw typed errors (no savepoints). Schema is code-first via `defineSchema` (no wire introspection) — `schemaDefToMetadata()` bridges it to any engine that needs runtime metadata. Embedded `syncMode: 'normal'` moves fsync off the commit path; the networked transport runs the same data over a socket. Cursor streaming and the Postgres-only trio (pgvector / LISTEN/NOTIFY / RLS session GUCs) throw `UnsupportedFeatureError`. Full details: **[turbineorm.dev/engines#powdb](https://turbineorm.dev/engines#powdb)**.
894
+ **PowDB** speaks its own non-SQL query language (PowQL), so it sits outside the SQL matrix above. Writes use a trailing **`returning`** keyword (upsert reselects by PK). PKs are server-assigned `auto` ints **or** client UUIDs. Nested relations load client-side (N+1, including many-to-many via the junction — no `json_agg`). Nested writes cover hasMany/hasOne/belongsTo; many-to-many nested writes are not supported. Transactions are single-writer: concurrent `$transaction` calls queue FIFO (bounded by `transactionQueueTimeoutMs`); nested/re-entrant transactions throw typed errors (no savepoints). Schema is code-first via `defineSchema` — `schemaDefToMetadata()` bridges it to any engine that needs runtime metadata, and a programmatic `describe`-based introspector exists since 0.34 (relations excluded). JSON documents are first-class on engine 0.12+: `JsonFilter` where-filters, JSON-path `orderBy`/`groupBy`, doc-field expression indexes, and a lossless native wire (0.13+) that keeps JSON `null`, missing fields, and the string `"null"` distinct. Embedded `syncMode: 'normal'` moves fsync off the commit path; the networked transport runs the same data over a socket. Cursor streaming and the Postgres-only trio (pgvector / LISTEN/NOTIFY / RLS session GUCs) throw `UnsupportedFeatureError`. Full details: **[turbineorm.dev/engines#powdb](https://turbineorm.dev/engines#powdb)**.
895
895
 
896
896
  Full setup, signatures, and the complete support matrix: **[turbineorm.dev/engines](https://turbineorm.dev/engines)**.
897
897
 
@@ -111,15 +111,27 @@ class TransactionClient {
111
111
  schema;
112
112
  middlewares;
113
113
  queryOptions;
114
+ sourcePool;
114
115
  tableCache = new Map();
115
116
  savepointCounter = 0;
116
117
  /** Active SQL dialect — owns savepoint keywords and raw-SQL placeholders. */
117
118
  dialect;
118
- constructor(client, schema, middlewares, queryOptions) {
119
+ constructor(client, schema, middlewares, queryOptions,
120
+ /**
121
+ * The parent pool this transaction runs on. Only its `readonly` and
122
+ * `capabilities` are read (both PowDB-only flags), so the transaction-scoped
123
+ * proxy pool built by {@link createTxPool} carries them through: without this
124
+ * a read-only client's `$transaction` writes bypass the E018 guard, and an
125
+ * older-engine client falls back to ALL_POWDB_CAPABILITIES inside the tx
126
+ * (emitting join PowQL a pre-0.13 engine rejects). Undefined / absent flags
127
+ * for a plain pg pool leave the proxy unchanged.
128
+ */
129
+ sourcePool) {
119
130
  this.client = client;
120
131
  this.schema = schema;
121
132
  this.middlewares = middlewares;
122
133
  this.queryOptions = queryOptions;
134
+ this.sourcePool = sourcePool;
123
135
  this.dialect = queryOptions?.dialect ?? dialect_js_1.postgresDialect;
124
136
  // Auto-create typed table accessors for all tables in the schema
125
137
  for (const tableName of Object.keys(schema.tables)) {
@@ -199,7 +211,7 @@ class TransactionClient {
199
211
  const client = this.client;
200
212
  // Return a minimal pool-compatible object that routes queries
201
213
  // through the transaction client
202
- return {
214
+ const txPool = {
203
215
  query: async (textOrConfig, values) => {
204
216
  try {
205
217
  if (typeof textOrConfig === 'string') {
@@ -216,6 +228,14 @@ class TransactionClient {
216
228
  },
217
229
  connect: () => Promise.resolve(client),
218
230
  };
231
+ // Carry the parent pool's PowDB-only flags through so a transaction-scoped
232
+ // PowqlInterface reads the same read-only guard and capabilities it would
233
+ // outside the transaction (a plain pg pool has neither, so nothing changes).
234
+ if (this.sourcePool?.readonly !== undefined)
235
+ txPool.readonly = this.sourcePool.readonly;
236
+ if (this.sourcePool?.capabilities !== undefined)
237
+ txPool.capabilities = this.sourcePool.capabilities;
238
+ return txPool;
219
239
  }
220
240
  }
221
241
  exports.TransactionClient = TransactionClient;
@@ -884,8 +904,10 @@ class TurbineClient {
884
904
  await client.query(cfg.sql, cfg.params);
885
905
  }
886
906
  }
887
- // Create the transaction client with typed table accessors
888
- const tx = new TransactionClient(client, this.schema, this.middlewares, this.queryOptions);
907
+ // Create the transaction client with typed table accessors. Pass the
908
+ // parent pool so its read-only guard + PowDB capabilities flow into the
909
+ // transaction-scoped proxy pool (see TransactionClient.createTxPool).
910
+ const tx = new TransactionClient(client, this.schema, this.middlewares, this.queryOptions, this.pool);
889
911
  // Dynamically attach table accessors to tx
890
912
  for (const tableName of Object.keys(this.schema.tables)) {
891
913
  const camelName = tableName.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
@@ -59,6 +59,7 @@ exports.postgresDialect = {
59
59
  supportsRLS: true,
60
60
  supportsAdvisoryLock: true,
61
61
  supportsLateralJoin: true,
62
+ explainQuery: { prefix: 'EXPLAIN' },
62
63
  paramPlaceholder(index) {
63
64
  return `$${index}`;
64
65
  },
@@ -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.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;
9
+ exports.ReadOnlyError = 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;
@@ -29,6 +29,7 @@ exports.TurbineErrorCode = {
29
29
  OPTIMISTIC_LOCK: 'TURBINE_E015',
30
30
  EXCLUSION_VIOLATION: 'TURBINE_E016',
31
31
  UNSUPPORTED_FEATURE: 'TURBINE_E017',
32
+ READ_ONLY: 'TURBINE_E018',
32
33
  };
33
34
  /**
34
35
  * Prefix a human message with its stable error code so logs are greppable
@@ -503,6 +504,45 @@ class UnsupportedFeatureError extends TurbineError {
503
504
  }
504
505
  }
505
506
  exports.UnsupportedFeatureError = UnsupportedFeatureError;
507
+ /**
508
+ * Thrown when a write or DDL statement is refused because the target is
509
+ * read-only. Two shapes reach here, both on PowDB:
510
+ * - an embedded database opened read-only for snapshot serving refuses a write
511
+ * with `readonly mode: statement requires a writer …`;
512
+ * - a networked read-only role refuses a write with `permission denied: role
513
+ * '<role>' cannot execute write statements` (translated by `wrapPowdbError`).
514
+ * It is also raised locally, before the wire, when a write is issued on a pool
515
+ * the caller marked read-only (fail-fast). The message carries the engine text
516
+ * plus a hint to route writes to a writable primary.
517
+ *
518
+ * NOT retryable: the same write against the same read-only target fails
519
+ * identically; route it to a writable primary instead.
520
+ */
521
+ class ReadOnlyError extends TurbineError {
522
+ /**
523
+ * Why the write was refused. `'snapshot'`: the database itself is read-only
524
+ * (snapshot serving, an embedded `readonly: true` open, or the client-level
525
+ * fail-fast flag), so NOTHING can write here and writes must route to the
526
+ * primary. `'rbac'`: the database is writable but THIS connection's role may
527
+ * not write (per-connection permission), so re-authenticating may suffice.
528
+ */
529
+ reason;
530
+ /**
531
+ * @param detail human-readable description of the refused write (the engine
532
+ * message, or a local fail-fast description). A "route writes to a writable
533
+ * primary" hint is always appended.
534
+ * @param options optional driver `cause` to preserve when wrapping a refusal,
535
+ * and the refusal `reason` (default `'snapshot'`).
536
+ */
537
+ constructor(detail, options) {
538
+ super(exports.TurbineErrorCode.READ_ONLY, `[turbine] ${detail} Route writes to a writable primary.`, {
539
+ cause: options?.cause,
540
+ });
541
+ this.name = 'ReadOnlyError';
542
+ this.reason = options?.reason ?? 'snapshot';
543
+ }
544
+ }
545
+ exports.ReadOnlyError = ReadOnlyError;
506
546
  /**
507
547
  * Parse column names out of a pg `detail` string like:
508
548
  * "Key (email)=(foo@bar) already exists."
Binary file
package/dist/cjs/index.js CHANGED
@@ -34,8 +34,8 @@
34
34
  * ```
35
35
  */
36
36
  Object.defineProperty(exports, "__esModule", { value: true });
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.defineSeed = exports.schemaToSQLString = exports.schemaToSQL = exports.schemaPush = exports.schemaDiff = exports.schemaDefToMetadata = exports.table = exports.defineSchema = exports.column = void 0;
37
+ 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.ReadOnlyError = 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.defineSeed = exports.schemaToSQLString = exports.schemaToSQL = exports.schemaPush = exports.schemaDiff = exports.schemaDefToMetadata = exports.table = exports.isDocFieldIndexDef = exports.defineSchema = exports.column = exports.ColumnBuilder = 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; } });
@@ -63,6 +63,7 @@ Object.defineProperty(exports, "NotFoundError", { enumerable: true, get: functio
63
63
  Object.defineProperty(exports, "NotNullViolationError", { enumerable: true, get: function () { return errors_js_1.NotNullViolationError; } });
64
64
  Object.defineProperty(exports, "OptimisticLockError", { enumerable: true, get: function () { return errors_js_1.OptimisticLockError; } });
65
65
  Object.defineProperty(exports, "PipelineError", { enumerable: true, get: function () { return errors_js_1.PipelineError; } });
66
+ Object.defineProperty(exports, "ReadOnlyError", { enumerable: true, get: function () { return errors_js_1.ReadOnlyError; } });
66
67
  Object.defineProperty(exports, "RelationError", { enumerable: true, get: function () { return errors_js_1.RelationError; } });
67
68
  Object.defineProperty(exports, "SerializationFailureError", { enumerable: true, get: function () { return errors_js_1.SerializationFailureError; } });
68
69
  Object.defineProperty(exports, "setErrorMessageMode", { enumerable: true, get: function () { return errors_js_1.setErrorMessageMode; } });
@@ -110,6 +111,7 @@ Object.defineProperty(exports, "applyManyToManyRelations", { enumerable: true, g
110
111
  Object.defineProperty(exports, "ColumnBuilder", { enumerable: true, get: function () { return schema_builder_js_1.ColumnBuilder; } });
111
112
  Object.defineProperty(exports, "column", { enumerable: true, get: function () { return schema_builder_js_1.column; } });
112
113
  Object.defineProperty(exports, "defineSchema", { enumerable: true, get: function () { return schema_builder_js_1.defineSchema; } });
114
+ Object.defineProperty(exports, "isDocFieldIndexDef", { enumerable: true, get: function () { return schema_builder_js_1.isDocFieldIndexDef; } });
113
115
  // Legacy compat (deprecated — use object format with defineSchema)
114
116
  Object.defineProperty(exports, "table", { enumerable: true, get: function () { return schema_builder_js_1.table; } });
115
117
  // Schema metadata bridge — defineSchema() → SchemaMetadata without a live DB
package/dist/cjs/mssql.js CHANGED
@@ -483,6 +483,11 @@ exports.mssqlDialect = {
483
483
  supportsLateralJoin: false,
484
484
  // sp_getapplock / sp_releaseapplock exist (used by a future migrate adapter).
485
485
  supportsAdvisoryLock: true,
486
+ // No in-band EXPLAIN: SQL Server's SHOWPLAN is a session toggle
487
+ // (SET SHOWPLAN_ALL ON), not a statement prefix, so a compiled query cannot
488
+ // be explained in one round-trip. Override the inherited Postgres `EXPLAIN`
489
+ // to absent → QueryInterface.explain() throws E017.
490
+ explainQuery: undefined,
486
491
  // FOR JSON over zero rows is NULL → coalesced in the relation override.
487
492
  aggSupportsInlineOrderBy: false,
488
493
  jsonPathSupport: 'limited',
package/dist/cjs/mysql.js CHANGED
@@ -384,6 +384,10 @@ exports.mysqlDialect = {
384
384
  supportsLateralJoin: false,
385
385
  // GET_LOCK / RELEASE_LOCK exist (used by a future migrate adapter).
386
386
  supportsAdvisoryLock: true,
387
+ // Plain `EXPLAIN` (one row of tabular plan columns) works on every supported
388
+ // MySQL 8.0.x; the readable `FORMAT=TREE` variant only exists from 8.0.16 and
389
+ // the engine floor here is 8.0.0. Plan text is a diagnostic, not a contract.
390
+ explainQuery: { prefix: 'EXPLAIN' },
387
391
  // JSON_ARRAYAGG has no inline ORDER BY argument → force the inner-subquery
388
392
  // rewrite for every ordered to-many relation.
389
393
  aggSupportsInlineOrderBy: false,
@@ -119,4 +119,32 @@ async function importOptionalPeer(specifier, allowEsmFallback = true) {
119
119
  return esmCapableCopy(specifier, false);
120
120
  }
121
121
  }
122
+ /**
123
+ * Merged namespace so callers can reach {@link peerPackageVersion} off the same
124
+ * default import (`importOptionalPeer.peerPackageVersion(...)`). Lives in this
125
+ * `.cts` file for the same reason the dynamic import does: a `.cts` compiles to
126
+ * CommonJS in BOTH build passes, so `require` / `require.resolve` are natively
127
+ * available and `import.meta` is never emitted (which would break the CJS build
128
+ * and crash CJS consumers, see `resolveEmbeddedVersion` in powdb.ts).
129
+ */
130
+ (function (importOptionalPeer) {
131
+ /**
132
+ * Resolve an optional peer's declared `package.json` version WITHOUT loading
133
+ * the package itself (so an ESM-only peer never trips `require`). `require` is
134
+ * anchored on THIS module's location (inside the published `dist/`), so bare
135
+ * resolution walks up `node_modules` and finds the peer exactly where
136
+ * `import.meta.url` used to point, but it compiles under `module: CommonJS`
137
+ * too. Returns `null` when the peer / its package.json cannot be resolved.
138
+ */
139
+ function peerPackageVersion(specifier) {
140
+ try {
141
+ const pkg = require(`${specifier}/package.json`);
142
+ return typeof pkg.version === 'string' ? pkg.version : null;
143
+ }
144
+ catch {
145
+ return null;
146
+ }
147
+ }
148
+ importOptionalPeer.peerPackageVersion = peerPackageVersion;
149
+ })(importOptionalPeer || (importOptionalPeer = {}));
122
150
  module.exports = importOptionalPeer;
@@ -0,0 +1,222 @@
1
+ "use strict";
2
+ /**
3
+ * turbine-orm/powdb — `describe`-based introspection.
4
+ *
5
+ * PowDB exposes its catalog through two ordinary rows-returning statements
6
+ * (keywords since engine 0.10):
7
+ * - `schema` → one row per type: `{ name, columns }` (columns = a count).
8
+ * - `describe <T>` / `schema <T>` → one row per column:
9
+ * `{ column, type, nullable, index }` where `type` is a PowQL type name
10
+ * (`str`/`int`/`float`/`bool`/`json`/`datetime`/`uuid`/`bytes`), `nullable`
11
+ * is `"true"`/`"false"`, and `index` is `"unique"` / `"index"` / `""`.
12
+ *
13
+ * {@link introspectPowdbDatabase} turns those into the same {@link SchemaMetadata}
14
+ * shape the SQL introspectors produce, so a code-first PowDB database can be
15
+ * introspected for bootstrap/verification. It is transport-agnostic: the caller
16
+ * supplies an `exec(powql)` that returns row objects **keyed by column name**.
17
+ * - Embedded / owned pool: `exec = async (q) => ({ rows: await db.raw([q]) })`
18
+ * using a live `turbinePowDB` client's `raw` tagged template.
19
+ * - Networked: the raw `@zvndev/powdb-client` returns POSITIONAL rows
20
+ * (`{ columns: string[], rows: string[][] }`), so zip them into records.
21
+ * A bare `(await client.query(q)).rows` would hand this function `string[][]`
22
+ * whose `.name` cell is `undefined` and every table would silently drop out:
23
+ * ```ts
24
+ * const exec = async (q) => {
25
+ * const r = await client.query(q);
26
+ * return { rows: r.rows.map((row) => Object.fromEntries(r.columns.map((c, i) => [c, row[i]]))) };
27
+ * };
28
+ * ```
29
+ * (A mis-shaped exec is now caught: if `schema` returns rows but none carry
30
+ * a `name`, {@link introspectPowdbDatabase} throws instead of returning an
31
+ * empty schema.)
32
+ *
33
+ * IMPORTANT LIMITATIONS (all documented, none silent):
34
+ * - Relations are ALWAYS `{}`: PowDB has no declared foreign keys, so
35
+ * `describe` cannot report them. The recommended flow for relation-aware
36
+ * metadata is code-first `defineSchema` + `schemaDefToMetadata`; use
37
+ * introspection to bootstrap or verify column shape.
38
+ * - Primary key is a HEURISTIC (`describe` has no PK concept): PowDB marks a
39
+ * PK column as `required unique`, so the first non-nullable `unique` column
40
+ * is chosen (a column named `id` wins ties). A table with no such column
41
+ * yields `primaryKey: []` and a warning; single-row ops on it fail loudly.
42
+ * - `isGenerated` is always `false`: `describe` does not expose PowDB's `auto`
43
+ * modifier, so an introspected int PK is treated as client-supplied unless
44
+ * the caller hand-edits the metadata.
45
+ * - Doc-field expression indexes are INVISIBLE to `describe`, so they never
46
+ * round-trip; only plain `unique`/`index` columns appear in `indexes`.
47
+ * - `datetime` / `uuid` / `bytes` columns map to read-oriented TS types
48
+ * (`Date` / `string` / `Uint8Array`). Turbine never emits those PowQL types
49
+ * on write, so writing to such a column may not round-trip.
50
+ *
51
+ * v1 is a PROGRAMMATIC API (exported from `turbine-orm/powdb`); the CLI's
52
+ * `turbine generate` still defaults to Postgres. Routing a `powdb://` URL
53
+ * through the CLI would additionally need: a `powdbDialect.introspector`
54
+ * wired to a networked `exec`, and `cli/config.ts` teaching the generate
55
+ * funnel to construct a PowDB client instead of a `pg` client for `powdb://`.
56
+ */
57
+ Object.defineProperty(exports, "__esModule", { value: true });
58
+ exports.introspectPowdbDatabase = introspectPowdbDatabase;
59
+ const errors_js_1 = require("./errors.js");
60
+ const powdb_js_1 = require("./powdb.js");
61
+ const schema_js_1 = require("./schema.js");
62
+ /** Coerce a wire cell to string (legacy wire cells are strings; native cells may be typed). */
63
+ function asString(v) {
64
+ return v === null || v === undefined ? '' : String(v);
65
+ }
66
+ /** Coerce a `describe` `nullable` cell (`"true"`/`"false"` or a native boolean) to a JS boolean. */
67
+ function asBool(v) {
68
+ return v === true || asString(v).toLowerCase() === 'true';
69
+ }
70
+ /**
71
+ * Map a PowQL type name to the {@link ColumnMetadata} TS/dialect types. The
72
+ * `tsType` drives read coercion (`coerceValue`) and write typing
73
+ * (`powqlColumnType`); `dialectType`/`pgType` carry the PowQL type name so
74
+ * `isFloatColumn` / `isJsonColumn` classify correctly.
75
+ */
76
+ function mapPowqlType(powqlType) {
77
+ switch (powqlType) {
78
+ case 'int':
79
+ return { tsType: 'number', dialectType: 'int' };
80
+ case 'float':
81
+ return { tsType: 'number', dialectType: 'float' };
82
+ case 'bool':
83
+ return { tsType: 'boolean', dialectType: 'bool' };
84
+ case 'json':
85
+ return { tsType: 'unknown', dialectType: 'json' };
86
+ case 'datetime':
87
+ return { tsType: 'Date', dialectType: 'datetime' };
88
+ case 'uuid':
89
+ return { tsType: 'string', dialectType: 'uuid' };
90
+ case 'bytes':
91
+ return { tsType: 'Uint8Array', dialectType: 'bytes' };
92
+ default:
93
+ // `str` and any unknown future scalar fall back to string.
94
+ return { tsType: 'string', dialectType: 'str' };
95
+ }
96
+ }
97
+ /**
98
+ * Read a live PowDB database into {@link SchemaMetadata} via `schema` +
99
+ * `describe <T>` statements run through the supplied {@link PowdbExec}.
100
+ *
101
+ * @param exec Rows-returning executor (embedded `db.raw` or networked `client.query`).
102
+ * @param options `include`/`exclude` table filters.
103
+ */
104
+ async function introspectPowdbDatabase(exec, options = {}) {
105
+ // Gate on the engine's introspection capability (>= 0.10) when the caller
106
+ // knows the version, so a pre-0.10 engine gets a typed E017 hint instead of
107
+ // an opaque `unexpected token schema` parse error.
108
+ if (options.capabilities) {
109
+ (0, powdb_js_1.requireCapability)(options.capabilities, 'introspection', 'PowDB `describe` introspection');
110
+ }
111
+ // ----- Types (one row per table, columns `name`, `columns`) -----
112
+ const schemaRows = (await exec('schema')).rows;
113
+ let tableNames = schemaRows.map((r) => asString(r.name)).filter((n) => n.length > 0);
114
+ // A mis-shaped `exec` (e.g. the raw client's positional `string[][]` rows
115
+ // passed straight through) yields rows whose `name` cell is `undefined`, so
116
+ // every table filters out and the schema comes back silently empty. Refuse
117
+ // that instead of losing data: real rows must carry a `name`.
118
+ if (schemaRows.length > 0 && tableNames.length === 0) {
119
+ throw new errors_js_1.ValidationError(`[turbine] PowDB introspection: the \`schema\` statement returned ${schemaRows.length} row(s) but none carried a ` +
120
+ '`name` cell. The `exec` you supplied likely returns POSITIONAL rows (string[][]) rather than records keyed by ' +
121
+ 'column name; zip `columns` with each row (see introspectPowdbDatabase docs).');
122
+ }
123
+ if (options.include?.length) {
124
+ const inc = new Set(options.include);
125
+ tableNames = tableNames.filter((t) => inc.has(t));
126
+ }
127
+ if (options.exclude?.length) {
128
+ const exc = new Set(options.exclude);
129
+ tableNames = tableNames.filter((t) => !exc.has(t));
130
+ }
131
+ const tables = {};
132
+ for (const tableName of tableNames) {
133
+ // `describe` needs the table name in bare-identifier position → quote it so
134
+ // a reserved-word / non-bare table name (`order`) does not become a parse
135
+ // error.
136
+ const describeRows = (await exec(`describe ${(0, powdb_js_1.quotePowqlIdent)(tableName)}`)).rows.map((r) => ({
137
+ column: asString(r.column),
138
+ type: asString(r.type),
139
+ nullable: asBool(r.nullable),
140
+ index: asString(r.index),
141
+ }));
142
+ const columns = [];
143
+ const columnMap = {};
144
+ const reverseColumnMap = {};
145
+ const dateColumns = new Set();
146
+ const dialectTypes = {};
147
+ const pgTypes = {};
148
+ const allColumns = [];
149
+ const uniqueColumns = [];
150
+ const indexes = [];
151
+ // PK heuristic candidates: non-nullable `unique` columns.
152
+ const pkCandidates = [];
153
+ for (const row of describeRows) {
154
+ const name = row.column;
155
+ const field = (0, schema_js_1.snakeToCamel)(name);
156
+ const { tsType, dialectType } = mapPowqlType(row.type);
157
+ const nullable = row.nullable;
158
+ const finalTs = nullable ? `${tsType} | null` : tsType;
159
+ const col = {
160
+ name,
161
+ field,
162
+ dialectType,
163
+ pgType: dialectType,
164
+ tsType: finalTs,
165
+ nullable,
166
+ // `describe` reports neither defaults nor the `auto` modifier.
167
+ hasDefault: false,
168
+ isGenerated: false,
169
+ isArray: false,
170
+ arrayType: undefined,
171
+ pgArrayType: 'text[]',
172
+ };
173
+ columns.push(col);
174
+ columnMap[field] = name;
175
+ reverseColumnMap[name] = field;
176
+ allColumns.push(name);
177
+ dialectTypes[name] = dialectType;
178
+ pgTypes[name] = dialectType;
179
+ if (dialectType === 'datetime')
180
+ dateColumns.add(name);
181
+ if (row.index === 'unique') {
182
+ uniqueColumns.push([name]);
183
+ if (!nullable)
184
+ pkCandidates.push(name);
185
+ }
186
+ if (row.index === 'unique' || row.index === 'index') {
187
+ indexes.push({
188
+ name: `${tableName}_${name}_idx`,
189
+ columns: [name],
190
+ unique: row.index === 'unique',
191
+ definition: `${row.index === 'unique' ? 'unique ' : ''}index on ${tableName}(${name})`,
192
+ });
193
+ }
194
+ }
195
+ // Primary key: first non-nullable unique column, preferring one named `id`.
196
+ let primaryKey = [];
197
+ if (pkCandidates.length > 0) {
198
+ primaryKey = [pkCandidates.includes('id') ? 'id' : pkCandidates[0]];
199
+ }
200
+ else {
201
+ console.warn(`[turbine] PowDB introspection: table "${tableName}" has no non-nullable unique column; ` +
202
+ 'primaryKey is [] (single-row operations will fail). Supply a primary key via code-first ' +
203
+ '`defineSchema` metadata if this table needs findUnique/update/delete by id.');
204
+ }
205
+ tables[tableName] = {
206
+ name: tableName,
207
+ columns,
208
+ columnMap,
209
+ reverseColumnMap,
210
+ dateColumns,
211
+ dialectTypes,
212
+ pgTypes,
213
+ allColumns,
214
+ primaryKey,
215
+ uniqueColumns,
216
+ // PowDB has no declared foreign keys → no relations from introspection.
217
+ relations: {},
218
+ indexes,
219
+ };
220
+ }
221
+ return { tables, enums: {} };
222
+ }