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 +1 -1
- package/dist/dialect/abstractSqlDialect.js +5 -1
- package/dist/migrate/generator/mongoSchemaGenerator.d.ts +9 -1
- package/dist/migrate/generator/mongoSchemaGenerator.js +18 -0
- package/dist/migrate/migrator.d.ts +9 -0
- package/dist/migrate/migrator.js +32 -21
- package/dist/migrate/schemaGenerator.d.ts +19 -8
- package/dist/migrate/schemaGenerator.js +48 -5
- package/dist/querier/abstractSqlQuerier.js +3 -1
- package/dist/schema/schemaASTBuilder.d.ts +1 -1
- package/dist/schema/schemaASTBuilder.js +4 -2
- package/dist/sqlite/nodeSqliteQuerierPool.js +1 -0
- package/dist/sqlite/sqliteQuerierPool.d.ts +5 -0
- package/dist/sqlite/sqliteQuerierPool.js +7 -0
- package/dist/turso/tursoLocalQuerierPool.js +1 -0
- package/dist/type/entity.d.ts +16 -2
- package/dist/type/migration.d.ts +30 -9
- package/package.json +4 -4
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,
|
|
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
|
-
|
|
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
|
package/dist/migrate/migrator.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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
|
|
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
|
-
|
|
360
|
+
creating.push(diff.tableName);
|
|
361
361
|
}
|
|
362
362
|
else if (diff.type === 'alter') {
|
|
363
|
-
|
|
363
|
+
altering.push(...this.generator.generateAlterTable(this.filterDiff(diff, options)));
|
|
364
364
|
}
|
|
365
365
|
}
|
|
366
|
-
return
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
185
|
-
|
|
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() {
|
package/dist/type/entity.d.ts
CHANGED
|
@@ -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
|
/**
|
package/dist/type/migration.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
183
|
-
*
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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.
|
|
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.
|
|
145
|
-
"pg-query-stream": "^4.
|
|
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": "
|
|
201
|
+
"gitHead": "cfe2b005308b808d2e6e3d6e062c13791c883d13"
|
|
202
202
|
}
|