uql-orm 0.24.7 → 0.25.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.
package/README.md CHANGED
@@ -51,7 +51,7 @@ from the browser to the server. The same object runs on every supported database
51
51
  ## Why UQL?
52
52
 
53
53
  - **The fastest.** Wins [all 8 categories](https://uql-orm.dev/benchmark) of our open-source [benchmark](https://github.com/rogerpadilla/ts-orm-benchmark), beating even query builders like Knex and Kysely: ~2.4× faster than the runner-up on average, reaching over 4.6M ops/s on simple SELECTs.
54
- - **Light.** Zero runtime dependencies, 288 kB on the wire, every dialect included. See [what we deleted to get there](https://uql-orm.dev/blog/zero-dependencies).
54
+ - **Light.** Zero runtime dependencies, 305 kB on the wire, every dialect included. See [what we deleted to get there](https://uql-orm.dev/blog/zero-dependencies).
55
55
  - **Queries are data (JSON), not method chains.** Plain JSON in, typed rows out. There's no DSL to learn and nothing to compile.
56
56
  - **Type-safe to the leaf.** Every key is autocompleted and checked against your entity, down to the fields of a populated relation. Operators are gated per field type, and [JSON/JSONB](https://uql-orm.dev/querying/json) dot-paths resolve each path's value type, so `$like` on a numeric column, or a typo'd path, is a compile error instead of a runtime surprise.
57
57
  - **No codegen, no build step.** Entities are TypeScript classes, so your code *is* the schema. There's no `.prisma` file to regenerate and no generated client to keep in sync.
@@ -534,7 +534,11 @@ export class AbstractSqlDialect extends IndexSqlDialect {
534
534
  }
535
535
  }
536
536
  appendInNin(ctx, field, op, val) {
537
- this.appendFieldSql(ctx, field, this.formatIn(ctx, Array.isArray(val) ? val : [], op === '$nin'));
537
+ if (!Array.isArray(val)) {
538
+ // Not covered by the types: `/http` casts client JSON straight to `Query`, so this arrives untyped.
539
+ throw TypeError(`${op} expects an array, got ${val === null ? 'null' : typeof val}`);
540
+ }
541
+ this.appendFieldSql(ctx, field, this.formatIn(ctx, val, op === '$nin'));
538
542
  }
539
543
  /**
540
544
  * Build a comparison condition for a JSON field.
@@ -1,6 +1,6 @@
1
1
  import { AbstractDialect } from '../../dialect/abstractDialect.js';
2
2
  import type { ForeignKeyAction, IndexNode, TableNode } from '../../schema/types.js';
3
- import type { FieldOptions, IndexSchema, InsertIdSource, NamingStrategy, SchemaDiff, SchemaGenerator, Type } from '../../type/index.js';
3
+ import type { CreateSchemaOptions, FieldOptions, IndexSchema, InsertIdSource, NamingStrategy, SchemaDiff, SchemaGenerator, Type } from '../../type/index.js';
4
4
  import type { TableDefinition } from '../builder/types.js';
5
5
  export declare class MongoSchemaGenerator extends AbstractDialect implements SchemaGenerator {
6
6
  protected readonly defaultForeignKeyAction?: ForeignKeyAction | undefined;
@@ -8,6 +8,14 @@ export declare class MongoSchemaGenerator extends AbstractDialect implements Sch
8
8
  protected readonly featureDefaults: import("../../type/dialect.js").DialectFeatures;
9
9
  readonly insertIdSource: InsertIdSource;
10
10
  constructor(namingStrategy?: NamingStrategy, defaultForeignKeyAction?: ForeignKeyAction | undefined);
11
+ /**
12
+ * A document store has no cross-collection constraint, so unlike the SQL generator there is nothing to
13
+ * defer and no order to respect: this is each collection and nothing more. `foreignKeys` is accepted
14
+ * and ignored for the same reason.
15
+ */
16
+ generateCreateSchema(entities: readonly Type<unknown>[], options?: CreateSchemaOptions): string[];
17
+ generateDropSchema(entities: readonly Type<unknown>[]): string[];
18
+ private selected;
11
19
  generateCreateTable<E>(entity: Type<E>, _options?: {
12
20
  ifNotExists?: boolean;
13
21
  }): string[];
@@ -13,6 +13,24 @@ export class MongoSchemaGenerator extends AbstractDialect {
13
13
  super({ namingStrategy });
14
14
  this.defaultForeignKeyAction = defaultForeignKeyAction;
15
15
  }
16
+ /**
17
+ * A document store has no cross-collection constraint, so unlike the SQL generator there is nothing to
18
+ * defer and no order to respect: this is each collection and nothing more. `foreignKeys` is accepted
19
+ * and ignored for the same reason.
20
+ */
21
+ generateCreateSchema(entities, options) {
22
+ return this.selected(entities, options?.only).flatMap((entity) => this.generateCreateTable(entity, options));
23
+ }
24
+ generateDropSchema(entities) {
25
+ return this.selected(entities).map((entity) => this.generateDropTable(this.resolveTableName(entity, getMeta(entity))));
26
+ }
27
+ selected(entities, only) {
28
+ if (!only) {
29
+ return entities;
30
+ }
31
+ const wanted = new Set(only);
32
+ return entities.filter((entity) => wanted.has(this.resolveTableName(entity, getMeta(entity))));
33
+ }
16
34
  generateCreateTable(entity, _options) {
17
35
  const meta = getMeta(entity);
18
36
  const collectionName = this.resolveTableName(entity, meta);
@@ -104,6 +104,15 @@ export declare class Migrator {
104
104
  safe?: boolean;
105
105
  drop?: boolean;
106
106
  }): Promise<string[]>;
107
+ /**
108
+ * New tables are emitted together, never one at a time: a single-entity AST has no other table for a
109
+ * relation to resolve against, so every cross-entity foreign key was dropped and generated schemas
110
+ * carried none. Spanning the graph is also what lets a cyclic relation (any `createdBy`
111
+ * back-reference) be created at all.
112
+ *
113
+ * Empty in, empty out, so a diff with no new tables does not build an AST for the whole graph.
114
+ */
115
+ private createSchema;
107
116
  /**
108
117
  * Each pending diff with the entity it came from, since resolving that is async and every caller
109
118
  * needs it. What to emit stays with the caller: a sync narrows the forward direction to what the
@@ -234,20 +234,22 @@ export class Migrator {
234
234
  * Generate a migration based on entity schema differences
235
235
  */
236
236
  async generateFromEntities(name) {
237
- const upStatements = [];
237
+ const creating = [];
238
+ const altering = [];
238
239
  const downStatements = [];
239
240
  for (const { diff, entity } of await this.pendingDiffs()) {
240
241
  if (diff.type === 'create') {
241
242
  if (entity) {
242
- upStatements.push(...this.generator.generateCreateTable(entity));
243
+ creating.push(diff.tableName);
243
244
  downStatements.push(this.generator.generateDropTable(diff.tableName, { ifExists: true }));
244
245
  }
245
246
  }
246
247
  else if (diff.type === 'alter') {
247
- upStatements.push(...this.generator.generateAlterTable(diff));
248
+ altering.push(...this.generator.generateAlterTable(diff));
248
249
  downStatements.push(...this.generator.generateAlterTableDown(diff));
249
250
  }
250
251
  }
252
+ const upStatements = [...this.createSchema(creating), ...altering];
251
253
  if (upStatements.length === 0) {
252
254
  this.logger.logInfo('No schema changes detected.');
253
255
  return '';
@@ -317,21 +319,18 @@ export class Migrator {
317
319
  */
318
320
  async syncForce() {
319
321
  await this.ensureSchemaGenerator();
322
+ // Both directions span the whole entity set rather than looping an entity at a time. A per-entity
323
+ // AST cannot resolve a cross-entity foreign key, so the old create loop silently produced a schema
324
+ // with no referential integrity; and the old drop loop went in reverse *declaration* order, which
325
+ // says nothing about the relation graph and is rejected as soon as the constraints are really there.
326
+ const statements = [
327
+ ...this.generator.generateDropSchema(this.entities, { ifExists: true, cascade: true }),
328
+ ...this.generator.generateCreateSchema(this.entities),
329
+ ];
320
330
  await withSqlQuerierForMigrations(this.pool, 'Migrator', (querier) => querier.transaction(async () => {
321
- // Drop all tables first (in reverse order for foreign keys)
322
- for (const entity of [...this.entities].reverse()) {
323
- const tableName = this.generator.resolveTableName(entity, getMeta(entity));
324
- const dropSql = this.generator.generateDropTable(tableName, { ifExists: true });
325
- this.logger.logSchema(`Executing: ${dropSql}`);
326
- await querier.run(dropSql);
327
- }
328
- // Create all tables
329
- for (const entity of this.entities) {
330
- const createStmts = this.generator.generateCreateTable(entity);
331
- for (const createSql of createStmts) {
332
- this.logger.logSchema(`Executing: ${createSql}`);
333
- await querier.run(createSql);
334
- }
331
+ for (const sql of statements) {
332
+ this.logger.logSchema(`Executing: ${sql}`);
333
+ await querier.run(sql);
335
334
  }
336
335
  }));
337
336
  this.logger.logSchema('Schema sync (force) completed');
@@ -353,17 +352,29 @@ export class Migrator {
353
352
  * statements rather than a summary of a second, differently-computed diff.
354
353
  */
355
354
  async planSync(options = {}) {
356
- const statements = [];
355
+ const creating = [];
356
+ const altering = [];
357
357
  for (const { diff, entity } of await this.pendingDiffs()) {
358
358
  if (diff.type === 'create') {
359
359
  if (entity)
360
- statements.push(...this.generator.generateCreateTable(entity));
360
+ creating.push(diff.tableName);
361
361
  }
362
362
  else if (diff.type === 'alter') {
363
- statements.push(...this.generator.generateAlterTable(this.filterDiff(diff, options)));
363
+ altering.push(...this.generator.generateAlterTable(this.filterDiff(diff, options)));
364
364
  }
365
365
  }
366
- return statements;
366
+ return [...this.createSchema(creating), ...altering];
367
+ }
368
+ /**
369
+ * New tables are emitted together, never one at a time: a single-entity AST has no other table for a
370
+ * relation to resolve against, so every cross-entity foreign key was dropped and generated schemas
371
+ * carried none. Spanning the graph is also what lets a cyclic relation (any `createdBy`
372
+ * back-reference) be created at all.
373
+ *
374
+ * Empty in, empty out, so a diff with no new tables does not build an AST for the whole graph.
375
+ */
376
+ createSchema(tableNames) {
377
+ return tableNames.length ? this.generator.generateCreateSchema(this.entities, { only: tableNames }) : [];
367
378
  }
368
379
  /**
369
380
  * Each pending diff with the entity it came from, since resolving that is async and every caller
@@ -1,6 +1,6 @@
1
1
  import { type AbstractDialect, AbstractSqlDialect } from '../dialect/index.js';
2
2
  import type { CanonicalType, ColumnNode, ForeignKeyAction, IndexNode, TableNode } from '../schema/types.js';
3
- import type { ColumnSchema, DialectFeatures, EntityMeta, FieldOptions, IndexSchema, NamingStrategy, SchemaDiff, SqlDdlGenerator, Type } from '../type/index.js';
3
+ import type { ColumnSchema, CreateSchemaOptions, DialectFeatures, DropSchemaOptions, EntityMeta, FieldOptions, IndexSchema, NamingStrategy, SchemaDiff, SqlDdlGenerator, Type } from '../type/index.js';
4
4
  import type { FullColumnDefinition, TableDefinition, TableForeignKeyDefinition } from './builder/types.js';
5
5
  /**
6
6
  * Unified SQL schema generator.
@@ -27,13 +27,24 @@ export declare class SqlSchemaGenerator implements SqlDdlGenerator {
27
27
  */
28
28
  protected getCanonicalType(field: FieldOptions, fieldType?: unknown): CanonicalType;
29
29
  protected canonicalTypeToSql(type: CanonicalType): string;
30
- generateCreateTable<E>(entity: Type<E>, options?: {
31
- ifNotExists?: boolean;
32
- }): string[];
33
- generateDropTable(tableName: string, options?: {
34
- ifExists?: boolean;
35
- cascade?: boolean;
36
- }): string;
30
+ /**
31
+ * Every `CREATE TABLE` for `entities`, then their foreign keys.
32
+ *
33
+ * Two phases rather than inline constraints, because a relation graph is routinely cyclic: any
34
+ * `createdBy`-style back-reference makes `A` reference `B` while `B` references `A`, and no create
35
+ * order satisfies that. TypeORM's schema builder splits for the same reason (`createNewTables()`
36
+ * then `createForeignKeys()`). SQLite is the exception and keeps them inline: it cannot `ALTER` a
37
+ * foreign key in, but it resolves targets lazily, so a forward reference is fine there.
38
+ */
39
+ generateCreateSchema(entities: readonly Type<unknown>[], options?: CreateSchemaOptions): string[];
40
+ generateDropSchema(entities: readonly Type<unknown>[], options?: DropSchemaOptions): string[];
41
+ /**
42
+ * The tables of `entities` in dependency order, optionally narrowed to `only`. The AST always spans
43
+ * every entity even when narrowed, so a relation pointing at a table outside the subset still
44
+ * resolves instead of being silently dropped.
45
+ */
46
+ private orderedTables;
47
+ generateDropTable(tableName: string, options?: DropSchemaOptions): string;
37
48
  generateAlterTable(diff: SchemaDiff): string[];
38
49
  generateAlterTableDown(diff: SchemaDiff): string[];
39
50
  generateCreateIndex(tableName: string, index: IndexSchema, options?: {
@@ -49,11 +49,54 @@ export class SqlSchemaGenerator {
49
49
  canonicalTypeToSql(type) {
50
50
  return canonicalToSql(type, this.dialect);
51
51
  }
52
- generateCreateTable(entity, options = {}) {
53
- const builder = new SchemaASTBuilder(this.dialect.namingStrategy);
54
- const ast = builder.fromEntities([entity]);
55
- const tableNode = ast.getTables()[0];
56
- return this.generateCreateTableFromNode(tableNode, options);
52
+ /**
53
+ * Every `CREATE TABLE` for `entities`, then their foreign keys.
54
+ *
55
+ * Two phases rather than inline constraints, because a relation graph is routinely cyclic: any
56
+ * `createdBy`-style back-reference makes `A` reference `B` while `B` references `A`, and no create
57
+ * order satisfies that. TypeORM's schema builder splits for the same reason (`createNewTables()`
58
+ * then `createForeignKeys()`). SQLite is the exception and keeps them inline: it cannot `ALTER` a
59
+ * foreign key in, but it resolves targets lazily, so a forward reference is fine there.
60
+ */
61
+ generateCreateSchema(entities, options = {}) {
62
+ const tables = this.orderedTables(entities, 'create', options.only);
63
+ const withForeignKeys = options.foreignKeys ?? true;
64
+ // Inline only where a constraint cannot be added afterwards, which is what makes the cyclic case
65
+ // work everywhere else.
66
+ const inline = withForeignKeys && !this.features.foreignKeyAlter;
67
+ const statements = tables.flatMap((table) => this.generateCreateTableFromNode(inline ? table : { ...table, outgoingRelations: [] }, options));
68
+ if (withForeignKeys && !inline) {
69
+ for (const table of tables) {
70
+ for (const rel of table.outgoingRelations) {
71
+ statements.push(this.generateAddForeignKeySql(table.name, {
72
+ name: rel.name,
73
+ columns: rel.from.columns.map((c) => c.name),
74
+ referencesTable: rel.to.table.name,
75
+ referencesColumns: rel.to.columns.map((c) => c.name),
76
+ onDelete: rel.onDelete ?? this.defaultForeignKeyAction,
77
+ onUpdate: rel.onUpdate ?? this.defaultForeignKeyAction,
78
+ }));
79
+ }
80
+ }
81
+ }
82
+ return statements;
83
+ }
84
+ generateDropSchema(entities, options = {}) {
85
+ return this.orderedTables(entities, 'drop').map((table) => this.generateDropTable(table.name, options));
86
+ }
87
+ /**
88
+ * The tables of `entities` in dependency order, optionally narrowed to `only`. The AST always spans
89
+ * every entity even when narrowed, so a relation pointing at a table outside the subset still
90
+ * resolves instead of being silently dropped.
91
+ */
92
+ orderedTables(entities, direction, only) {
93
+ const ast = new SchemaASTBuilder(this.dialect.namingStrategy).fromEntities(entities);
94
+ const tables = direction === 'create' ? ast.getCreateOrder() : ast.getDropOrder();
95
+ if (!only) {
96
+ return tables;
97
+ }
98
+ const wanted = new Set(only);
99
+ return tables.filter((table) => wanted.has(table.name));
57
100
  }
58
101
  generateDropTable(tableName, options = {}) {
59
102
  const ifExists = options.ifExists ? 'IF EXISTS ' : '';
@@ -231,10 +231,12 @@ export class AbstractSqlQuerier extends AbstractQuerier {
231
231
  return 0;
232
232
  }
233
233
  const ids = founds.map((it) => it[meta.id]);
234
+ // Children first: they hold the foreign key, so deleting the parent ahead of them is rejected
235
+ // outright by any schema that declares the constraint without `ON DELETE CASCADE`.
236
+ await this.deleteRelations(entity, ids, opts);
234
237
  const deleteCtx = this.dialect.createContext();
235
238
  this.dialect.delete(deleteCtx, entity, { $where: ids }, opts);
236
239
  const { changes = 0 } = await this.run(deleteCtx.sql, deleteCtx.values);
237
- await this.deleteRelations(entity, ids, opts);
238
240
  return changes;
239
241
  }
240
242
  get hasOpenTransaction() {
@@ -41,7 +41,7 @@ export declare class SchemaASTBuilder {
41
41
  /**
42
42
  * Build AST from entity classes (decorated with @Entity, @Field, etc.)
43
43
  */
44
- fromEntities(entities: Type<unknown>[], options?: BuildFromEntitiesOptions): SchemaAST;
44
+ fromEntities(entities: readonly Type<unknown>[], options?: BuildFromEntitiesOptions): SchemaAST;
45
45
  /**
46
46
  * Resolve the canonical type for a field, inheriting from the referenced
47
47
  * entity's primary key when the field is a foreign-key reference
@@ -181,8 +181,10 @@ export class SchemaASTBuilder {
181
181
  type: relation.cardinality === 'm1' ? 'ManyToOne' : 'OneToOne',
182
182
  from: { table, columns: [localColumn] },
183
183
  to: { table: relatedTable, columns: [foreignColumn] },
184
- onDelete: options.defaultForeignKeyAction ?? this.defaultForeignKeyAction,
185
- onUpdate: options.defaultForeignKeyAction ?? this.defaultForeignKeyAction,
184
+ // The relation's own action wins over the global default, so one relation can cascade in the
185
+ // database while the rest stay on `NO ACTION`.
186
+ onDelete: relation.onDelete ?? options.defaultForeignKeyAction ?? this.defaultForeignKeyAction,
187
+ onUpdate: relation.onUpdate ?? options.defaultForeignKeyAction ?? this.defaultForeignKeyAction,
186
188
  confidence: 1.0,
187
189
  inferredFrom: 'entity_decorator',
188
190
  };
@@ -24,6 +24,7 @@ export class NodeSqliteQuerierPool extends AbstractLocalSqliteQuerierPool {
24
24
  ...(extensions?.length ? { allowExtension: true } : undefined),
25
25
  });
26
26
  nodeDb.exec('PRAGMA journal_mode = WAL');
27
+ nodeDb.exec('PRAGMA foreign_keys = ON');
27
28
  return adaptNodeSqlite(nodeDb);
28
29
  }
29
30
  }
@@ -11,5 +11,10 @@ export type Sqlite3PoolOptions = Options & LocalSqlitePoolOptions;
11
11
  export declare class Sqlite3QuerierPool extends AbstractLocalSqliteQuerierPool<Sqlite3PoolOptions> {
12
12
  readonly filename: string | Buffer;
13
13
  constructor(filename?: string | Buffer, opts?: Sqlite3PoolOptions, extra?: ExtraOptions);
14
+ /**
15
+ * SQLite ships with foreign keys unenforced, per connection, for backward compatibility. UQL emits the
16
+ * constraints in its DDL, so leaving them off means a declared `onDelete: 'CASCADE'` silently does
17
+ * nothing and a dangling reference is accepted. Enabled here on every driver, as TypeORM also does.
18
+ */
14
19
  protected createDb(): Promise<SqliteDatabase>;
15
20
  }
@@ -9,6 +9,11 @@ export class Sqlite3QuerierPool extends AbstractLocalSqliteQuerierPool {
9
9
  super(opts, extra);
10
10
  this.filename = filename;
11
11
  }
12
+ /**
13
+ * SQLite ships with foreign keys unenforced, per connection, for backward compatibility. UQL emits the
14
+ * constraints in its DDL, so leaving them off means a declared `onDelete: 'CASCADE'` silently does
15
+ * nothing and a dangling reference is accepted. Enabled here on every driver, as TypeORM also does.
16
+ */
12
17
  async createDb() {
13
18
  // `bun:sqlite` rejects option keys it does not know, and rejects an options object carrying no
14
19
  // open flags, so `extensions` is stripped out and what remains of it collapses back to nothing.
@@ -19,11 +24,13 @@ export class Sqlite3QuerierPool extends AbstractLocalSqliteQuerierPool {
19
24
  const { adaptBunSqlite } = await import('./bunSqliteAdapter.bun.js');
20
25
  const bunDb = new BunDatabase(this.filename, opts);
21
26
  bunDb.run('PRAGMA journal_mode = WAL');
27
+ bunDb.run('PRAGMA foreign_keys = ON');
22
28
  return adaptBunSqlite(bunDb);
23
29
  }
24
30
  const { default: BetterSqlite3 } = await import('better-sqlite3');
25
31
  const db = new BetterSqlite3(this.filename, opts);
26
32
  db.pragma('journal_mode = WAL');
33
+ db.pragma('foreign_keys = ON');
27
34
  return db;
28
35
  }
29
36
  }
@@ -30,6 +30,7 @@ export class TursoLocalQuerierPool extends AbstractSqlQuerierPool {
30
30
  // Annotated rather than cast, so the structural contract is checked against the real driver.
31
31
  const db = await connect(this.filename, this.opts);
32
32
  await db.pragma('journal_mode = WAL');
33
+ await db.pragma('foreign_keys = ON');
33
34
  return db;
34
35
  }
35
36
  async end() {
@@ -1,4 +1,4 @@
1
- import type { IndexType } from '../schema/types.js';
1
+ import type { ForeignKeyAction, IndexType } from '../schema/types.js';
2
2
  import type { FilterOptions } from './query.js';
3
3
  import type { QueryRaw } from './queryRaw.js';
4
4
  import type { DistributiveOmit, Json, Scalar, Type, Unpacked } from './utility.js';
@@ -362,6 +362,20 @@ export type RelationOptions<E = any> = {
362
362
  entity: EntityGetter<E>;
363
363
  cardinality: RelationCardinality;
364
364
  readonly cascade?: boolean | CascadeType;
365
+ /**
366
+ * Referential actions for the generated foreign key, letting the database do the work instead of the
367
+ * ORM: `onDelete: 'CASCADE'` makes deleting the parent a single statement rather than the graph walk
368
+ * `cascade` performs. Defaults to the migrator's `defaultForeignKeyAction`.
369
+ *
370
+ * Read only from the side that owns the key (`@ManyToOne`, or a `@OneToOne` without `mappedBy`),
371
+ * matching where TypeORM, Prisma and Drizzle put it. Not available on a bare `@Field({ references })`
372
+ * with no relation, because `FieldOptions.onUpdate` already means a value callback there.
373
+ *
374
+ * This overlaps `cascade` rather than conflicting with it: declaring both deletes the children in JS
375
+ * and then leaves the foreign key nothing to cascade, so pick one.
376
+ */
377
+ readonly onDelete?: ForeignKeyAction;
378
+ readonly onUpdate?: ForeignKeyAction;
365
379
  mappedBy?: RelationMappedBy<E>;
366
380
  /**
367
381
  * The pivot entity of a many-to-many. Unconstrained by `E`: a pivot holds foreign keys to both
@@ -392,7 +406,7 @@ type RelationOwnerJoin<E> = Required<Pick<RelationOptions<E>, 'through'>> | Requ
392
406
  * without one of the three, resolution has no columns to join on and throws.
393
407
  */
394
408
  type RelationJoin<E> = RelationOwnerJoin<E> | Required<Pick<RelationOptions<E>, 'mappedBy'>>;
395
- type RelationOptionsOwner<E> = Pick<RelationOptions<E>, 'entity' | 'references' | 'cascade'>;
409
+ type RelationOptionsOwner<E> = Pick<RelationOptions<E>, 'entity' | 'references' | 'cascade' | 'onDelete' | 'onUpdate'>;
396
410
  type RelationOptionsInverseSide<E> = Pick<RelationOptions<E>, 'entity' | 'cascade'> & Required<Pick<RelationOptions<E>, 'mappedBy'>>;
397
411
  type RelationOptionsThroughOwner<E> = Pick<RelationOptions<E>, 'entity' | 'cascade'> & RelationOwnerJoin<E>;
398
412
  /**
@@ -174,22 +174,43 @@ export interface SchemaDiff {
174
174
  readonly foreignKeysToAdd?: ForeignKeySchema[];
175
175
  readonly foreignKeysToDrop?: string[];
176
176
  }
177
+ export interface CreateSchemaOptions {
178
+ readonly ifNotExists?: boolean;
179
+ /**
180
+ * Restrict which tables are created, for an incremental migration adding one table to a schema that
181
+ * already exists. Constraints still resolve against the full entity graph.
182
+ */
183
+ readonly only?: readonly string[];
184
+ /**
185
+ * Emit the tables without their foreign keys. Only the integration fixtures want this, and only until
186
+ * their data stops relying on dangling references; a migration always wants the constraints.
187
+ */
188
+ readonly foreignKeys?: boolean;
189
+ }
190
+ export interface DropSchemaOptions {
191
+ readonly ifExists?: boolean;
192
+ readonly cascade?: boolean;
193
+ }
177
194
  /**
178
195
  * Interface for generating DDL statements from entity metadata
179
196
  */
180
197
  export interface SchemaGenerator {
181
198
  /**
182
- * DDL to create an entity’s table: one element per `querier.run` (optional `CREATE EXTENSION`, `CREATE TABLE`, each `CREATE INDEX`, …).
183
- * Join with `'\n'` if you need a single script blob.
199
+ * The whole schema for `entities`: every table, then the foreign keys between them.
200
+ *
201
+ * There is deliberately no per-entity counterpart. One entity means an AST holding one table, so every
202
+ * cross-entity foreign key has nothing to resolve against and is dropped: all three call sites that
203
+ * used to work that way emitted schemas with no referential integrity.
184
204
  */
185
- generateCreateTable<E>(entity: Type<E>, options?: {
186
- ifNotExists?: boolean;
187
- }): string[];
205
+ generateCreateSchema(entities: readonly Type<unknown>[], options?: CreateSchemaOptions): string[];
206
+ /**
207
+ * Every `DROP TABLE` for `entities`, dependents first. The inverse of {@link generateCreateSchema},
208
+ * and the reason it takes the whole set: dropping in any order that ignores the relation graph is
209
+ * rejected once the foreign keys are really there.
210
+ */
211
+ generateDropSchema(entities: readonly Type<unknown>[], options?: DropSchemaOptions): string[];
188
212
  /** Generate DROP TABLE statement. */
189
- generateDropTable(tableName: string, options?: {
190
- ifExists?: boolean;
191
- cascade?: boolean;
192
- }): string;
213
+ generateDropTable(tableName: string, options?: DropSchemaOptions): string;
193
214
  /**
194
215
  * Generate ALTER TABLE statements based on schema diff
195
216
  */
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "homepage": "https://uql-orm.dev",
4
4
  "description": "Extremely fast, type-safe TypeScript ORM - one API for every database",
5
5
  "license": "MIT",
6
- "version": "0.24.7",
6
+ "version": "0.25.0",
7
7
  "type": "module",
8
8
  "engines": {
9
9
  "node": ">=24"
@@ -141,8 +141,8 @@
141
141
  "mariadb": "^3.5.3",
142
142
  "mongodb": "^7.5.0",
143
143
  "mysql2": "^3.23.2",
144
- "pg": "^8.22.0",
145
- "pg-query-stream": "^4.16.0",
144
+ "pg": "^8.23.0",
145
+ "pg-query-stream": "^4.17.0",
146
146
  "rxjs": "^7.8.2",
147
147
  "sqlite-vec": "^0.1.9",
148
148
  "ws": "^8.21.3"
@@ -198,5 +198,5 @@
198
198
  "publishConfig": {
199
199
  "access": "public"
200
200
  },
201
- "gitHead": "26ffc7e2968761bc95a0bb1ffe0aeb37600ba1f1"
201
+ "gitHead": "cfe2b005308b808d2e6e3d6e062c13791c883d13"
202
202
  }