uql-orm 0.42.1 → 0.44.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 (54) hide show
  1. package/dist/browser/querier/httpQuerier.d.ts +6 -0
  2. package/dist/browser/querier/httpQuerier.js +1 -1
  3. package/dist/browser/uql-browser.min.js +2 -2
  4. package/dist/browser/uql-browser.min.js.map +5 -5
  5. package/dist/dialect/abstractDialect.js +5 -6
  6. package/dist/dialect/abstractSqlDialect.d.ts +10 -12
  7. package/dist/dialect/abstractSqlDialect.js +34 -34
  8. package/dist/dialect/jsonSql.d.ts +3 -2
  9. package/dist/dialect/jsonSql.js +7 -5
  10. package/dist/dialect/vectorCast.d.ts +0 -6
  11. package/dist/dialect/vectorCast.js +0 -8
  12. package/dist/entity/decorator/bag.d.ts +3 -0
  13. package/dist/entity/decorator/members.d.ts +5 -2
  14. package/dist/entity/index.d.ts +1 -1
  15. package/dist/entity/index.js +1 -1
  16. package/dist/entity/metadata/definition.d.ts +15 -13
  17. package/dist/entity/metadata/definition.js +73 -25
  18. package/dist/http/handler.d.ts +8 -0
  19. package/dist/http/handler.js +5 -5
  20. package/dist/maria/mariaDialect.js +2 -2
  21. package/dist/migrate/cli.d.ts +5 -0
  22. package/dist/migrate/cli.js +33 -16
  23. package/dist/migrate/codegen/entityTypes.d.ts +7 -0
  24. package/dist/migrate/codegen/entityTypes.js +69 -0
  25. package/dist/migrate/codegen/index.d.ts +1 -0
  26. package/dist/migrate/codegen/index.js +1 -0
  27. package/dist/migrate/drift/driftDetector.js +5 -1
  28. package/dist/migrate/generator/mongoSchemaGenerator.d.ts +1 -1
  29. package/dist/migrate/generator/mongoSchemaGenerator.js +1 -1
  30. package/dist/migrate/index.d.ts +1 -1
  31. package/dist/migrate/migrator.d.ts +34 -20
  32. package/dist/migrate/migrator.js +77 -34
  33. package/dist/migrate/schemaGenerator.d.ts +1 -5
  34. package/dist/migrate/schemaGenerator.js +11 -15
  35. package/dist/schema/canonicalType.d.ts +19 -4
  36. package/dist/schema/canonicalType.js +114 -164
  37. package/dist/schema/schemaASTBuilder.d.ts +23 -2
  38. package/dist/schema/schemaASTBuilder.js +2 -2
  39. package/dist/schema/schemaASTDiffer.js +6 -2
  40. package/dist/type/entity.d.ts +36 -6
  41. package/dist/type/migration.d.ts +14 -1
  42. package/dist/type/query.d.ts +2 -6
  43. package/dist/type/queryWhere.d.ts +13 -3
  44. package/dist/util/field.util.d.ts +19 -8
  45. package/dist/util/field.util.js +47 -51
  46. package/dist/util/fieldOption.util.d.ts +79 -0
  47. package/dist/util/fieldOption.util.js +84 -0
  48. package/dist/util/index.d.ts +1 -0
  49. package/dist/util/index.js +1 -0
  50. package/dist/util/object.util.d.ts +3 -3
  51. package/dist/util/object.util.js +3 -3
  52. package/dist/util/sql.util.d.ts +4 -0
  53. package/dist/util/sql.util.js +4 -0
  54. package/package.json +3 -3
@@ -1,4 +1,4 @@
1
- import type { DialectName, LoggingOptions, Migration, MigrationDefinition, MigrationResult, MigrationStorage, MigratorOptions, MongoQuerier, Querier, QuerierPool, SchemaDiff, SchemaGenerator, SchemaIntrospector, Type } from '../type/index.js';
1
+ import type { DialectName, LoggingOptions, Migration, MigrationDefinition, MigrationResult, MigrationStorage, MigratorOptions, MongoQuerier, Querier, QuerierPool, SchemaDiff, SchemaGenerator, SchemaIntrospector, SyncOptions, Type } from '../type/index.js';
2
2
  import { LoggerWrapper } from '../util/index.js';
3
3
  import type { IMigrationBuilder } from './builder/types.js';
4
4
  /**
@@ -88,31 +88,45 @@ export declare class Migrator {
88
88
  private introspectClaimedSchemas;
89
89
  findEntityForTable(tableName: string): Promise<Type<unknown> | undefined>;
90
90
  /**
91
- * Sync schema directly (for development only - not for production!)
92
- */
93
- sync(options?: {
94
- force?: boolean;
95
- }): Promise<void>;
96
- /**
97
- * Drops and recreates all tables (Development only!)
91
+ * Applies the entity schema to the database: every registered entity, or the one `entity` names.
92
+ *
93
+ * The whole surface is this and {@link planSync}, which answers the same question without running
94
+ * it - `force` and a single entity included, so `--dry-run` means the same thing whatever else was
95
+ * asked for.
98
96
  */
99
- syncForce(): Promise<void>;
97
+ sync(options?: SyncOptions): Promise<void>;
100
98
  /**
101
- * Safely synchronizes the schema by only adding missing tables and columns.
99
+ * Every table dropped and recreated.
100
+ *
101
+ * Both directions span the whole entity set rather than looping an entity at a time. A per-entity
102
+ * AST cannot resolve a cross-entity foreign key, so the old create loop silently produced a schema
103
+ * with no referential integrity; and the old drop loop went in reverse *declaration* order, which
104
+ * says nothing about the relation graph and is rejected as soon as the constraints are really there.
102
105
  */
103
- autoSync(options?: {
104
- safe?: boolean;
105
- drop?: boolean;
106
- logging?: boolean;
107
- }): Promise<void>;
106
+ private forceStatements;
108
107
  /**
109
- * The DDL {@link autoSync} would run, without running it. Separate so `--dry-run` shows the real
108
+ * Sync one entity, for a schema that grows while the process runs: a content type an admin just
109
+ * created is one table to add, where the whole set would read the catalogue to work that out.
110
+ *
111
+ * The new-table case costs one existence check and creates with `IF NOT EXISTS`, so instances racing
112
+ * the same admin save settle instead of colliding. An existing table still pays for introspection,
113
+ * since a column diff needs the columns.
114
+ */
115
+ /** The DDL for one entity: {@link planSync} narrowed to the table it names. */
116
+ private planEntity;
117
+ /** An alter diff as statements, narrowed to what the caller allows. */
118
+ private alterFromDiff;
119
+ /** The same for one entity against the table it already has, and nothing where the two agree. */
120
+ private alterFromEntity;
121
+ /** The configured entities, with `entity` among them however the migrator was built. */
122
+ private entitiesWith;
123
+ /** The introspector for a claimed schema, which is the connection's own where none is claimed. */
124
+ private introspectorFor;
125
+ /**
126
+ * The DDL {@link sync} would run, without running it. Separate so `--dry-run` shows the real
110
127
  * statements rather than a summary of a second, differently-computed diff.
111
128
  */
112
- planSync(options?: {
113
- safe?: boolean;
114
- drop?: boolean;
115
- }): Promise<string[]>;
129
+ planSync(options?: SyncOptions): Promise<string[]>;
116
130
  /**
117
131
  * New tables are emitted together, never one at a time: a single-entity AST has no other table for a
118
132
  * relation to resolve against, so every cross-entity foreign key was dropped and generated schemas
@@ -305,7 +305,7 @@ export class Migrator {
305
305
  const claimed = new Set(this.entities.map((entity) => this.pool.dialect.resolveSchema(getMeta(entity))));
306
306
  const merged = new SchemaAST();
307
307
  for (const schema of claimed) {
308
- const introspector = schema === undefined ? this.schemaIntrospector : this.createIntrospector(schema);
308
+ const introspector = this.introspectorFor(schema);
309
309
  if (!introspector) {
310
310
  continue;
311
311
  }
@@ -330,52 +330,90 @@ export class Migrator {
330
330
  return undefined;
331
331
  }
332
332
  /**
333
- * Sync schema directly (for development only - not for production!)
333
+ * Applies the entity schema to the database: every registered entity, or the one `entity` names.
334
+ *
335
+ * The whole surface is this and {@link planSync}, which answers the same question without running
336
+ * it - `force` and a single entity included, so `--dry-run` means the same thing whatever else was
337
+ * asked for.
334
338
  */
335
339
  async sync(options = {}) {
336
- if (options.force) {
337
- return this.syncForce();
340
+ const statements = await this.planSync(options);
341
+ if (statements.length) {
342
+ await this.executeSyncStatements(statements, options);
343
+ }
344
+ else if (options.logging) {
345
+ this.logger.logSchema('Schema is already in sync.');
338
346
  }
339
- return this.autoSync({ safe: true });
340
347
  }
341
348
  /**
342
- * Drops and recreates all tables (Development only!)
349
+ * Every table dropped and recreated.
350
+ *
351
+ * Both directions span the whole entity set rather than looping an entity at a time. A per-entity
352
+ * AST cannot resolve a cross-entity foreign key, so the old create loop silently produced a schema
353
+ * with no referential integrity; and the old drop loop went in reverse *declaration* order, which
354
+ * says nothing about the relation graph and is rejected as soon as the constraints are really there.
343
355
  */
344
- async syncForce() {
345
- await this.ensureSchemaGenerator();
346
- // Both directions span the whole entity set rather than looping an entity at a time. A per-entity
347
- // AST cannot resolve a cross-entity foreign key, so the old create loop silently produced a schema
348
- // with no referential integrity; and the old drop loop went in reverse *declaration* order, which
349
- // says nothing about the relation graph and is rejected as soon as the constraints are really there.
350
- const statements = [
356
+ forceStatements() {
357
+ return [
351
358
  ...this.generator.generateDropSchema(this.entities, { ifExists: true, cascade: true }),
352
359
  ...this.generator.generateCreateSchema(this.entities),
353
360
  ];
354
- await withSqlQuerierForMigrations(this.pool, 'Migrator', (querier) => querier.transaction(async () => {
355
- for (const sql of statements) {
356
- this.logger.logSchema(`Executing: ${sql}`);
357
- await querier.run(sql);
358
- }
359
- }));
360
- this.logger.logSchema('Schema sync (force) completed');
361
361
  }
362
362
  /**
363
- * Safely synchronizes the schema by only adding missing tables and columns.
363
+ * Sync one entity, for a schema that grows while the process runs: a content type an admin just
364
+ * created is one table to add, where the whole set would read the catalogue to work that out.
365
+ *
366
+ * The new-table case costs one existence check and creates with `IF NOT EXISTS`, so instances racing
367
+ * the same admin save settle instead of colliding. An existing table still pays for introspection,
368
+ * since a column diff needs the columns.
364
369
  */
365
- async autoSync(options = {}) {
366
- const statements = await this.planSync(options);
367
- if (statements.length === 0) {
368
- if (options.logging)
369
- this.logger.logSchema('Schema is already in sync.');
370
- return;
370
+ /** The DDL for one entity: {@link planSync} narrowed to the table it names. */
371
+ async planEntity(entity, options) {
372
+ const meta = getMeta(entity);
373
+ // Before anything reads `this.generator`, whose own failure names neither the entity nor the
374
+ // dialect that has no support.
375
+ const introspector = this.introspectorFor(this.pool.dialect.resolveSchema(meta));
376
+ if (!introspector) {
377
+ throw new TypeError(`No introspector for '${meta.entity.name}' on '${this.dialectName}'`);
371
378
  }
372
- await this.executeSyncStatements(statements, options);
379
+ const tableName = this.generator.resolveTableName(meta);
380
+ return (await introspector.tableExists(tableName))
381
+ ? this.alterFromEntity(entity, (await introspectSchema(introspector)).getTable(tableName), options)
382
+ : // Spanning the whole set, so a foreign key resolves against the tables it points at, and
383
+ // always including this entity: pinned to an explicit `entities` list, a sync of one outside
384
+ // it emitted nothing at all. `only` is what keeps the statements to this table.
385
+ this.generator.generateCreateSchema(this.entitiesWith(entity), { only: [tableName], ifNotExists: true });
386
+ }
387
+ /** An alter diff as statements, narrowed to what the caller allows. */
388
+ alterFromDiff(diff, options) {
389
+ return this.generator.generateAlterTable(this.filterDiff(diff, options));
390
+ }
391
+ /** The same for one entity against the table it already has, and nothing where the two agree. */
392
+ alterFromEntity(entity, table, options) {
393
+ const diff = this.generator.diffSchema(entity, table);
394
+ return diff?.type === 'alter' ? this.alterFromDiff(diff, options) : [];
395
+ }
396
+ /** The configured entities, with `entity` among them however the migrator was built. */
397
+ entitiesWith(entity) {
398
+ const entities = this.entities;
399
+ return entities.includes(entity) ? entities : [...entities, entity];
400
+ }
401
+ /** The introspector for a claimed schema, which is the connection's own where none is claimed. */
402
+ introspectorFor(schema) {
403
+ return schema === undefined ? this.schemaIntrospector : this.createIntrospector(schema);
373
404
  }
374
405
  /**
375
- * The DDL {@link autoSync} would run, without running it. Separate so `--dry-run` shows the real
406
+ * The DDL {@link sync} would run, without running it. Separate so `--dry-run` shows the real
376
407
  * statements rather than a summary of a second, differently-computed diff.
377
408
  */
378
409
  async planSync(options = {}) {
410
+ await this.ensureSchemaGenerator();
411
+ if (options.force) {
412
+ return this.forceStatements();
413
+ }
414
+ if (options.entity) {
415
+ return this.planEntity(options.entity, options);
416
+ }
379
417
  const creating = [];
380
418
  const altering = [];
381
419
  for (const { diff, entity } of await this.pendingDiffs()) {
@@ -384,7 +422,7 @@ export class Migrator {
384
422
  creating.push(diff.tableName);
385
423
  }
386
424
  else if (diff.type === 'alter') {
387
- altering.push(...this.generator.generateAlterTable(this.filterDiff(diff, options)));
425
+ altering.push(...this.alterFromDiff(diff, options));
388
426
  }
389
427
  }
390
428
  return [...this.createSchema(creating), ...altering];
@@ -452,10 +490,15 @@ export class Migrator {
452
490
  return filteredDiff;
453
491
  }
454
492
  async executeSyncStatements(statements, options) {
455
- // Mongo creates collections and indexes outside any transaction, so only the SQL path opens one.
456
- await withQuerierForMigrations(this.pool, (querier) => this.dialectName === 'mongodb'
457
- ? this.executeMongoSyncStatements(statements, options, querier)
458
- : querier.transaction(() => this.executeSqlSyncStatements(statements, options, querier)));
493
+ // Mongo creates collections and indexes outside any transaction, so only the SQL path opens one -
494
+ // and asks for a SQL querier before it opens it, since `transaction` is what a Mongo one lacks and
495
+ // reaching for it first reports that instead of which querier the dialect needs.
496
+ if (this.dialectName === 'mongodb') {
497
+ await withQuerierForMigrations(this.pool, (querier) => this.executeMongoSyncStatements(statements, options, querier));
498
+ }
499
+ else {
500
+ await withSqlQuerierForMigrations(this.pool, 'Migrator', (querier) => querier.transaction(() => this.executeSqlSyncStatements(statements, options, querier)));
501
+ }
459
502
  if (options.logging)
460
503
  this.logger.logSchema('Schema synchronization completed');
461
504
  }
@@ -27,10 +27,6 @@ export declare class SqlSchemaGenerator implements SqlDdlGenerator {
27
27
  * Primary key type for auto-increment integer IDs
28
28
  */
29
29
  protected get serialType(): string;
30
- /**
31
- * Convert FieldOptions to CanonicalType using the unified type system.
32
- */
33
- protected getCanonicalType(field: FieldOptions, fieldType?: unknown): CanonicalType;
34
30
  protected canonicalTypeToSql(type: CanonicalType): string;
35
31
  /**
36
32
  * Every `CREATE TABLE` for `entities`, then their foreign keys.
@@ -85,7 +81,7 @@ export declare class SqlSchemaGenerator implements SqlDdlGenerator {
85
81
  /** ` DEFAULT <sql>`, or nothing where the column declares none. Empty rather than `DEFAULT NULL`
86
82
  * so an absent default stays absent - `defaultValue: null` is the way to ask for one. */
87
83
  private defaultClause;
88
- getSqlType(field: FieldMeta, fieldType?: unknown, isSoleKey?: boolean): string;
84
+ getSqlType(field: FieldMeta): string;
89
85
  /**
90
86
  * Generate ALTER COLUMN statements (database-specific)
91
87
  */
@@ -1,6 +1,6 @@
1
1
  import { AbstractSqlDialect } from '../dialect/index.js';
2
2
  import { getMeta, soleIdOf } from '../entity/index.js';
3
- import { areTypesEqual, canonicalToSql, fieldOptionsToCanonical, isVectorCategory, sqlToCanonical, } from '../schema/canonicalType.js';
3
+ import { areTypesEqual, canonicalToSql, engineType, fieldOptionsToCanonical, isVectorCategory, } from '../schema/canonicalType.js';
4
4
  import { indexSignature } from '../schema/indexDifferences.js';
5
5
  import { buildSchemaAST } from '../schema/schemaASTBuilder.js';
6
6
  import { diffTable } from '../schema/schemaASTDiffer.js';
@@ -52,12 +52,6 @@ export class SqlSchemaGenerator {
52
52
  get serialType() {
53
53
  return this.dialect.serialType;
54
54
  }
55
- /**
56
- * Convert FieldOptions to CanonicalType using the unified type system.
57
- */
58
- getCanonicalType(field, fieldType) {
59
- return fieldOptionsToCanonical(field, fieldType);
60
- }
61
55
  canonicalTypeToSql(type) {
62
56
  return canonicalToSql(type, this.dialect);
63
57
  }
@@ -275,18 +269,20 @@ export class SqlSchemaGenerator {
275
269
  ? ''
276
270
  : ` DEFAULT ${formatDefaultValue(column.defaultValue, this.dialect, column.type)}`;
277
271
  }
278
- getSqlType(field, fieldType, isSoleKey = field.isId === true) {
279
- // If field has a reference, inherit type from the target primary key
272
+ getSqlType(field) {
273
+ // A foreign key takes the type of the key it points at. A `referencedKey` the target does not
274
+ // have falls through to this column's own options, as the AST builder does with the same case.
280
275
  if (field.references) {
281
- const refEntity = field.references();
282
- const refMeta = getMeta(refEntity);
276
+ const refMeta = getMeta(field.references());
283
277
  const refIdField = refMeta.fields[field.referencedKey ?? soleIdOf(refMeta, 'a foreign key')];
284
- return this.getSqlType({ ...refIdField, references: undefined, isId: undefined, autoIncrement: false }, refIdField.type);
278
+ if (refIdField) {
279
+ return this.getSqlType({ ...refIdField, references: undefined, isId: undefined, autoIncrement: false });
280
+ }
285
281
  }
286
282
  // Get canonical type and convert to SQL
287
- const canonical = this.getCanonicalType(field, fieldType);
283
+ const canonical = fieldOptionsToCanonical(field);
288
284
  // Special case for serial primary keys
289
- if (isAutoIncrement(field, isSoleKey)) {
285
+ if (isAutoIncrement(field, field.isId === true)) {
290
286
  return this.dialect.serialType;
291
287
  }
292
288
  return this.canonicalTypeToSql(canonical);
@@ -408,7 +404,7 @@ export class SqlSchemaGenerator {
408
404
  }
409
405
  diffOptions() {
410
406
  return {
411
- normalizeType: (type) => sqlToCanonical(this.canonicalTypeToSql(type)),
407
+ normalizeType: engineType(this.dialect),
412
408
  defaultsEqual: (expected, actual) => this.isDefaultValueEqual(actual, expected),
413
409
  };
414
410
  }
@@ -36,17 +36,32 @@ export declare function canonicalToSql(type: CanonicalType, dialect: AbstractDia
36
36
  * Convert a canonical type to a TypeScript type string.
37
37
  */
38
38
  export declare function canonicalToTypeScript(type: CanonicalType): string;
39
+ /**
40
+ * A type as `dialect` would actually store it: rendered to that engine's SQL and read back.
41
+ *
42
+ * Several canonical types share one storage type per engine - a `boolean` is `TINYINT(1)` on MySQL and
43
+ * `INTEGER` on SQLite - and only the engine settles an unstated bound, since `VARCHAR` is 255 on MySQL
44
+ * and `TEXT` on Postgres. Both paths that diff a schema compare through this, so a migration and a
45
+ * drift report cannot disagree about what changed.
46
+ */
47
+ export declare function engineType(dialect: AbstractDialect): (type: CanonicalType) => CanonicalType;
39
48
  /**
40
49
  * Convert UQL FieldOptions to a canonical type.
41
50
  */
42
- export declare function fieldOptionsToCanonical(options: FieldOptions, tsType?: unknown): CanonicalType;
51
+ export declare function fieldOptionsToCanonical(options: FieldOptions): CanonicalType;
43
52
  /**
44
- * Compare two canonical types for equality.
45
- * Used for schema diffing.
53
+ * Compare two canonical types for equality. Used for schema diffing.
54
+ *
55
+ * Nothing here guesses an engine's own default for an unstated bound: `VARCHAR` is 255 on MySQL and
56
+ * `TEXT` on Postgres, and a comparison that assumed either was blind to that difference on the other.
57
+ * `DiffOptions.normalizeType` is what settles it, by putting both sides through the engine first.
46
58
  */
47
59
  export declare function areTypesEqual(a: CanonicalType, b: CanonicalType): boolean;
48
60
  /**
49
- * Check if changing from type A to type B could cause data loss.
61
+ * Whether changing a column from one type to the other can lose what it holds.
62
+ *
63
+ * An unstated bound is the engine's widest, so stating one for the first time narrows the column just
64
+ * as lowering one does: `TEXT` to `VARCHAR(50)` truncates, and `NUMERIC` to `NUMERIC(5,2)` rounds.
50
65
  */
51
66
  export declare function isBreakingTypeChange(from: CanonicalType, to: CanonicalType): boolean;
52
67
  /**