turbine-orm 0.40.1 → 0.41.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 (63) hide show
  1. package/README.md +22 -4
  2. package/dist/cjs/cli/config.js +3 -0
  3. package/dist/cjs/cli/index.js +179 -0
  4. package/dist/cjs/cli/prisma-report.js +216 -0
  5. package/dist/cjs/cli/prisma-resolve.js +335 -0
  6. package/dist/cjs/cli/prisma-schema.js +484 -0
  7. package/dist/cjs/client.js +1 -0
  8. package/dist/cjs/generate.js +279 -22
  9. package/dist/cjs/index.js +3 -2
  10. package/dist/cjs/introspect.js +203 -26
  11. package/dist/cjs/mssql.js +9 -10
  12. package/dist/cjs/mysql.js +3 -9
  13. package/dist/cjs/powdb-introspect.js +5 -10
  14. package/dist/cjs/powql.js +13 -0
  15. package/dist/cjs/prisma-compat.js +1147 -0
  16. package/dist/cjs/query/aggregates.js +67 -7
  17. package/dist/cjs/query/builder.js +388 -17
  18. package/dist/cjs/query/compound-unique.js +0 -0
  19. package/dist/cjs/query/relations.js +7 -5
  20. package/dist/cjs/query/warn-registry.js +98 -0
  21. package/dist/cjs/query/writes.js +13 -5
  22. package/dist/cjs/schema.js +47 -0
  23. package/dist/cjs/sqlite.js +4 -9
  24. package/dist/cli/config.d.ts +26 -0
  25. package/dist/cli/config.js +3 -0
  26. package/dist/cli/index.d.ts +11 -0
  27. package/dist/cli/index.js +180 -1
  28. package/dist/cli/prisma-report.d.ts +19 -0
  29. package/dist/cli/prisma-report.js +211 -0
  30. package/dist/cli/prisma-resolve.d.ts +87 -0
  31. package/dist/cli/prisma-resolve.js +330 -0
  32. package/dist/cli/prisma-schema.d.ts +116 -0
  33. package/dist/cli/prisma-schema.js +479 -0
  34. package/dist/cli/ui.d.ts +1 -1
  35. package/dist/client.d.ts +18 -2
  36. package/dist/client.js +1 -0
  37. package/dist/generate.d.ts +80 -1
  38. package/dist/generate.js +277 -25
  39. package/dist/index.d.ts +2 -2
  40. package/dist/index.js +1 -1
  41. package/dist/introspect.d.ts +92 -2
  42. package/dist/introspect.js +198 -26
  43. package/dist/mssql.js +10 -11
  44. package/dist/mysql.js +4 -10
  45. package/dist/powdb-introspect.js +5 -10
  46. package/dist/powql.js +13 -0
  47. package/dist/prisma-compat.d.ts +281 -0
  48. package/dist/prisma-compat.js +1143 -0
  49. package/dist/query/aggregates.js +67 -7
  50. package/dist/query/builder.d.ts +77 -4
  51. package/dist/query/builder.js +390 -19
  52. package/dist/query/compound-unique.d.ts +49 -0
  53. package/dist/query/compound-unique.js +0 -0
  54. package/dist/query/deferred.d.ts +18 -0
  55. package/dist/query/relations.js +7 -5
  56. package/dist/query/types.d.ts +70 -9
  57. package/dist/query/warn-registry.d.ts +57 -0
  58. package/dist/query/warn-registry.js +92 -0
  59. package/dist/query/writes.js +13 -5
  60. package/dist/schema.d.ts +75 -0
  61. package/dist/schema.js +46 -0
  62. package/dist/sqlite.js +5 -10
  63. package/package.json +6 -1
@@ -12,12 +12,17 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
12
12
  return (mod && mod.__esModule) ? mod : { "default": mod };
13
13
  };
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.DEFAULT_EXCLUDED_TABLES = void 0;
15
16
  exports.pgConfActionToReferential = pgConfActionToReferential;
17
+ exports.applyTableFilters = applyTableFilters;
18
+ exports.defaultExcludedTablesPresent = defaultExcludedTablesPresent;
16
19
  exports.introspect = introspect;
17
20
  exports.introspectPostgresCatalog = introspectPostgresCatalog;
18
21
  exports.stripCheckWrapper = stripCheckWrapper;
19
22
  exports.relationNameFromColumn = relationNameFromColumn;
20
23
  exports.isUnknownTsType = isUnknownTsType;
24
+ exports.parsePlainUniqueIndexColumns = parsePlainUniqueIndexColumns;
25
+ exports.detectUniqueForeignKeySets = detectUniqueForeignKeySets;
21
26
  exports.buildRelationsFromForeignKeys = buildRelationsFromForeignKeys;
22
27
  exports.addAutoManyToManyRelations = addAutoManyToManyRelations;
23
28
  exports.deriveEngineRelations = deriveEngineRelations;
@@ -190,6 +195,61 @@ const SQL_ENUMS = `
190
195
  ORDER BY t.typname, e.enumsortorder
191
196
  `;
192
197
  // ---------------------------------------------------------------------------
198
+ // Default table exclusions (F12)
199
+ // ---------------------------------------------------------------------------
200
+ /**
201
+ * Migration-bookkeeping tables that introspection drops by default: Turbine's
202
+ * own `_turbine_migrations` / `_turbine_metrics` and Prisma's
203
+ * `_prisma_migrations`. These are almost never meant to be surfaced as typed
204
+ * accessors, and a fresh migrate-from-Prisma introspection would otherwise emit
205
+ * a `PrismaMigrations` entity plus stray FK-derived relations on neighbours.
206
+ *
207
+ * A table named here is dropped UNLESS it is explicitly listed in
208
+ * `options.include` (`include` is the escape hatch, no separate flag), and
209
+ * naming a default-excluded table restores its old generated output byte for
210
+ * byte. The list is deliberately tight (exactly these three); leading-
211
+ * underscore tables are legitimate user tables and are never blanket-excluded.
212
+ */
213
+ exports.DEFAULT_EXCLUDED_TABLES = ['_turbine_migrations', '_prisma_migrations', '_turbine_metrics'];
214
+ /**
215
+ * The single authority for turning a raw list of candidate table names into the
216
+ * introspected set, shared by the Postgres catalog reader and every engine
217
+ * introspector (SQLite / MySQL / MSSQL / PowDB) so all surfaces agree.
218
+ *
219
+ * Order of operations:
220
+ * 1. `include` filter: when non-empty, keep only the named tables.
221
+ * 2. user `exclude`: drop anything the caller listed.
222
+ * 3. {@link DEFAULT_EXCLUDED_TABLES}: drop migration bookkeeping tables,
223
+ * EXCEPT any that the caller explicitly named in `include` (the escape
224
+ * hatch that restores the pre-0.41 output for those tables).
225
+ */
226
+ function applyTableFilters(names, options = {}) {
227
+ let result = names;
228
+ const includeSet = options.include?.length ? new Set(options.include) : null;
229
+ if (includeSet) {
230
+ result = result.filter((t) => includeSet.has(t));
231
+ }
232
+ if (options.exclude?.length) {
233
+ const excludeSet = new Set(options.exclude);
234
+ result = result.filter((t) => !excludeSet.has(t));
235
+ }
236
+ // Default exclusions never override an explicit include.
237
+ const defaults = new Set(exports.DEFAULT_EXCLUDED_TABLES);
238
+ result = result.filter((t) => !defaults.has(t) || (includeSet?.has(t) ?? false));
239
+ return result;
240
+ }
241
+ /**
242
+ * The subset of {@link DEFAULT_EXCLUDED_TABLES} that were present in `names` but
243
+ * dropped by {@link applyTableFilters} (i.e. not re-added via `include`). Pure
244
+ * helper so the CLI can report "skipped internal table X" without re-deriving
245
+ * the filtering rule.
246
+ */
247
+ function defaultExcludedTablesPresent(names, options = {}) {
248
+ const includeSet = options.include?.length ? new Set(options.include) : null;
249
+ const present = new Set(names);
250
+ return exports.DEFAULT_EXCLUDED_TABLES.filter((t) => present.has(t) && !(includeSet?.has(t) ?? false));
251
+ }
252
+ // ---------------------------------------------------------------------------
193
253
  // Main introspection function
194
254
  // ---------------------------------------------------------------------------
195
255
  /**
@@ -257,16 +317,18 @@ async function introspectPostgresCatalog(options) {
257
317
  onUpdate: pgConfActionToReferential(row.confupdtype),
258
318
  });
259
319
  }
260
- // Filter tables by include/exclude. Views/matviews join the base tables as
261
- // candidates so include/exclude apply uniformly.
262
- let tableNames = [...tablesResult.rows.map((r) => r.table_name), ...viewNameSet];
263
- if (options.include?.length) {
264
- const includeSet = new Set(options.include);
265
- tableNames = tableNames.filter((t) => includeSet.has(t));
266
- }
267
- if (options.exclude?.length) {
268
- const excludeSet = new Set(options.exclude);
269
- tableNames = tableNames.filter((t) => !excludeSet.has(t));
320
+ // Filter tables by include/exclude + default bookkeeping-table exclusions
321
+ // (F12). Views/matviews join the base tables as candidates so the filters
322
+ // apply uniformly.
323
+ const candidateTables = [
324
+ ...tablesResult.rows.map((r) => r.table_name),
325
+ ...viewNameSet,
326
+ ];
327
+ const tableNames = applyTableFilters(candidateTables, options);
328
+ if (options.onDefaultTableExclusion) {
329
+ const skipped = defaultExcludedTablesPresent(candidateTables, options);
330
+ if (skipped.length > 0)
331
+ options.onDefaultTableExclusion(skipped);
270
332
  }
271
333
  const tableSet = new Set(tableNames);
272
334
  // ----- Group columns by table -----
@@ -421,7 +483,13 @@ async function introspectPostgresCatalog(options) {
421
483
  // main, so only genuine json/jsonb columns qualify as historical shadows.
422
484
  unknownTypedFieldsByTable.set(tbl, new Set(cols.filter((c) => isUnknownTsType(c.tsType) && !Object.hasOwn(enums, c.pgType)).map((c) => c.field)));
423
485
  }
424
- const relationsByTable = buildRelationsFromForeignKeys(foreignKeys, columnFieldsByTable, fkActions, unknownTypedFieldsByTable);
486
+ // F2: unless the caller opts out, detect child FK column sets that a unique
487
+ // constraint / plain unique index exactly covers, so the reverse relation is
488
+ // emitted as a one-to-one (`hasOne`) instead of `hasMany`.
489
+ const uniqueSetsByTable = options.legacyToManyUniques
490
+ ? undefined
491
+ : detectUniqueForeignKeySets(pkByTable, uniqueByTable, indexesByTable);
492
+ const relationsByTable = buildRelationsFromForeignKeys(foreignKeys, columnFieldsByTable, fkActions, unknownTypedFieldsByTable, uniqueSetsByTable);
425
493
  // ----- Conservative many-to-many auto-detection (PURELY ADDITIVE) -----
426
494
  //
427
495
  // Auto-detecting m2m is a footgun: any table with two FKs *looks* like a
@@ -551,6 +619,92 @@ function upperFirst(s) {
551
619
  function isUnknownTsType(tsType) {
552
620
  return tsType === 'unknown' || tsType === 'unknown | null';
553
621
  }
622
+ // ---------------------------------------------------------------------------
623
+ // Unique-foreign-key detection for one-to-one relations (F2)
624
+ // ---------------------------------------------------------------------------
625
+ /** True when two column lists cover the same set (order-insensitive, no dupes). */
626
+ function columnSetsEqual(a, b) {
627
+ if (a.length !== b.length)
628
+ return false;
629
+ const bs = new Set(b);
630
+ return a.every((c) => bs.has(c));
631
+ }
632
+ /**
633
+ * Parse the column list of a PLAIN unique index from its `pg_indexes.indexdef`,
634
+ * returning `null` for anything that does NOT guarantee at-most-one child row:
635
+ *
636
+ * - a PARTIAL index (has a `WHERE` clause): only unique within the predicate;
637
+ * - an EXPRESSION index (`lower(email)`, `(a || b)`): the uniqueness is on the
638
+ * expression, not the raw FK column set.
639
+ *
640
+ * Anchors on the `USING <method> (` clause the same way
641
+ * {@link describeIndexDefMismatch} does, so a partial index's `WHERE (...)`
642
+ * parentheses are never mistaken for the column list. Every column token must be
643
+ * a bare or double-quoted identifier; anything else (a function call, an
644
+ * operator expression) fails the check and yields `null`.
645
+ */
646
+ function parsePlainUniqueIndexColumns(indexdef) {
647
+ // Partial index: uniqueness is scoped to the WHERE predicate.
648
+ if (/\bWHERE\b/i.test(indexdef))
649
+ return null;
650
+ const paren = indexdef.match(/USING\s+\w+\s*\(([^)]*)\)/i);
651
+ if (!paren)
652
+ return null;
653
+ const tokens = paren[1].split(',').map((c) => c
654
+ .trim()
655
+ .replace(/\s+(ASC|DESC|NULLS\s+(FIRST|LAST))\b/gi, '')
656
+ .trim());
657
+ const columns = [];
658
+ for (const token of tokens) {
659
+ if (token.length === 0)
660
+ return null;
661
+ if (/^"(?:[^"]|"")*"$/.test(token)) {
662
+ // Quoted identifier: unquote and unescape doubled quotes.
663
+ columns.push(token.slice(1, -1).replace(/""/g, '"'));
664
+ }
665
+ else if (/^[A-Za-z_][A-Za-z0-9_$]*$/.test(token)) {
666
+ columns.push(token);
667
+ }
668
+ else {
669
+ // Expression column (function call, operator, cast, and the like): never a plain FK.
670
+ return null;
671
+ }
672
+ }
673
+ return columns.length > 0 ? columns : null;
674
+ }
675
+ /**
676
+ * Assemble, per table, every column set that EXACTLY guarantees at-most-one row:
677
+ * the primary key, every UNIQUE constraint, and every PLAIN (non-partial,
678
+ * non-expression) UNIQUE index. Consumed by
679
+ * {@link buildRelationsFromForeignKeys} to flip a child relation whose FK column
680
+ * set matches one of these sets from `hasMany` to `hasOne` (F2, Prisma
681
+ * one-to-one parity).
682
+ */
683
+ function detectUniqueForeignKeySets(pkByTable, uniqueByTable, indexesByTable) {
684
+ const result = new Map();
685
+ const add = (table, cols) => {
686
+ if (cols.length === 0)
687
+ return;
688
+ if (!result.has(table))
689
+ result.set(table, []);
690
+ result.get(table).push(cols);
691
+ };
692
+ for (const [table, pk] of pkByTable)
693
+ add(table, pk);
694
+ for (const [table, sets] of uniqueByTable)
695
+ for (const cols of sets)
696
+ add(table, cols);
697
+ for (const [table, indexes] of indexesByTable) {
698
+ for (const idx of indexes) {
699
+ if (!idx.unique)
700
+ continue;
701
+ const cols = parsePlainUniqueIndexColumns(idx.definition);
702
+ if (cols)
703
+ add(table, cols);
704
+ }
705
+ }
706
+ return result;
707
+ }
554
708
  /**
555
709
  * Resolve a derived relation name against the names already taken on the
556
710
  * table (scalar column fields + previously assigned relations). On collision,
@@ -597,8 +751,16 @@ function resolveRelationNameCollision(candidate, taken, table, source) {
597
751
  * guarantee relations never shadow concrete-typed scalar columns.
598
752
  * @param unknownTypedFieldsByTable subset of the column fields whose tsType is
599
753
  * `unknown` (json/jsonb) — legacy shadows of these are preserved (rule 2).
754
+ * @param uniqueSetsByTable when provided (F2), the child-table column sets that
755
+ * guarantee at-most-one row (PK + unique constraints + plain unique indexes,
756
+ * from {@link detectUniqueForeignKeySets}). A reverse relation whose FK column
757
+ * set EXACTLY matches one of the child's unique sets is emitted as `hasOne`
758
+ * (to-one) instead of `hasMany`, and named with the SINGULAR of the child
759
+ * table (falling back to the legacy plural name on collision). Omit it (the
760
+ * engine introspectors and `defineSchema` path do) to keep every reverse
761
+ * relation `hasMany`.
600
762
  */
601
- function buildRelationsFromForeignKeys(foreignKeys, columnFieldsByTable, fkActions, unknownTypedFieldsByTable) {
763
+ function buildRelationsFromForeignKeys(foreignKeys, columnFieldsByTable, fkActions, unknownTypedFieldsByTable, uniqueSetsByTable) {
602
764
  // Count FKs per (source, target) pair for disambiguation.
603
765
  const fkCounts = new Map();
604
766
  for (const fk of foreignKeys) {
@@ -687,26 +849,41 @@ function buildRelationsFromForeignKeys(foreignKeys, columnFieldsByTable, fkActio
687
849
  referenceKey,
688
850
  ...actionFields,
689
851
  };
690
- // --- hasMany on the target (parent) table ---
691
- // e.g. posts.user_id → users.id creates users.posts (hasMany)
692
- const legacyHasMany = needsDisambiguation
852
+ // --- reverse relation on the target (parent) table ---
853
+ // e.g. posts.user_id → users.id creates users.posts (hasMany), UNLESS the
854
+ // child's FK column set is exactly covered by a unique constraint / plain
855
+ // unique index (F2), then it is a one-to-one, emitted as `hasOne` and
856
+ // named with the SINGULAR of the child table.
857
+ const isUniqueFk = (uniqueSetsByTable?.get(fk.sourceTable) ?? []).some((set) => columnSetsEqual(set, fk.sourceColumns));
858
+ const disambSuffix = needsDisambiguation
859
+ ? singleColumn
860
+ ? `By${upperFirst(relationNameFromColumn(fk.sourceColumns[0]))}`
861
+ : `By${upperFirst((0, schema_js_1.snakeToCamel)(constraintBase))}`
862
+ : '';
863
+ const legacyReverse = needsDisambiguation
693
864
  ? singleColumn
694
865
  ? (0, schema_js_1.snakeToCamel)(`${fk.sourceTable}_by_${fk.sourceColumns[0].replace(/_id$/, '')}`)
695
866
  : (0, schema_js_1.snakeToCamel)(`${fk.sourceTable}_by_${constraintBase}`)
696
867
  : (0, schema_js_1.snakeToCamel)(fk.sourceTable);
697
- const modernHasMany = needsDisambiguation
698
- ? singleColumn
699
- ? `${(0, schema_js_1.snakeToCamel)(fk.sourceTable)}By${upperFirst(relationNameFromColumn(fk.sourceColumns[0]))}`
700
- : `${(0, schema_js_1.snakeToCamel)(fk.sourceTable)}By${upperFirst((0, schema_js_1.snakeToCamel)(constraintBase))}`
701
- : null;
702
- const hasManyName = resolveName(legacyHasMany, modernHasMany, fk.targetTable, `FK ${fk.constraintName}`);
703
- takenFor(fk.targetTable).add(hasManyName);
704
- assignedFor(fk.targetTable).add(hasManyName);
868
+ const modernReverse = needsDisambiguation ? `${(0, schema_js_1.snakeToCamel)(fk.sourceTable)}${disambSuffix}` : null;
869
+ let reverseName;
870
+ const reverseType = isUniqueFk ? 'hasOne' : 'hasMany';
871
+ if (isUniqueFk) {
872
+ // Prefer the singular child-table name; fall back to the legacy plural
873
+ // (which stays byte-stable for any app that was on the pre-flip shape).
874
+ const singularReverse = `${(0, schema_js_1.singularize)((0, schema_js_1.snakeToCamel)(fk.sourceTable))}${disambSuffix}`;
875
+ reverseName = resolveName(singularReverse, legacyReverse, fk.targetTable, `FK ${fk.constraintName}`);
876
+ }
877
+ else {
878
+ reverseName = resolveName(legacyReverse, modernReverse, fk.targetTable, `FK ${fk.constraintName}`);
879
+ }
880
+ takenFor(fk.targetTable).add(reverseName);
881
+ assignedFor(fk.targetTable).add(reverseName);
705
882
  if (!relationsByTable.has(fk.targetTable))
706
883
  relationsByTable.set(fk.targetTable, {});
707
- relationsByTable.get(fk.targetTable)[hasManyName] = {
708
- type: 'hasMany',
709
- name: hasManyName,
884
+ relationsByTable.get(fk.targetTable)[reverseName] = {
885
+ type: reverseType,
886
+ name: reverseName,
710
887
  from: fk.targetTable,
711
888
  to: fk.sourceTable,
712
889
  foreignKey,
package/dist/cjs/mssql.js CHANGED
@@ -560,7 +560,12 @@ exports.mssqlDialect = {
560
560
  .map((row) => `(${row.map(() => this.paramPlaceholder(++n)).join(', ')})`)
561
561
  .join(', ');
562
562
  const out = mssqlOutput(input.returning, 'INSERTED');
563
- // skipDuplicates has no single-statement equivalent here; ignored (documented).
563
+ // SQL Server has no single-statement skip-duplicates form (no ON CONFLICT /
564
+ // INSERT IGNORE), so silently dropping the flag would let duplicate rows
565
+ // through against the caller's intent. Refuse loudly instead.
566
+ if (input.skipDuplicates) {
567
+ throw new errors_js_1.UnsupportedFeatureError('createMany({ skipDuplicates: true })', 'mssql', 'SQL Server has no ON CONFLICT DO NOTHING equivalent, pre-filter conflicting rows or use a MERGE.');
568
+ }
564
569
  return {
565
570
  sql: `INSERT INTO ${input.table} (${input.columns.join(', ')})${out} VALUES ${placeholders}`,
566
571
  params: input.rowValues.flat(),
@@ -934,15 +939,9 @@ function buildRelationsFromForeignKeys(tableNames, foreignKeys, pkByTable, colum
934
939
  */
935
940
  async function introspectMssqlWith(exec, schema = 'dbo', options = {}) {
936
941
  // ----- Tables -----
937
- let tableNames = (await exec("SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = @p1 AND TABLE_TYPE = 'BASE TABLE' ORDER BY TABLE_NAME", [schema])).map((r) => String(r.TABLE_NAME));
938
- if (options.include?.length) {
939
- const inc = new Set(options.include);
940
- tableNames = tableNames.filter((t) => inc.has(t));
941
- }
942
- if (options.exclude?.length) {
943
- const exc = new Set(options.exclude);
944
- tableNames = tableNames.filter((t) => !exc.has(t));
945
- }
942
+ const candidateTables = (await exec("SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = @p1 AND TABLE_TYPE = 'BASE TABLE' ORDER BY TABLE_NAME", [schema])).map((r) => String(r.TABLE_NAME));
943
+ // include / exclude + default bookkeeping-table exclusions (F12).
944
+ const tableNames = (0, introspect_js_1.applyTableFilters)(candidateTables, options);
946
945
  const tableSet = new Set(tableNames);
947
946
  // ----- Identity columns (mark hasDefault) -----
948
947
  const identityRows = await exec(`SELECT t.name AS TABLE_NAME, c.name AS COLUMN_NAME
package/dist/cjs/mysql.js CHANGED
@@ -596,15 +596,9 @@ function buildRelationsFromForeignKeys(tableNames, foreignKeys, pkByTable, colum
596
596
  */
597
597
  async function introspectMysqlWith(exec, schema, options = {}) {
598
598
  // ----- Tables -----
599
- let tableNames = (await exec("SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA = :p1 AND TABLE_TYPE = 'BASE TABLE' ORDER BY TABLE_NAME", [schema])).map((r) => String(r.TABLE_NAME));
600
- if (options.include?.length) {
601
- const inc = new Set(options.include);
602
- tableNames = tableNames.filter((t) => inc.has(t));
603
- }
604
- if (options.exclude?.length) {
605
- const exc = new Set(options.exclude);
606
- tableNames = tableNames.filter((t) => !exc.has(t));
607
- }
599
+ const candidateTables = (await exec("SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA = :p1 AND TABLE_TYPE = 'BASE TABLE' ORDER BY TABLE_NAME", [schema])).map((r) => String(r.TABLE_NAME));
600
+ // include / exclude + default bookkeeping-table exclusions (F12).
601
+ const tableNames = (0, introspect_js_1.applyTableFilters)(candidateTables, options);
608
602
  const tableSet = new Set(tableNames);
609
603
  // ----- Columns -----
610
604
  const columnRows = (await exec(`SELECT TABLE_NAME, COLUMN_NAME, DATA_TYPE, COLUMN_TYPE, IS_NULLABLE, COLUMN_KEY,
@@ -57,6 +57,7 @@
57
57
  Object.defineProperty(exports, "__esModule", { value: true });
58
58
  exports.introspectPowdbDatabase = introspectPowdbDatabase;
59
59
  const errors_js_1 = require("./errors.js");
60
+ const introspect_js_1 = require("./introspect.js");
60
61
  const powdb_js_1 = require("./powdb.js");
61
62
  const schema_js_1 = require("./schema.js");
62
63
  /** Coerce a wire cell to string (legacy wire cells are strings; native cells may be typed). */
@@ -110,24 +111,18 @@ async function introspectPowdbDatabase(exec, options = {}) {
110
111
  }
111
112
  // ----- Types (one row per table, columns `name`, `columns`) -----
112
113
  const schemaRows = (await exec('schema')).rows;
113
- let tableNames = schemaRows.map((r) => asString(r.name)).filter((n) => n.length > 0);
114
+ const candidateTables = schemaRows.map((r) => asString(r.name)).filter((n) => n.length > 0);
114
115
  // A mis-shaped `exec` (e.g. the raw client's positional `string[][]` rows
115
116
  // passed straight through) yields rows whose `name` cell is `undefined`, so
116
117
  // every table filters out and the schema comes back silently empty. Refuse
117
118
  // that instead of losing data: real rows must carry a `name`.
118
- if (schemaRows.length > 0 && tableNames.length === 0) {
119
+ if (schemaRows.length > 0 && candidateTables.length === 0) {
119
120
  throw new errors_js_1.ValidationError(`[turbine] PowDB introspection: the \`schema\` statement returned ${schemaRows.length} row(s) but none carried a ` +
120
121
  '`name` cell. The `exec` you supplied likely returns POSITIONAL rows (string[][]) rather than records keyed by ' +
121
122
  'column name; zip `columns` with each row (see introspectPowdbDatabase docs).');
122
123
  }
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
- }
124
+ // include / exclude + default bookkeeping-table exclusions (F12).
125
+ const tableNames = (0, introspect_js_1.applyTableFilters)(candidateTables, options);
131
126
  const tables = {};
132
127
  for (const tableName of tableNames) {
133
128
  // `describe` needs the table name in bare-identifier position → quote it so
package/dist/cjs/powql.js CHANGED
@@ -74,6 +74,7 @@ const node_crypto_1 = require("node:crypto");
74
74
  const errors_js_1 = require("./errors.js");
75
75
  const nested_write_js_1 = require("./nested-write.js");
76
76
  const powdb_js_1 = require("./powdb.js");
77
+ const compound_unique_js_1 = require("./query/compound-unique.js");
77
78
  const filters_js_1 = require("./query/filters.js");
78
79
  const utils_js_1 = require("./query/utils.js");
79
80
  const schema_js_1 = require("./schema.js");
@@ -1043,6 +1044,13 @@ class PowqlInterface {
1043
1044
  .filter((line) => line.length > 0);
1044
1045
  }
1045
1046
  async findUnique(args) {
1047
+ // Prisma compound-unique selector → column conjunction (engine parity with
1048
+ // the SQL findUnique family; pure metadata, so this is a one-line adoption).
1049
+ if (args.where) {
1050
+ const expanded = (0, compound_unique_js_1.expandCompoundUniqueWhere)(this.meta, args.where);
1051
+ if (expanded !== args.where)
1052
+ args = { ...args, where: expanded };
1053
+ }
1046
1054
  return this.withMiddleware('findUnique', args, async () => {
1047
1055
  const { rows, native, nestedPlans, residualWith } = await this.runFind({ ...args, limit: 1 }, 'findUnique');
1048
1056
  if (!rows.length)
@@ -1772,6 +1780,11 @@ class PowqlInterface {
1772
1780
  }
1773
1781
  async createMany(args) {
1774
1782
  return this.withMiddleware('createMany', args, async () => {
1783
+ if (args.skipDuplicates) {
1784
+ // PowQL's `insert … returning` has no conflict clause, so there is no
1785
+ // faithful skip-duplicates form; refuse rather than silently insert.
1786
+ throw new errors_js_1.UnsupportedFeatureError('createMany({ skipDuplicates: true })', 'powdb', 'PowQL insert has no ON CONFLICT DO NOTHING equivalent, filter duplicates before inserting.');
1787
+ }
1775
1788
  const inputs = args.data.map((d) => this.applyPkDefault(d));
1776
1789
  if (!inputs.length)
1777
1790
  return [];