uql-orm 0.64.0 → 0.65.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 (77) hide show
  1. package/dist/bunSql/bunSqlQuerier.d.ts +3 -10
  2. package/dist/bunSql/bunSqlQuerier.js +2 -16
  3. package/dist/bunSql/bunSqlQuerierPool.d.ts +0 -2
  4. package/dist/bunSql/bunSqlQuerierPool.js +14 -16
  5. package/dist/d1/d1Querier.d.ts +13 -34
  6. package/dist/d1/d1Querier.js +1 -1
  7. package/dist/d1/d1QuerierPool.d.ts +3 -3
  8. package/dist/dialect/abstractDialect.d.ts +4 -9
  9. package/dist/dialect/abstractDialect.js +4 -5
  10. package/dist/dialect/abstractSqlDialect.d.ts +2 -2
  11. package/dist/dialect/index.d.ts +0 -1
  12. package/dist/dialect/index.js +2 -3
  13. package/dist/dialect/mysqlLikeSqlDialect.js +0 -3
  14. package/dist/dialect/pgLikeSqlDialect.d.ts +9 -1
  15. package/dist/dialect/pgLikeSqlDialect.js +8 -5
  16. package/dist/dialect/queryContext.d.ts +3 -3
  17. package/dist/entity/metadata/definition.d.ts +6 -0
  18. package/dist/entity/metadata/definition.js +109 -76
  19. package/dist/migrate/cli.d.ts +1 -1
  20. package/dist/migrate/cli.js +20 -34
  21. package/dist/migrate/codegen/entityCodeGenerator.d.ts +5 -0
  22. package/dist/migrate/codegen/entityCodeGenerator.js +29 -7
  23. package/dist/migrate/codegen/indexDecoratorSource.js +1 -5
  24. package/dist/migrate/codegen/sourceLiteral.d.ts +2 -0
  25. package/dist/migrate/codegen/sourceLiteral.js +4 -0
  26. package/dist/migrate/index.d.ts +1 -1
  27. package/dist/migrate/index.js +1 -1
  28. package/dist/migrate/introspection/registry.d.ts +2 -2
  29. package/dist/migrate/introspection/registry.js +6 -11
  30. package/dist/migrate/migrationTarget.d.ts +6 -6
  31. package/dist/migrate/migrationTarget.js +18 -16
  32. package/dist/migrate/migrator.d.ts +3 -7
  33. package/dist/migrate/migrator.js +10 -31
  34. package/dist/migrate/schemaGenerator.d.ts +1 -3
  35. package/dist/migrate/schemaGenerator.js +0 -4
  36. package/dist/mongo/mongoDialect.d.ts +1 -1
  37. package/dist/mongo/mongoDialect.js +1 -4
  38. package/dist/mssql/mssqlDialect.js +0 -3
  39. package/dist/pglite/pgliteQuerier.d.ts +2 -6
  40. package/dist/pglite/pgliteQuerier.js +2 -9
  41. package/dist/postgres/index.d.ts +0 -1
  42. package/dist/postgres/index.js +0 -1
  43. package/dist/querier/abstractSharedHandleQuerierPool.d.ts +2 -5
  44. package/dist/querier/abstractSharedHandleQuerierPool.js +2 -5
  45. package/dist/querier/abstractSqlQuerier.d.ts +2 -3
  46. package/dist/querier/abstractSqlQuerier.js +8 -4
  47. package/dist/querier/cursorStream.d.ts +13 -0
  48. package/dist/{postgres/pgCursorStream.js → querier/cursorStream.js} +4 -13
  49. package/dist/schema/canonicalType.js +1 -1
  50. package/dist/schema/schemaASTBuilder.js +34 -40
  51. package/dist/sqlite/abstractSqliteQuerier.d.ts +7 -3
  52. package/dist/sqlite/abstractSqliteQuerier.js +18 -4
  53. package/dist/sqlite/hranaQuerier.d.ts +1 -1
  54. package/dist/sqlite/localSqliteQuerierPool.d.ts +7 -0
  55. package/dist/sqlite/localSqliteQuerierPool.js +19 -0
  56. package/dist/sqlite/nodeSqliteQuerierPool.js +2 -3
  57. package/dist/sqlite/sqliteDialect.js +0 -3
  58. package/dist/sqlite/sqliteQuerier.d.ts +8 -5
  59. package/dist/sqlite/sqliteQuerier.js +4 -13
  60. package/dist/sqlite/sqliteQuerierPool.js +4 -4
  61. package/dist/turso/tursoSessionQuerier.d.ts +2 -2
  62. package/dist/turso/tursoSessionQuerier.js +16 -18
  63. package/dist/type/dialect.d.ts +25 -35
  64. package/dist/type/entity.d.ts +26 -28
  65. package/dist/type/migratorDialect.d.ts +0 -9
  66. package/dist/type/migratorDialect.js +1 -16
  67. package/dist/type/querierPool.d.ts +0 -4
  68. package/dist/type/queryRaw.d.ts +2 -2
  69. package/dist/type/universalQuerier.d.ts +2 -2
  70. package/package.json +1 -3
  71. package/dist/postgres/pgCursorStream.d.ts +0 -20
  72. package/dist/postgres/postgresWireDriverCapabilities.d.ts +0 -21
  73. package/dist/postgres/postgresWireDriverCapabilities.js +0 -21
  74. package/dist/sqlite/bunSqliteAdapter.bun.d.ts +0 -27
  75. package/dist/sqlite/bunSqliteAdapter.bun.js +0 -25
  76. package/dist/sqlite/nodeSqliteAdapter.d.ts +0 -33
  77. package/dist/sqlite/nodeSqliteAdapter.js +0 -25
@@ -1,6 +1,6 @@
1
1
  import { SOFT_DELETE_FILTER } from '../../type/index.js';
2
2
  import { isInlinedExpression } from '../../util/field.util.js';
3
- import { entityName, entitySql, entityWhere, fieldOptionConflict, getKeys, hasKeys, isToManyRelation, lowerFirst, memberRefs, normalizeIndexColumn, upperFirst, definedEntries, } from '../../util/index.js';
3
+ import { entitySql, entityWhere, fieldOptionConflict, getKeys, hasKeys, isToManyRelation, lowerFirst, memberRefs, normalizeIndexColumn, upperFirst, definedEntries, } from '../../util/index.js';
4
4
  import { ownRegistrations } from '../decorator/bag.js';
5
5
  /**
6
6
  * A map held on `globalThis` through the global symbol registry, so a single one survives multiple
@@ -54,10 +54,11 @@ export function defineRelation(entity, key, opts) {
54
54
  * exist, and the registry holds data alone.
55
55
  */
56
56
  export function relationRegistration({ mappedBy, references, ...opts }) {
57
+ const joined = references?.(keyMap(), keyMap());
57
58
  return {
58
59
  ...opts,
59
60
  ...(mappedBy && { mappedBy: mappedBy(keyMap()) }),
60
- ...(references && { references: [...references(keyMap(), keyMap())] }),
61
+ ...(joined && { references: typeof joined === 'string' ? joined : [...joined] }),
61
62
  };
62
63
  }
63
64
  /** Every entity's key map: a callback only reads one property off it, and that property is its own key. */
@@ -70,8 +71,8 @@ function addRelation(entity, key, registration) {
70
71
  throw new TypeError(`'${entity.name}.${key}' needs an 'entity' getter, e.g. '@ManyToOne({ entity: () => Company })'.`);
71
72
  }
72
73
  if (registration.through && registration.references) {
73
- throw new TypeError(`'${entity.name}.${key}' joins through a junction, whose columns follow the convention; 'references' ` +
74
- "pairs the declaring entity's columns with the target's instead.");
74
+ throw new TypeError(`'${entity.name}.${key}' joins through a junction, whose column referencing each side is the join; ` +
75
+ "'references' pairs the declaring entity's columns with the target's instead.");
75
76
  }
76
77
  const meta = ensureWritableMeta(entity);
77
78
  // Registration writes into a map declared as resolved: `getMeta` runs `fillRelations`, which settles
@@ -312,51 +313,62 @@ export function getMeta(entity) {
312
313
  if (meta.processedAt === meta.revision) {
313
314
  return meta;
314
315
  }
315
- // Stamped before finalizing, not after: `fillInverseSide` reads the other side through `getMeta`,
316
- // and with each side mapped by the other that recursion has to find this half-filled meta rather
317
- // than run again. Finalizing twice is harmless anyway - every step of it skips what it settled.
316
+ // Stamped before finalizing: `fillInverseSide` reads the other side through `getMeta`, and with each
317
+ // side mapped by the other that recursion has to find this half-filled meta rather than run again.
318
+ // Unstamped when finalizing throws, so the next read reports the same mistake; running it again is
319
+ // harmless, since every step skips what it settled.
318
320
  meta.processedAt = meta.revision;
319
- return fillRelations(meta);
321
+ try {
322
+ return fillRelations(meta);
323
+ }
324
+ catch (error) {
325
+ meta.processedAt = undefined;
326
+ throw error;
327
+ }
320
328
  }
321
329
  function fillRelations(meta) {
322
330
  for (const [relKey, relation] of definedEntries(meta.relations)) {
323
- // The registered view: `references` may be unset until this settles it.
331
+ // The registered view: `references` may be unset, or the one column a to-one names, until this settles it.
324
332
  const relOpts = relation;
325
333
  const at = `'${meta.entity.name}.${relKey}'`;
326
- if (relOpts.mappedBy) {
327
- fillInverseSide(at, meta, relOpts, relOpts.mappedBy);
328
- }
329
- else if (!relOpts.references) {
330
- fillOwningSide(at, meta, relKey, relOpts);
331
- }
332
- if (!relOpts.references?.length) {
334
+ const references = relOpts.mappedBy
335
+ ? fillInverseSide(at, meta, relOpts, relOpts.mappedBy)
336
+ : (pairedReferences(at, relOpts) ?? fillOwningSide(at, meta, relKey, relOpts));
337
+ if (!references.length) {
333
338
  throw new TypeError(`${at} has no columns to join on.`);
334
339
  }
335
- // Hand-written `references` land here too: naming the columns says which they are, not that they exist.
336
- if (relOpts.through) {
337
- const junction = getMeta(relOpts.through());
338
- for (const { local } of relOpts.references) {
339
- if (junction.fields[local])
340
- continue;
341
- throw new TypeError(`${at} joins through '${junction.entity.name}', which has no '${local}' field: a junction's ` +
342
- 'columns are named after the entities it joins. Declare it.');
343
- }
344
- }
345
340
  }
346
- fillForeignKeyRelations(meta);
341
+ // A column `references` names is a foreign key with or without a relation over it, and one cannot point
342
+ // at a composite key: refused on first read, as a relation that cannot join is, not at the schema build.
343
+ foreignKeysOf(meta);
347
344
  return meta;
348
345
  }
346
+ /**
347
+ * The pairs a relation joins on, with the one column a to-one names paired with the target's primary key:
348
+ * on first read rather than at registration, which can run before the target has a key to pair with.
349
+ */
350
+ function pairedReferences(at, relOpts) {
351
+ const { references } = relOpts;
352
+ if (typeof references !== 'string') {
353
+ return references;
354
+ }
355
+ const target = ensureMeta(relOpts.entity());
356
+ if (relOpts.mappedBy || isToManyRelation(relOpts) || target.ids.length > 1) {
357
+ throw new TypeError(`${at} names one column, '${references}', which only a to-one holding a foreign key to a one-column key ` +
358
+ 'can: pair the columns, [{ local, foreign }].');
359
+ }
360
+ relOpts.references = [{ local: references, foreign: soleIdOf(target, 'a foreign key') }];
361
+ return relOpts.references;
362
+ }
349
363
  function fillOwningSide(at, meta, relKey, relOpts) {
350
364
  const relMeta = ensureMeta(relOpts.entity());
351
365
  if (relOpts.through) {
352
366
  // Both columns live on the junction, whatever the cardinality: `deleteRelations` and every dialect
353
367
  // read them as junction columns. A composite key contributes one pair per column of it, which is
354
368
  // what makes the join address a whole key rather than part.
355
- relOpts.references = [
356
- ...meta.ids.map((key) => ({ local: junctionColumn(meta, key), foreign: key })),
357
- ...relMeta.ids.map((key) => ({ local: junctionColumn(relMeta, key), foreign: key })),
358
- ];
359
- return;
369
+ const junction = getMeta(relOpts.through());
370
+ relOpts.references = [...junctionReferences(at, junction, meta), ...junctionReferences(at, junction, relMeta)];
371
+ return relOpts.references;
360
372
  }
361
373
  if (isToManyRelation(relOpts)) {
362
374
  throw new TypeError(`${at} is a to-many relation with no way to join: it needs 'mappedBy' (the field on the other side), ` +
@@ -366,16 +378,23 @@ function fillOwningSide(at, meta, relKey, relOpts) {
366
378
  // property, so both are spelled from the referenced *property* - a column name is what the naming
367
379
  // strategy makes of this afterwards.
368
380
  const sole = relMeta.ids.length === 1;
369
- relOpts.references = relMeta.ids.map((key) => ({
381
+ const references = relMeta.ids.map((key) => ({
370
382
  local: sole ? `${relKey}Id` : `${relKey}${upperFirst(key)}`,
371
383
  foreign: key,
372
384
  }));
385
+ // A column the entity declares would be joined by its name alone, so renaming either one would leave
386
+ // the other behind, still compiling.
387
+ const fields = meta.fields;
388
+ if (references.some(({ local }) => fields[local])) {
389
+ const own = lowerFirst(meta.entity.name);
390
+ throw new TypeError(`${at} joins ${references.map(({ local }) => `'${local}'`).join(', ')} by name, which a rename does not ` +
391
+ `follow: link them with ${sole ? `'references: (${own}) => ${own}.${references[0].local}'` : "'references' pairs"}.`);
392
+ }
373
393
  // `typeFromReference` so schema generation resolves the referenced primary key's exact type
374
394
  // (columnType, length, chained keys) rather than trusting the fallback, as it does for an
375
395
  // explicit `@Field({ references })`.
376
- const fields = meta.fields;
377
- for (const { local, foreign } of relOpts.references) {
378
- fields[local] ??= {
396
+ for (const { local, foreign } of references) {
397
+ fields[local] = {
379
398
  name: local,
380
399
  type: fieldOf(relMeta, foreign).type ?? Number,
381
400
  references: relOpts.entity,
@@ -383,12 +402,15 @@ function fillOwningSide(at, meta, relKey, relOpts) {
383
402
  typeFromReference: true,
384
403
  };
385
404
  }
405
+ relOpts.references = references;
406
+ return references;
386
407
  }
387
408
  function fillInverseSide(at, meta, relOpts, mappedBy) {
388
409
  const relEntity = relOpts.entity();
389
410
  const relMeta = getMeta(relEntity);
390
- if (relOpts.references)
391
- return;
411
+ const own = pairedReferences(at, relOpts);
412
+ if (own)
413
+ return own;
392
414
  if (relMeta.fields[mappedBy]) {
393
415
  if (meta.ids.length > 1) {
394
416
  throw new TypeError(`${at} is mapped by '${relEntity.name}.${mappedBy}', one column, but the primary key of ` +
@@ -397,7 +419,7 @@ function fillInverseSide(at, meta, relOpts, mappedBy) {
397
419
  }
398
420
  // `local` is this entity's own key, as in every other pair.
399
421
  relOpts.references = [{ local: meta.ids[0], foreign: mappedBy }];
400
- return;
422
+ return relOpts.references;
401
423
  }
402
424
  // Authored view again: with each side mapped by the other, the target is still mid-resolution here and
403
425
  // its own `references` are unset, which is what the second throw reports.
@@ -405,7 +427,8 @@ function fillInverseSide(at, meta, relOpts, mappedBy) {
405
427
  if (!owner) {
406
428
  throw new TypeError(`${at} is mapped by '${mappedBy}', which is neither a field nor a relation of '${relEntity.name}'.`);
407
429
  }
408
- if (!owner.references?.length) {
430
+ const ownerReferences = pairedReferences(`'${relEntity.name}.${mappedBy}'`, owner);
431
+ if (!ownerReferences?.length) {
409
432
  throw new TypeError(`${at} is mapped by '${relEntity.name}.${mappedBy}', an inverse side too, so neither owns the foreign key.`);
410
433
  }
411
434
  // Two different flips: a junction's pairs are the owner's group followed by ours, so the two groups
@@ -413,50 +436,55 @@ function fillInverseSide(at, meta, relOpts, mappedBy) {
413
436
  // plain foreign key is one pair per key whose ends swap.
414
437
  relOpts.references =
415
438
  relOpts.cardinality === 'm1' || relOpts.cardinality === 'mm'
416
- ? [...owner.references.slice(relMeta.ids.length), ...owner.references.slice(0, relMeta.ids.length)]
417
- : owner.references.map(({ local, foreign }) => ({ local: foreign, foreign: local }));
439
+ ? [...ownerReferences.slice(relMeta.ids.length), ...ownerReferences.slice(0, relMeta.ids.length)]
440
+ : ownerReferences.map(({ local, foreign }) => ({ local: foreign, foreign: local }));
418
441
  relOpts.through = owner.through;
442
+ return relOpts.references;
419
443
  }
420
444
  /**
421
- * A field carrying `references` is a foreign key, and a foreign key is a many-to-one whether or not
422
- * anyone declared the relation. Deriving it everywhere is what lets a junction be written as two plain
423
- * columns: it needs the relations for `$populate` and for its DDL constraints, and it used to get them
424
- * only because some *other* entity pointed `through` at it. Gaps only, so a declared relation keeps its
425
- * own cardinality and `cascade`.
445
+ * The foreign keys an entity holds: each owning to-one's columns, and each `@Field({ references })` no
446
+ * relation joins on, as the many-to-one it describes, once its target has registered a key. What the
447
+ * schema build constrains and a junction joins by.
426
448
  */
427
- function fillForeignKeyRelations(meta) {
428
- const joined = new Set(definedEntries(meta.relations).flatMap(([, relation]) => relation.references.map(({ local }) => local)));
429
- for (const [fieldKey, { references }] of definedEntries(meta.fields)) {
430
- if (!references || joined.has(fieldKey))
431
- continue;
432
- const target = ensureMeta(references());
433
- // Nothing to derive from an entity that has not registered its own fields yet.
449
+ export function foreignKeysOf(meta) {
450
+ const owning = definedEntries(meta.relations)
451
+ .map(([, relation]) => relation)
452
+ .filter(({ cardinality, mappedBy }) => cardinality === 'm1' || (cardinality === '11' && !mappedBy));
453
+ const joined = new Set(owning.flatMap(({ references }) => references.map(({ local }) => local)));
454
+ const columns = definedEntries(meta.fields).flatMap(([key, field]) => {
455
+ if (!field.references || joined.has(key))
456
+ return [];
457
+ const target = ensureMeta(field.references());
434
458
  if (!target.ids.length)
435
- continue;
459
+ return [];
436
460
  if (target.ids.length > 1) {
437
- throw new TypeError(`'${meta.entity.name}.${fieldKey}' cannot reference '${target.entity.name}', whose primary key is composite ` +
461
+ throw new TypeError(`'${meta.entity.name}.${key}' cannot reference '${target.entity.name}', whose primary key is composite ` +
438
462
  `(${target.ids.join(', ')}): a column points at one. Use ` +
439
463
  `'@ManyToOne({ entity: () => ${target.entity.name} })', which declares one column per key.`);
440
464
  }
441
- const [foreign] = target.ids;
442
- // The relation takes the column's name minus the key it points at (`itemId` -> `item`); a column
443
- // named anything else has no name to take, so it stays a plain foreign key.
444
- const suffix = upperFirst(foreign);
445
- if (!fieldKey.endsWith(suffix))
446
- continue;
447
- const relKey = fieldKey.slice(0, -suffix.length);
448
- if (!relKey || meta.fields[relKey] || meta.relations[relKey])
449
- continue;
450
- meta.relations[relKey] = {
451
- entity: references,
452
- cardinality: 'm1',
453
- references: [{ local: fieldKey, foreign }],
454
- };
455
- }
456
- }
457
- /** `<entityName><IdColumn>`, not the `<relationKey>Id` an owning to-one derives: a junction row has no relation key to borrow from. */
458
- function junctionColumn(meta, idKey) {
459
- return lowerFirst(entityName(meta)) + upperFirst(fieldOf(meta, idKey).name ?? idKey);
465
+ return [{ entity: field.references, cardinality: 'm1', references: [{ local: key, foreign: target.ids[0] }] }];
466
+ });
467
+ return [...owning, ...columns];
468
+ }
469
+ /** Each key of `side`, paired with the one column of `junction` referencing it: a rename of either follows. */
470
+ function junctionReferences(at, junction, side) {
471
+ const pairs = foreignKeysOf(junction).flatMap(({ entity, references }) => entity() === side.entity ? references : []);
472
+ return side.ids.map((key) => {
473
+ const [pair, ...others] = pairs.filter(({ foreign }) => foreign === key);
474
+ const referenced = `'${side.entity.name}.${key}'`;
475
+ if (!pair) {
476
+ const declare = side.ids.length > 1
477
+ ? `@ManyToOne({ entity: () => ${side.entity.name} })`
478
+ : `@Field({ references: () => ${side.entity.name} })`;
479
+ throw new TypeError(`${at} joins through '${junction.entity.name}', which has no column referencing ${referenced}: declare one, '${declare}'.`);
480
+ }
481
+ if (others.length) {
482
+ const columns = [pair, ...others].map(({ local }) => `'${local}'`).join(' and ');
483
+ throw new TypeError(`${at} joins through '${junction.entity.name}', where ${columns} each reference ${referenced}: a junction ` +
484
+ 'needs exactly one column per key of each side.');
485
+ }
486
+ return { local: pair.local, foreign: key };
487
+ });
460
488
  }
461
489
  /** Every key the entity marks, in declaration order. More than one is a composite primary key. */
462
490
  function getIdKeys(meta) {
@@ -490,7 +518,12 @@ function extendMeta(target, source) {
490
518
  }
491
519
  }
492
520
  target.fields = { ...sourceFields, ...target.fields };
493
- target.relations = { ...source.relations, ...target.relations };
521
+ // A copy of each relation per entity: resolving one writes the columns it joins on into it, and those
522
+ // columns are the resolving entity's, so one shared object left every later entity without them.
523
+ target.relations = {
524
+ ...Object.fromEntries(definedEntries(source.relations).map(([key, relation]) => [key, { ...relation }])),
525
+ ...target.relations,
526
+ };
494
527
  // Inherit user-defined filters from the parent (child overrides by name). The built-in soft-delete
495
528
  // filter + `meta.softDelete` are (re)derived from the merged fields in `defineEntity`.
496
529
  if (source.filters) {
@@ -14,5 +14,5 @@ export declare function runGenerateFromEntities(migrator: Migrator, args: string
14
14
  */
15
15
  export declare function runTypes(migrator: Migrator, args: string[]): void;
16
16
  export declare function runSync(migrator: Migrator, args: string[], config: Partial<Config>): Promise<void>;
17
- export declare function runGenerateFromDb(migrator: Migrator, args: string[], config: Partial<Config>): Promise<void>;
17
+ export declare function runGenerateFromDb(migrator: Migrator, args: string[]): Promise<void>;
18
18
  export declare function runDriftCheck(migrator: Migrator, config: Partial<Config>): Promise<void>;
@@ -56,7 +56,7 @@ export async function main(args = process.argv.slice(2)) {
56
56
  break;
57
57
  case 'generate:from-db':
58
58
  case 'generate-from-db':
59
- await runGenerateFromDb(migrator, filteredArgs.slice(1), config);
59
+ await runGenerateFromDb(migrator, filteredArgs.slice(1));
60
60
  break;
61
61
  case 'sync':
62
62
  await runSync(migrator, filteredArgs.slice(1), config);
@@ -197,7 +197,7 @@ function readOutput(args) {
197
197
  export async function runSync(migrator, args, config) {
198
198
  // Pulling the database into entity files is what `generate:from-db` does; one implementation.
199
199
  if (args.includes('--pull')) {
200
- return runGenerateFromDb(migrator, args, config);
200
+ return runGenerateFromDb(migrator, args);
201
201
  }
202
202
  const force = args.includes('--force');
203
203
  const safe = !args.includes('--unsafe');
@@ -216,46 +216,32 @@ export async function runSync(migrator, args, config) {
216
216
  await migrator.sync({ ...options, logging: true });
217
217
  console.log('\nSchema sync completed.');
218
218
  }
219
- export async function runGenerateFromDb(migrator, args, config) {
219
+ export async function runGenerateFromDb(migrator, args) {
220
220
  const outputDir = readOutput(args) ?? './src/entities';
221
- if (!migrator.schemaIntrospector) {
222
- console.error('No introspector available. Check your pool configuration.');
223
- process.exit(1);
224
- }
225
- else {
226
- console.log('\nAnalyzing database schema...');
227
- const ast = await migrator.schemaIntrospector.introspect();
228
- const tableCount = ast.tables.size;
229
- console.log(`Found ${tableCount} table(s): ${Array.from(ast.tables.keys()).join(', ')}`);
230
- console.log('\nGenerating entities...');
231
- const generator = createEntityCodeGenerator(ast, {
232
- addSyncComments: true,
233
- includeRelations: true,
234
- includeIndexes: true,
235
- });
236
- const entities = generator.generateAll();
237
- // Ensure output directory exists
238
- if (!fs.existsSync(outputDir)) {
239
- fs.mkdirSync(outputDir, { recursive: true });
240
- }
241
- // Write entity files
242
- for (const entity of entities) {
243
- const filePath = path.join(outputDir, entity.fileName);
244
- fs.writeFileSync(filePath, entity.code, 'utf-8');
245
- console.log(` ✓ ${entity.className} -> ${filePath}`);
246
- }
247
- console.log(`\nGenerated ${entities.length} entities to ${outputDir}`);
221
+ console.log('\nAnalyzing database schema...');
222
+ const ast = await migrator.schemaIntrospector.introspect();
223
+ const tableCount = ast.tables.size;
224
+ console.log(`Found ${tableCount} table(s): ${Array.from(ast.tables.keys()).join(', ')}`);
225
+ console.log('\nGenerating entities...');
226
+ const generator = createEntityCodeGenerator(ast, {
227
+ addSyncComments: true,
228
+ includeRelations: true,
229
+ includeIndexes: true,
230
+ });
231
+ const entities = generator.generateAll();
232
+ fs.mkdirSync(outputDir, { recursive: true });
233
+ for (const entity of entities) {
234
+ const filePath = path.join(outputDir, entity.fileName);
235
+ fs.writeFileSync(filePath, entity.code, 'utf-8');
236
+ console.log(` ✓ ${entity.className} -> ${filePath}`);
248
237
  }
238
+ console.log(`\nGenerated ${entities.length} entities to ${outputDir}`);
249
239
  }
250
240
  export async function runDriftCheck(migrator, config) {
251
241
  if (!config.entities || config.entities.length === 0) {
252
242
  console.error('No entities configured. Add entities to your uql config.');
253
243
  process.exit(1);
254
244
  }
255
- else if (!migrator.schemaIntrospector) {
256
- console.error('No introspector available. Check your pool configuration.');
257
- process.exit(1);
258
- }
259
245
  else {
260
246
  console.log('\nChecking for schema drift...');
261
247
  const expectedAST = buildEntityAST(await migrator.getSchemaGenerator(), config.entities);
@@ -98,6 +98,11 @@ export declare class EntityCodeGenerator {
98
98
  * Build outgoing relation (ManyToOne or OneToOne where this table has FK).
99
99
  */
100
100
  private buildOutgoingRelation;
101
+ /**
102
+ * The `references` callback of a to-one: its foreign key column where that is the target's whole primary
103
+ * key, column pairs otherwise, and nothing when the columns do not pair up.
104
+ */
105
+ private referencesSource;
101
106
  /**
102
107
  * Build incoming relation (OneToMany where other tables have FK to this).
103
108
  */
@@ -14,6 +14,7 @@ import { DEFAULT_FOREIGN_KEY_ACTION, } from '../../schema/types.js';
14
14
  import { camelCase, lowerFirst, pascalCase, singularize } from '../../util/string.util.js';
15
15
  import { buildFieldOptionsSource, fieldNeedsRaw } from './fieldOptionsSource.js';
16
16
  import { buildIndexDecoratorSource, indexNeedsRaw, isPlainFieldIndex } from './indexDecoratorSource.js';
17
+ import { memberSource } from './sourceLiteral.js';
17
18
  /**
18
19
  * Generates TypeScript entity code from SchemaAST.
19
20
  */
@@ -234,17 +235,38 @@ export class EntityCodeGenerator {
234
235
  }
235
236
  // Decorator. `onDelete`/`onUpdate` only when introspection found a real referential action, so a
236
237
  // round-trip through an unconstrained column stays as terse as before.
237
- const fkActions = [];
238
+ const options = [`entity: () => ${relatedClassName}`];
239
+ const references = this.referencesSource(rel, relatedClassName);
240
+ if (references)
241
+ options.push(`references: ${references}`);
238
242
  if (rel.onDelete && rel.onDelete !== DEFAULT_FOREIGN_KEY_ACTION)
239
- fkActions.push(`onDelete: '${rel.onDelete}'`);
243
+ options.push(`onDelete: '${rel.onDelete}'`);
240
244
  if (rel.onUpdate && rel.onUpdate !== DEFAULT_FOREIGN_KEY_ACTION)
241
- fkActions.push(`onUpdate: '${rel.onUpdate}'`);
242
- const fkActionsSource = fkActions.length ? `, ${fkActions.join(', ')}` : '';
243
- lines.push(` @${decoratorName}({ entity: () => ${relatedClassName}${fkActionsSource} })`);
245
+ options.push(`onUpdate: '${rel.onUpdate}'`);
246
+ lines.push(` @${decoratorName}({ ${options.join(', ')} })`);
244
247
  // Property
245
248
  lines.push(` ${propertyName}?: ${relatedClassName};`);
246
249
  return lines.join('\n');
247
250
  }
251
+ /**
252
+ * The `references` callback of a to-one: its foreign key column where that is the target's whole primary
253
+ * key, column pairs otherwise, and nothing when the columns do not pair up.
254
+ */
255
+ referencesSource(rel, relatedClassName) {
256
+ if (!rel.from.columns.length || rel.from.columns.length !== rel.to.columns.length) {
257
+ return undefined;
258
+ }
259
+ const member = (param, column) => memberSource(param, this.options.propertyNameTransformer(column.name));
260
+ const own = lowerFirst(this.options.classNameTransformer(rel.from.table.name));
261
+ const key = rel.to.table.primaryKey;
262
+ if (rel.from.columns.length === 1 && key.length === 1 && rel.to.columns[0].name === key[0].name) {
263
+ return `(${own}) => ${member(own, rel.from.columns[0])}`;
264
+ }
265
+ const target = lowerFirst(relatedClassName);
266
+ const [local, foreign] = own === target ? ['local', 'foreign'] : [own, target];
267
+ const pairs = rel.from.columns.map((column, i) => `{ local: ${member(local, column)}, foreign: ${member(foreign, rel.to.columns[i])} }`);
268
+ return `(${local}, ${foreign}) => [${pairs.join(', ')}]`;
269
+ }
248
270
  /**
249
271
  * Build incoming relation (OneToMany where other tables have FK to this).
250
272
  */
@@ -262,9 +284,9 @@ export class EntityCodeGenerator {
262
284
  lines.push(' */');
263
285
  }
264
286
  // The inverse side, mapped by the related class's property that points back at this one.
265
- const inverseProp = this.options.propertyNameTransformer(this.options.singularize(table.name));
266
287
  const param = lowerFirst(relatedClassName);
267
- lines.push(` @${decoratorName}({ entity: () => ${relatedClassName}, mappedBy: (${param}) => ${param}.${inverseProp} })`);
288
+ const inverse = memberSource(param, this.options.propertyNameTransformer(this.options.singularize(table.name)));
289
+ lines.push(` @${decoratorName}({ entity: () => ${relatedClassName}, mappedBy: (${param}) => ${inverse} })`);
268
290
  // Property
269
291
  if (inverseType === 'OneToMany' || inverseType === 'ManyToMany') {
270
292
  lines.push(` ${propertyName}?: ${relatedClassName}[];`);
@@ -1,5 +1,5 @@
1
1
  import { isVectorIndexType } from '../../type/index.js';
2
- import { isIdentifierName, quoted, rawTag } from './sourceLiteral.js';
2
+ import { memberSource, rawTag } from './sourceLiteral.js';
3
3
  /**
4
4
  * A vector index carries its metric in the operator class pgvector names after it
5
5
  * (`vector_cosine_ops`), which is the only place introspection can recover it from. `@Index` requires
@@ -93,7 +93,3 @@ function indexEntrySource(entry, propertyName, param) {
93
93
  const modifiers = significantModifiers(entry);
94
94
  return modifiers.length === 0 ? column : `{ column: ${column}, ${modifiers.join(', ')} }`;
95
95
  }
96
- /** `user.email`, or `user['first-name']` for a property name that is no identifier. */
97
- function memberSource(param, property) {
98
- return isIdentifierName(property) ? `${param}.${property}` : `${param}[${quoted(property)}]`;
99
- }
@@ -6,6 +6,8 @@ export declare function isIdentifierName(text: string): boolean;
6
6
  * parsing.
7
7
  */
8
8
  export declare function quoted(text: string): string;
9
+ /** `user.email`, or `user['first-name']` for a property name that is no identifier. */
10
+ export declare function memberSource(param: string, property: string): string;
9
11
  /**
10
12
  * SQL as a `raw` tagged template. A database reprints an expression as arbitrary text, and exactly
11
13
  * three sequences can end or interpolate a template literal, so escaping those is the whole job.
@@ -10,6 +10,10 @@ export function isIdentifierName(text) {
10
10
  export function quoted(text) {
11
11
  return `'${text.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
12
12
  }
13
+ /** `user.email`, or `user['first-name']` for a property name that is no identifier. */
14
+ export function memberSource(param, property) {
15
+ return isIdentifierName(property) ? `${param}.${property}` : `${param}[${quoted(property)}]`;
16
+ }
13
17
  /**
14
18
  * SQL as a `raw` tagged template. A database reprints an expression as arbitrary text, and exactly
15
19
  * three sequences can end or interpolate a template literal, so escaping those is the whole job.
@@ -10,7 +10,7 @@ export * from './drift/index.js';
10
10
  export * from './introspection/index.js';
11
11
  export { migrationBuilderFor } from './migrationTarget.js';
12
12
  export { type BuilderMigrationDefinition, defineBuilderMigration, defineMigration, Migrator } from './migrator.js';
13
- export { createSchemaGenerator, SqlSchemaGenerator } from './schemaGenerator.js';
13
+ export { SqlSchemaGenerator } from './schemaGenerator.js';
14
14
  export { DatabaseMigrationStorage } from './storage/databaseStorage.js';
15
15
  export { JsonMigrationStorage } from './storage/jsonStorage.js';
16
16
  export { MongoMigrationStorage } from './storage/mongoStorage.js';
@@ -16,7 +16,7 @@ export * from './introspection/index.js';
16
16
  export { migrationBuilderFor } from './migrationTarget.js';
17
17
  export { defineBuilderMigration, defineMigration, Migrator } from './migrator.js';
18
18
  // Schema generators
19
- export { createSchemaGenerator, SqlSchemaGenerator } from './schemaGenerator.js';
19
+ export { SqlSchemaGenerator } from './schemaGenerator.js';
20
20
  // Storage implementations
21
21
  export { DatabaseMigrationStorage } from './storage/databaseStorage.js';
22
22
  export { JsonMigrationStorage } from './storage/jsonStorage.js';
@@ -1,3 +1,3 @@
1
1
  import type { QuerierPool, SchemaIntrospector } from '../../type/index.js';
2
- /** The introspector for `dialectName`, or `undefined` where the migrator has none for it. */
3
- export declare function introspectorFor(dialectName: string, pool: QuerierPool, schema?: string): SchemaIntrospector | undefined;
2
+ /** The introspector for the engine `pool` runs on, reading `schema` where the engine has schemas to read. */
3
+ export declare function introspectorFor(pool: QuerierPool, schema?: string): SchemaIntrospector;
@@ -4,12 +4,9 @@ import { MariadbSchemaIntrospector, MysqlSchemaIntrospector } from './mysqlIntro
4
4
  import { CockroachSchemaIntrospector, PostgresSchemaIntrospector } from './postgresIntrospector.js';
5
5
  import { SqliteSchemaIntrospector } from './sqliteIntrospector.js';
6
6
  /**
7
- * Which introspector each engine gets. A table rather than a `switch` so `Migrator` names no
8
- * constructor and adding an engine touches one line, not a control flow.
9
- *
10
- * Every entry is statically imported, so `uql-orm/migrate` still carries all of them; making the
11
- * table's values dynamic imports would shrink that entry, at the cost of an async
12
- * `createIntrospector` the constructor cannot await.
7
+ * Which introspector each engine gets; SQLite and MongoDB have no schemas, so they ignore the argument.
8
+ * Statically imported, so `uql-orm/migrate` carries every one: dynamic imports would shrink that entry,
9
+ * but `Migrator` builds its introspector in its constructor, which cannot await.
13
10
  */
14
11
  const INTROSPECTORS = {
15
12
  postgres: (pool, schema) => new PostgresSchemaIntrospector(pool, schema),
@@ -17,12 +14,10 @@ const INTROSPECTORS = {
17
14
  mysql: (pool, schema) => new MysqlSchemaIntrospector(pool, schema),
18
15
  mariadb: (pool, schema) => new MariadbSchemaIntrospector(pool, schema),
19
16
  mssql: (pool, schema) => new MsSqlSchemaIntrospector(pool, schema),
20
- // Neither has schemas to read: SQLite attaches database files and MongoDB takes its database from
21
- // the connection, so both ignore the argument rather than filtering on it.
22
17
  sqlite: (pool) => new SqliteSchemaIntrospector(pool),
23
18
  mongodb: (pool) => new MongoSchemaIntrospector(pool),
24
19
  };
25
- /** The introspector for `dialectName`, or `undefined` where the migrator has none for it. */
26
- export function introspectorFor(dialectName, pool, schema) {
27
- return INTROSPECTORS[dialectName]?.(pool, schema);
20
+ /** The introspector for the engine `pool` runs on, reading `schema` where the engine has schemas to read. */
21
+ export function introspectorFor(pool, schema) {
22
+ return INTROSPECTORS[pool.dialect.dialectName](pool, schema);
28
23
  }
@@ -10,14 +10,14 @@ export type MigrationSession = {
10
10
  /** `work` in one transaction where the engine takes DDL in one; MongoDB creates collections outside any. */
11
11
  transaction(work: () => Promise<void>): Promise<void>;
12
12
  };
13
- /** Everything a migrator does differently per engine family, chosen once from its dialect. */
13
+ /** Everything a migrator does differently per engine family, chosen once from its pool. */
14
14
  export type MigrationTarget = {
15
15
  readonly source: MigrationSource;
16
- storage(pool: QuerierPool, tableName: string | undefined): MigrationStorage;
17
- /** The schema generator, `undefined` for a dialect with none. Async because MongoDB's loads its optional peer. */
18
- generator(dialect: MigratorDialect, defaultForeignKeyAction?: ForeignKeyAction): Promise<SchemaGenerator | undefined>;
19
- withSession<T>(pool: QuerierPool, task: (session: MigrationSession) => Promise<T>): Promise<T>;
16
+ storage(tableName: string | undefined): MigrationStorage;
17
+ /** The dialect's schema generator. Async because MongoDB's loads its optional peer. */
18
+ generator(): Promise<SchemaGenerator>;
19
+ withSession<T>(task: (session: MigrationSession) => Promise<T>): Promise<T>;
20
20
  };
21
- export declare function migrationTargetFor(dialect: MigratorDialect): MigrationTarget;
21
+ export declare function migrationTargetFor(pool: QuerierPool<Querier, MigratorDialect>, defaultForeignKeyAction?: ForeignKeyAction): MigrationTarget;
22
22
  /** A builder running each operation on `querier`, as SQL or as MongoDB driver commands. */
23
23
  export declare function migrationBuilderFor(querier: Querier): Promise<MigrationBuilder>;