uql-orm 0.45.1 → 0.47.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/bunSql/bunSqlQuerier.d.ts +7 -0
- package/dist/bunSql/bunSqlQuerier.js +7 -0
- package/dist/bunSql/bunSqlQuerierPool.d.ts +1 -0
- package/dist/bunSql/bunSqlQuerierPool.js +6 -1
- package/dist/bunSql/index.d.ts +1 -0
- package/dist/bunSql/index.js +1 -0
- package/dist/dialect/abstractSqlDialect.d.ts +22 -2
- package/dist/dialect/abstractSqlDialect.js +55 -18
- package/dist/dialect/aliases.d.ts +4 -0
- package/dist/dialect/aliases.js +4 -0
- package/dist/dialect/mysqlLikeSqlDialect.js +2 -1
- package/dist/dialect/pgLikeSqlDialect.d.ts +14 -0
- package/dist/dialect/pgLikeSqlDialect.js +42 -2
- package/dist/entity/metadata/definition.js +8 -1
- package/dist/migrate/builder/columnBuilder.d.ts +14 -0
- package/dist/migrate/builder/columnBuilder.js +28 -9
- package/dist/migrate/builder/tableBuilder.js +8 -19
- package/dist/migrate/builder/types.d.ts +16 -33
- package/dist/migrate/codegen/entityCodeGenerator.js +4 -5
- package/dist/migrate/codegen/fieldOptionsSource.d.ts +2 -0
- package/dist/migrate/codegen/fieldOptionsSource.js +49 -33
- package/dist/migrate/codegen/indexDecoratorSource.js +1 -9
- package/dist/migrate/codegen/sourceLiteral.d.ts +12 -0
- package/dist/migrate/codegen/sourceLiteral.js +17 -0
- package/dist/migrate/generator/definitionToNode.d.ts +21 -0
- package/dist/migrate/generator/definitionToNode.js +47 -25
- package/dist/migrate/introspection/baseSqlIntrospector.d.ts +4 -2
- package/dist/migrate/introspection/baseSqlIntrospector.js +11 -11
- package/dist/migrate/introspection/mongoIntrospector.d.ts +1 -1
- package/dist/migrate/introspection/mongoIntrospector.js +2 -2
- package/dist/migrate/introspection/sqliteIntrospector.d.ts +5 -0
- package/dist/migrate/introspection/sqliteIntrospector.js +6 -2
- package/dist/migrate/schemaGenerator.d.ts +48 -2
- package/dist/migrate/schemaGenerator.js +109 -36
- package/dist/mongo/mongoDialect.js +2 -1
- package/dist/mongo/mongodbQuerier.d.ts +16 -0
- package/dist/mongo/mongodbQuerier.js +50 -1
- package/dist/querier/abstractQuerier.d.ts +18 -1
- package/dist/querier/abstractQuerier.js +50 -14
- package/dist/querier/abstractSqlQuerier.d.ts +14 -0
- package/dist/querier/abstractSqlQuerier.js +17 -0
- package/dist/schema/schemaAST.d.ts +34 -3
- package/dist/schema/schemaAST.js +4 -9
- package/dist/schema/schemaASTBuilder.js +5 -2
- package/dist/schema/schemaASTDiffer.js +8 -4
- package/dist/schema/types.d.ts +2 -0
- package/dist/sqlite/sqliteDialect.js +2 -1
- package/dist/type/dialect.d.ts +21 -2
- package/dist/type/entity.d.ts +21 -0
- package/dist/type/migration.d.ts +11 -18
- package/dist/util/dialect.util.js +5 -4
- package/dist/util/field.util.d.ts +23 -0
- package/dist/util/field.util.js +28 -0
- package/dist/util/fieldOption.util.d.ts +16 -3
- package/dist/util/fieldOption.util.js +23 -4
- package/dist/util/relationQuery.util.d.ts +34 -1
- package/dist/util/relationQuery.util.js +40 -3
- package/package.json +1 -1
|
@@ -8,7 +8,7 @@ import { getKeys, isAutoIncrement, isSoleIdField, qualifyName } from '../util/in
|
|
|
8
8
|
import { derivedCheckName, derivedForeignKeyName, derivedPrimaryKeyName } from '../util/sql.util.js';
|
|
9
9
|
import { formatDefaultValue, SqlExpression } from './builder/expressions.js';
|
|
10
10
|
import { indexDdlFor } from './ddl/index.js';
|
|
11
|
-
import { fullColumnDefinitionToNode, tableDefinitionToNode } from './generator/definitionToNode.js';
|
|
11
|
+
import { columnForeignKey, columnIndex, fullColumnDefinitionToNode, tableDefinitionToNode, } from './generator/definitionToNode.js';
|
|
12
12
|
import { indexNodeToSchema } from './generator/indexNodeToSchema.js';
|
|
13
13
|
/**
|
|
14
14
|
* Unified SQL schema generator.
|
|
@@ -61,6 +61,17 @@ export class SqlSchemaGenerator {
|
|
|
61
61
|
serialType(type) {
|
|
62
62
|
return `${this.canonicalTypeToSql(type)} ${this.dialect.autoIncrementSuffix}`;
|
|
63
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* The SQL type a column is spelled with: the engine's generated-key form for an auto-increment key,
|
|
66
|
+
* the canonical type otherwise.
|
|
67
|
+
*
|
|
68
|
+
* One method because both paths that spell a column need the same answer - written twice, with a
|
|
69
|
+
* comment asking the two to stay in sync, is how the generated key and the column referencing it
|
|
70
|
+
* came to disagree in the first place.
|
|
71
|
+
*/
|
|
72
|
+
columnSqlType(col) {
|
|
73
|
+
return col.isPrimaryKey && col.isAutoIncrement ? this.serialType(col.type) : this.canonicalTypeToSql(col.type);
|
|
74
|
+
}
|
|
64
75
|
canonicalTypeToSql(type) {
|
|
65
76
|
return canonicalToSql(type, this.dialect);
|
|
66
77
|
}
|
|
@@ -142,7 +153,9 @@ export class SqlSchemaGenerator {
|
|
|
142
153
|
// Add new columns
|
|
143
154
|
if (diff.columnsToAdd?.length) {
|
|
144
155
|
for (const column of diff.columnsToAdd) {
|
|
156
|
+
this.assertColumnAddable(diff.tableName, column);
|
|
145
157
|
statements.push(`ALTER TABLE ${tableName} ADD COLUMN ${this.generateColumnDefinitionFromSchema(column)};`);
|
|
158
|
+
statements.push(...this.generateColumnCommentStatement(diff.tableName, column, diff.schema));
|
|
146
159
|
}
|
|
147
160
|
}
|
|
148
161
|
// Alter existing columns
|
|
@@ -278,6 +291,12 @@ export class SqlSchemaGenerator {
|
|
|
278
291
|
*/
|
|
279
292
|
renderColumn(column) {
|
|
280
293
|
let def = `${this.escapeId(column.name)} ${column.type}`;
|
|
294
|
+
// Before the constraints, which every engine here accepts and is where each documents it. The
|
|
295
|
+
// clause takes the place of a `DEFAULT`, which is mutually exclusive with it; everything else -
|
|
296
|
+
// `NOT NULL`, `UNIQUE`, an enum `CHECK`, a comment - a generated column carries like any other.
|
|
297
|
+
if (column.generatedAs) {
|
|
298
|
+
def += ` GENERATED ALWAYS AS (${column.generatedAs}) STORED`;
|
|
299
|
+
}
|
|
281
300
|
if (!column.nullable && !column.isPrimaryKey) {
|
|
282
301
|
def += ' NOT NULL';
|
|
283
302
|
}
|
|
@@ -290,7 +309,7 @@ export class SqlSchemaGenerator {
|
|
|
290
309
|
}
|
|
291
310
|
def += this.defaultClause(column);
|
|
292
311
|
if (column.comment) {
|
|
293
|
-
def += this.generateColumnComment(column.
|
|
312
|
+
def += this.generateColumnComment(column.comment);
|
|
294
313
|
}
|
|
295
314
|
return def;
|
|
296
315
|
}
|
|
@@ -349,15 +368,40 @@ export class SqlSchemaGenerator {
|
|
|
349
368
|
}
|
|
350
369
|
return [`ALTER TABLE ${table} ${this.dialect.alterColumnSyntax} ${newDefinition};`];
|
|
351
370
|
}
|
|
371
|
+
/** The inline ` COMMENT '...'` a column declaration carries, where the engine takes one there. */
|
|
372
|
+
generateColumnComment(comment) {
|
|
373
|
+
return this.features.commentSyntax === 'inline' ? ` COMMENT ${this.dialect.escape(comment)}` : '';
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* The `COMMENT ON` statements a table and its columns need, on an engine that carries a comment
|
|
377
|
+
* that way. Empty on the others: MySQL writes them inline, SQLite has no comments at all.
|
|
378
|
+
*
|
|
379
|
+
* Emitted after the `CREATE TABLE` rather than folded into it, which is what `COMMENT ON` requires -
|
|
380
|
+
* and what makes a comment reach Postgres, where it was previously read as unsupported and dropped.
|
|
381
|
+
*/
|
|
382
|
+
generateCommentStatements(table) {
|
|
383
|
+
if (this.features.commentSyntax !== 'statement') {
|
|
384
|
+
return [];
|
|
385
|
+
}
|
|
386
|
+
const tableRef = this.dialect.escapeQualifiedId(table.name, table.schema);
|
|
387
|
+
const statements = table.comment ? [`COMMENT ON TABLE ${tableRef} IS ${this.dialect.escape(table.comment)};`] : [];
|
|
388
|
+
for (const col of table.columns.values()) {
|
|
389
|
+
statements.push(...this.generateColumnCommentStatement(table.name, col, table.schema));
|
|
390
|
+
}
|
|
391
|
+
return statements;
|
|
392
|
+
}
|
|
352
393
|
/**
|
|
353
|
-
*
|
|
394
|
+
* The `COMMENT ON COLUMN` one column needs, on an engine that carries a comment that way.
|
|
395
|
+
*
|
|
396
|
+
* Shared by `CREATE TABLE` and every path that adds a column: written only for the former, a column
|
|
397
|
+
* added later reached the database undocumented, the way its enum `CHECK` used to.
|
|
354
398
|
*/
|
|
355
|
-
|
|
356
|
-
if (this.features.
|
|
357
|
-
|
|
358
|
-
return ` COMMENT '${escapedComment}'`;
|
|
399
|
+
generateColumnCommentStatement(tableName, column, schema) {
|
|
400
|
+
if (!column.comment || this.features.commentSyntax !== 'statement') {
|
|
401
|
+
return [];
|
|
359
402
|
}
|
|
360
|
-
|
|
403
|
+
const tableRef = this.dialect.escapeQualifiedId(tableName, schema);
|
|
404
|
+
return [`COMMENT ON COLUMN ${tableRef}.${this.escapeId(column.name)} IS ${this.dialect.escape(column.comment)};`];
|
|
361
405
|
}
|
|
362
406
|
/**
|
|
363
407
|
* Compare an entity with a database table node and return the differences.
|
|
@@ -468,20 +512,10 @@ export class SqlSchemaGenerator {
|
|
|
468
512
|
defaultsEqual: (expected, actual) => this.isDefaultValueEqual(actual, expected),
|
|
469
513
|
};
|
|
470
514
|
}
|
|
515
|
+
/** Spread, not copied field by field, so a field the node gains cannot go missing here. */
|
|
471
516
|
columnNodeToSchema(col) {
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
// The same rule `generateColumnFromNode` renders by, so a column added to an existing table
|
|
475
|
-
// gets the type it would have had if the table were created from scratch.
|
|
476
|
-
type: col.isPrimaryKey && col.isAutoIncrement ? this.serialType(col.type) : this.canonicalTypeToSql(col.type),
|
|
477
|
-
nullable: col.nullable,
|
|
478
|
-
defaultValue: col.defaultValue,
|
|
479
|
-
isPrimaryKey: col.isPrimaryKey,
|
|
480
|
-
isAutoIncrement: col.isAutoIncrement,
|
|
481
|
-
isUnique: col.isUnique,
|
|
482
|
-
comment: col.comment,
|
|
483
|
-
enum: col.enum,
|
|
484
|
-
};
|
|
517
|
+
const { table: _table, referencedBy: _referencedBy, references: _references, ...column } = col;
|
|
518
|
+
return { ...column, type: this.columnSqlType(col) };
|
|
485
519
|
}
|
|
486
520
|
/**
|
|
487
521
|
* Compare two default values for equality
|
|
@@ -540,11 +574,8 @@ export class SqlSchemaGenerator {
|
|
|
540
574
|
});
|
|
541
575
|
for (const rel of table.outgoingRelations) {
|
|
542
576
|
if (rel.from.columns.length > 0) {
|
|
543
|
-
const
|
|
544
|
-
|
|
545
|
-
const constraintName = rel.name ? `CONSTRAINT ${this.escapeId(rel.name)} ` : '';
|
|
546
|
-
constraints.push(`${constraintName}FOREIGN KEY (${fromCols}) REFERENCES ${this.dialect.escapeQualifiedId(rel.to.table.name, rel.to.table.schema)} (${toCols})` +
|
|
547
|
-
` ON DELETE ${rel.onDelete ?? this.defaultForeignKeyAction} ON UPDATE ${rel.onUpdate ?? this.defaultForeignKeyAction}`);
|
|
577
|
+
const refTable = this.dialect.escapeQualifiedId(rel.to.table.name, rel.to.table.schema);
|
|
578
|
+
constraints.push(this.foreignKeyConstraint(table.name, foreignKeyOf(rel), refTable));
|
|
548
579
|
}
|
|
549
580
|
}
|
|
550
581
|
const ifNotExists = options.ifNotExists && this.features.ifNotExists ? 'IF NOT EXISTS ' : '';
|
|
@@ -558,6 +589,9 @@ export class SqlSchemaGenerator {
|
|
|
558
589
|
if (this.dialect.tableOptions) {
|
|
559
590
|
createSql += ` ${this.dialect.tableOptions}`;
|
|
560
591
|
}
|
|
592
|
+
if (table.comment && this.features.commentSyntax === 'inline') {
|
|
593
|
+
createSql += ` COMMENT=${this.dialect.escape(table.comment)}`;
|
|
594
|
+
}
|
|
561
595
|
createSql += ';';
|
|
562
596
|
const statements = [];
|
|
563
597
|
if (this.dialect.vectorExtension) {
|
|
@@ -567,6 +601,7 @@ export class SqlSchemaGenerator {
|
|
|
567
601
|
}
|
|
568
602
|
}
|
|
569
603
|
statements.push(createSql);
|
|
604
|
+
statements.push(...this.generateCommentStatements(table));
|
|
570
605
|
for (const idx of table.indexes) {
|
|
571
606
|
statements.push(this.generateCreateIndexFromNode(idx));
|
|
572
607
|
}
|
|
@@ -577,10 +612,7 @@ export class SqlSchemaGenerator {
|
|
|
577
612
|
* so only a lone primary key column carries `PRIMARY KEY` inline.
|
|
578
613
|
*/
|
|
579
614
|
generateColumnFromNode(col) {
|
|
580
|
-
return this.renderColumn({
|
|
581
|
-
...col,
|
|
582
|
-
type: col.isPrimaryKey && col.isAutoIncrement ? this.serialType(col.type) : this.canonicalTypeToSql(col.type),
|
|
583
|
-
});
|
|
615
|
+
return this.renderColumn({ ...col, type: this.columnSqlType(col) });
|
|
584
616
|
}
|
|
585
617
|
/**
|
|
586
618
|
* Generate CREATE INDEX SQL from an IndexNode.
|
|
@@ -600,9 +632,28 @@ export class SqlSchemaGenerator {
|
|
|
600
632
|
}
|
|
601
633
|
return `ALTER TABLE ${this.escapeId(oldName)} RENAME TO ${this.escapeId(newName)};`;
|
|
602
634
|
}
|
|
635
|
+
/**
|
|
636
|
+
* `ADD COLUMN`, plus the constraint and index the column declares.
|
|
637
|
+
*
|
|
638
|
+
* `CREATE TABLE` lifts a column's `references` and `index` onto the table it is building; this had
|
|
639
|
+
* no lift, so a hand-written `addColumn(...).references(...).index()` emitted the column alone and
|
|
640
|
+
* dropped both without a word. Several statements in one string is what `generateAlterColumnSql`
|
|
641
|
+
* already returns, and `execute` splits them.
|
|
642
|
+
*/
|
|
603
643
|
generateAddColumnSql(tableName, column) {
|
|
644
|
+
this.assertColumnAddable(tableName, column);
|
|
604
645
|
const colSql = this.generateColumnFromNode(fullColumnDefinitionToNode(column, tableName));
|
|
605
|
-
|
|
646
|
+
const statements = [`ALTER TABLE ${this.escapeId(tableName)} ADD COLUMN ${colSql};`];
|
|
647
|
+
const foreignKey = columnForeignKey(column);
|
|
648
|
+
if (foreignKey) {
|
|
649
|
+
statements.push(...this.addForeignKeyStatements(tableName, [foreignKey]));
|
|
650
|
+
}
|
|
651
|
+
const index = columnIndex(tableName, column);
|
|
652
|
+
if (index) {
|
|
653
|
+
statements.push(this.generateCreateIndex(tableName, index));
|
|
654
|
+
}
|
|
655
|
+
statements.push(...this.generateColumnCommentStatement(tableName, column));
|
|
656
|
+
return statements.join('\n');
|
|
606
657
|
}
|
|
607
658
|
generateAlterColumnSql(tableName, columnName, column) {
|
|
608
659
|
const node = fullColumnDefinitionToNode(column, tableName);
|
|
@@ -614,16 +665,26 @@ export class SqlSchemaGenerator {
|
|
|
614
665
|
generateRenameColumnSql(tableName, oldName, newName) {
|
|
615
666
|
return `ALTER TABLE ${this.escapeId(tableName)} RENAME COLUMN ${this.escapeId(oldName)} TO ${this.escapeId(newName)};`;
|
|
616
667
|
}
|
|
617
|
-
|
|
668
|
+
/**
|
|
669
|
+
* `CONSTRAINT <name> FOREIGN KEY (...) REFERENCES ... ON DELETE ... ON UPDATE ...`.
|
|
670
|
+
*
|
|
671
|
+
* One spelling for the two places that need it - inline in a `CREATE TABLE`, and after `ADD` in an
|
|
672
|
+
* `ALTER`. Written twice, the two drifted over which end they qualified with a schema.
|
|
673
|
+
*/
|
|
674
|
+
foreignKeyConstraint(tableName, foreignKey, refTableSql) {
|
|
618
675
|
const fkCols = foreignKey.columns.map((c) => this.escapeId(c)).join(', ');
|
|
619
676
|
const refCols = foreignKey.references.columns.map((c) => this.escapeId(c)).join(', ');
|
|
620
|
-
|
|
677
|
+
return (`CONSTRAINT ${this.escapeId(constraintNameOf(tableName, foreignKey))} ` +
|
|
678
|
+
`FOREIGN KEY (${fkCols}) REFERENCES ${refTableSql} (${refCols}) ` +
|
|
679
|
+
`ON DELETE ${foreignKey.onDelete ?? this.defaultForeignKeyAction} ` +
|
|
680
|
+
`ON UPDATE ${foreignKey.onUpdate ?? this.defaultForeignKeyAction}`);
|
|
681
|
+
}
|
|
682
|
+
generateAddForeignKeySql(tableName, foreignKey) {
|
|
621
683
|
if (!this.features.foreignKeyAlter) {
|
|
622
684
|
throw new TypeError(`Dialect ${this.dialect} does not support adding foreign keys to existing tables`);
|
|
623
685
|
}
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
`ON DELETE ${foreignKey.onDelete ?? this.defaultForeignKeyAction} ON UPDATE ${foreignKey.onUpdate ?? this.defaultForeignKeyAction};`);
|
|
686
|
+
const constraint = this.foreignKeyConstraint(tableName, foreignKey, this.escapeId(foreignKey.references.table));
|
|
687
|
+
return `ALTER TABLE ${this.escapeId(tableName)} ADD ${constraint};`;
|
|
627
688
|
}
|
|
628
689
|
generateDropForeignKeySql(tableName, constraintName) {
|
|
629
690
|
return `ALTER TABLE ${this.escapeId(tableName)} ${this.dialect.dropForeignKeySyntax} ${this.escapeId(constraintName)};`;
|
|
@@ -659,6 +720,18 @@ export class SqlSchemaGenerator {
|
|
|
659
720
|
}
|
|
660
721
|
return `ALTER TABLE ${table} DROP CONSTRAINT ${this.escapeId(constraintName)};`;
|
|
661
722
|
}
|
|
723
|
+
/**
|
|
724
|
+
* A column an `ALTER` can carry. Only a generated one is ever refused, and only where the engine
|
|
725
|
+
* takes it in a `CREATE TABLE` but not afterwards.
|
|
726
|
+
*/
|
|
727
|
+
assertColumnAddable(tableName, column) {
|
|
728
|
+
if (!column.generatedAs || this.features.generatedColumnAdd) {
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
throw new TypeError(`${this.dialect}: Cannot add the computed column "${column.name}" to the existing table ` +
|
|
732
|
+
`"${tableName}" - this database only accepts one in a CREATE TABLE. Drop \`stored\` to have the ` +
|
|
733
|
+
'expression spliced into each statement instead, or recreate the table in a written migration.');
|
|
734
|
+
}
|
|
662
735
|
assertPrimaryKeyAlterable(tableName) {
|
|
663
736
|
if (this.features.primaryKeyAlter) {
|
|
664
737
|
return;
|
|
@@ -17,7 +17,8 @@ export const mongoDialectFeatures = {
|
|
|
17
17
|
renameColumn: false,
|
|
18
18
|
foreignKeyAlter: false,
|
|
19
19
|
primaryKeyAlter: false,
|
|
20
|
-
|
|
20
|
+
generatedColumnAdd: false,
|
|
21
|
+
commentSyntax: 'none',
|
|
21
22
|
vectorIndexRequiresNotNull: false,
|
|
22
23
|
vectorSupportsLength: false,
|
|
23
24
|
supportsTimestamptz: false,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Document, MongoClient } from 'mongodb';
|
|
2
2
|
import { AbstractQuerier } from '../querier/index.js';
|
|
3
3
|
import type { EntityData, ExtraOptions, IdValue, PrimaryKey, Query, QueryAggMap, QueryAggregate, QueryAggregateResult, QueryConflictPaths, QueryFilter, QueryGroupMap, QueryOptions, QuerySearch, TransactionOptions, Type, UpdatePayload } from '../type/index.js';
|
|
4
|
+
import { type ParentPartition } from '../util/index.js';
|
|
4
5
|
import type { MongoDialect } from './mongoDialect.js';
|
|
5
6
|
export declare class MongodbQuerier extends AbstractQuerier {
|
|
6
7
|
readonly dialect: MongoDialect;
|
|
@@ -10,6 +11,21 @@ export declare class MongodbQuerier extends AbstractQuerier {
|
|
|
10
11
|
constructor(dialect: MongoDialect, conn: MongoClient, extra?: ExtraOptions | undefined);
|
|
11
12
|
private execute;
|
|
12
13
|
protected internalFindMany<E extends Document>(entity: Type<E>, q: Query<E>, opts?: QueryOptions): Promise<E[]>;
|
|
14
|
+
/**
|
|
15
|
+
* Every parent's own bounded page. One `$unionWith` per parent after the first, so the whole page is
|
|
16
|
+
* one round trip - measured ~6x faster than a query each (11.0 ms -> 1.9 ms at 50 parents, 87.5 ms
|
|
17
|
+
* -> 14.1 ms at 500), because `execute` serializes on the session and a query each is N round trips
|
|
18
|
+
* rather than N concurrent ones.
|
|
19
|
+
*
|
|
20
|
+
* Both arms return documents with their own relations already filled, so this only chooses between
|
|
21
|
+
* them: leaving that to the caller once meant the arm that fills its own did it twice.
|
|
22
|
+
* [The design](../../../../architecture/populate-limits.md).
|
|
23
|
+
*/
|
|
24
|
+
protected internalFindManyPerParent<E extends Document>(entity: Type<E>, q: Query<E>, { joins, parents }: ParentPartition): Promise<E[]>;
|
|
25
|
+
/** Every parent's page as one `$unionWith` pipeline. */
|
|
26
|
+
private readInOnePipeline;
|
|
27
|
+
/** A query each, for what one pipeline cannot carry. `internalFindMany` fills its own relations. */
|
|
28
|
+
private readEachInTurn;
|
|
13
29
|
protected internalFindManyStream<E extends Document>(entity: Type<E>, q: Query<E>, opts?: QueryOptions): AsyncGenerator<E, void, unknown>;
|
|
14
30
|
private buildScalarProjection;
|
|
15
31
|
/** Build a MongoDB FindCursor with filter, projection, sort, skip, and limit from the query. */
|
|
@@ -2,7 +2,7 @@ import { COUNT_ALIAS } from '../dialect/aliases.js';
|
|
|
2
2
|
import { hasRequiredJoin } from '../dialect/queryJoins.js';
|
|
3
3
|
import { getMeta, idOf, soleIdOf } from '../entity/index.js';
|
|
4
4
|
import { AbstractQuerier, enrichError } from '../querier/index.js';
|
|
5
|
-
import { clone, getKeys, getRelationRequestSummary, getSoftDeleteValue, hasKeys, idOnlyQuery, isPagedQuery, populatesRelations, throwNoPendingTransaction, throwPendingTransaction, withoutSoftDeleteFilter, } from '../util/index.js';
|
|
5
|
+
import { clone, getKeys, getRelationRequestSummary, getSoftDeleteValue, hasKeys, idOnlyQuery, isPagedQuery, populatesRelations, queryChildrenOf, throwNoPendingTransaction, throwPendingTransaction, withoutSoftDeleteFilter, } from '../util/index.js';
|
|
6
6
|
/**
|
|
7
7
|
* `$limit: 0` asks for no rows, the way it does on every SQL dialect - but MongoDB reads `limit(0)`
|
|
8
8
|
* as *unlimited*, so a read that passed it straight to the driver came back with the whole
|
|
@@ -11,6 +11,12 @@ import { clone, getKeys, getRelationRequestSummary, getSoftDeleteValue, hasKeys,
|
|
|
11
11
|
function asksForNoRows(q) {
|
|
12
12
|
return q.$limit === 0;
|
|
13
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* What MongoDB accepts in one pipeline. Bisected against a real server: 1000 top-level stages are
|
|
16
|
+
* accepted and 1001 refused (`Pipeline length must be no longer than 1000 stages`), and a
|
|
17
|
+
* `$unionWith`'s own sub-pipeline stages do not count toward it.
|
|
18
|
+
*/
|
|
19
|
+
const MAX_PIPELINE_STAGES = 1000;
|
|
14
20
|
export class MongodbQuerier extends AbstractQuerier {
|
|
15
21
|
dialect;
|
|
16
22
|
conn;
|
|
@@ -63,6 +69,49 @@ export class MongodbQuerier extends AbstractQuerier {
|
|
|
63
69
|
return documents;
|
|
64
70
|
});
|
|
65
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* Every parent's own bounded page. One `$unionWith` per parent after the first, so the whole page is
|
|
74
|
+
* one round trip - measured ~6x faster than a query each (11.0 ms -> 1.9 ms at 50 parents, 87.5 ms
|
|
75
|
+
* -> 14.1 ms at 500), because `execute` serializes on the session and a query each is N round trips
|
|
76
|
+
* rather than N concurrent ones.
|
|
77
|
+
*
|
|
78
|
+
* Both arms return documents with their own relations already filled, so this only chooses between
|
|
79
|
+
* them: leaving that to the caller once meant the arm that fills its own did it twice.
|
|
80
|
+
* [The design](../../../../architecture/populate-limits.md).
|
|
81
|
+
*/
|
|
82
|
+
async internalFindManyPerParent(entity, q, { joins, parents }) {
|
|
83
|
+
const queries = parents.map((parent) => queryChildrenOf(q, joins, parent));
|
|
84
|
+
// A vector sort needs a pipeline of its own shape, which only `internalFindMany` builds.
|
|
85
|
+
if (this.dialect.extractVectorSort(q.$sort)) {
|
|
86
|
+
return this.readEachInTurn(entity, queries);
|
|
87
|
+
}
|
|
88
|
+
const pipelines = queries.map((it) => this.dialect.aggregationPipeline(entity, it));
|
|
89
|
+
// Counted, not estimated: the leading branch's own length grows with every `$lookup` a populate
|
|
90
|
+
// adds, so a fixed parent budget would let a richer query overflow at the server instead.
|
|
91
|
+
const stages = (pipelines[0]?.length ?? 0) + pipelines.length - 1;
|
|
92
|
+
return stages > MAX_PIPELINE_STAGES
|
|
93
|
+
? this.readEachInTurn(entity, queries)
|
|
94
|
+
: this.readInOnePipeline(entity, q, pipelines);
|
|
95
|
+
}
|
|
96
|
+
/** Every parent's page as one `$unionWith` pipeline. */
|
|
97
|
+
async readInOnePipeline(entity, q, pipelines) {
|
|
98
|
+
const meta = getMeta(entity);
|
|
99
|
+
const [first, ...rest] = pipelines;
|
|
100
|
+
const documents = await this.runPipeline(entity, meta, [
|
|
101
|
+
...first,
|
|
102
|
+
...rest.map((pipeline) => ({ $unionWith: { coll: meta.name, pipeline } })),
|
|
103
|
+
]);
|
|
104
|
+
await this.fillToManyRelations(entity, documents, q.$populate);
|
|
105
|
+
return documents;
|
|
106
|
+
}
|
|
107
|
+
/** A query each, for what one pipeline cannot carry. `internalFindMany` fills its own relations. */
|
|
108
|
+
async readEachInTurn(entity, queries) {
|
|
109
|
+
const documents = [];
|
|
110
|
+
for (const query of queries) {
|
|
111
|
+
documents.push(...(await this.internalFindMany(entity, query)));
|
|
112
|
+
}
|
|
113
|
+
return documents;
|
|
114
|
+
}
|
|
66
115
|
async *internalFindManyStream(entity, q, opts) {
|
|
67
116
|
if (asksForNoRows(q)) {
|
|
68
117
|
return;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { EntityData, EntityId, ExtraOptions, FieldKey, IdValue, Querier, Query, QueryAggMap, QueryAggregate, QueryAggregateResult, QueryConflictPaths, QueryFilter, QueryFindResult, QueryGroupMap, QueryOneProjected, QueryOptions, QueryPage, QueryPopulate, QueryProjected, QuerySearch, QueryStreamProjected, QueryUpdateResult, RawRow, RelationKey, TransactionOptions, Type, UpdatePayload } from '../type/index.js';
|
|
2
|
-
import { LoggerWrapper, type ParentJoin } from '../util/index.js';
|
|
2
|
+
import { LoggerWrapper, type ParentJoin, type ParentPartition } from '../util/index.js';
|
|
3
3
|
/**
|
|
4
4
|
* Base class for all database queriers.
|
|
5
5
|
* It provides a standardized way to execute tasks serially to prevent race conditions on database connections.
|
|
@@ -64,6 +64,14 @@ export declare abstract class AbstractQuerier implements Querier {
|
|
|
64
64
|
$entity: Type<E>;
|
|
65
65
|
}, opts?: QueryOptions): AsyncIterable<QueryFindResult<E, S, V, X, P>>;
|
|
66
66
|
findManyStream<E extends object, const S extends FieldKey<E> = never, const V = true, const X extends FieldKey<E> = never, const P extends RelationKey<E> = never>(entity: Type<E>, q: QueryStreamProjected<E, S, V, X, P>, opts?: QueryOptions): AsyncIterable<QueryFindResult<E, S, V, X, P>>;
|
|
67
|
+
/**
|
|
68
|
+
* The children of every parent in `parents`, at most `$limit` each after `$skip` - what a to-many
|
|
69
|
+
* `$populate` carrying either one means. One statement, not one per parent.
|
|
70
|
+
*
|
|
71
|
+
* Abstract rather than defaulted: a default would be N queries, which is the N+1 that batched
|
|
72
|
+
* population exists to prevent, and it would be invisible to whichever backend forgot to override.
|
|
73
|
+
*/
|
|
74
|
+
protected abstract internalFindManyPerParent<E extends object>(entity: Type<E>, q: Query<E>, partition: ParentPartition): Promise<E[]>;
|
|
67
75
|
protected abstract internalFindManyStream<E extends object>(entity: Type<E>, q: Query<E>, opts?: QueryOptions): AsyncIterable<E>;
|
|
68
76
|
/**
|
|
69
77
|
* Find multiple records and return both the records and total count.
|
|
@@ -137,6 +145,15 @@ export declare abstract class AbstractQuerier implements Querier {
|
|
|
137
145
|
protected fillToManyRelations<E>(entity: Type<E>, payload: E[], populate?: QueryPopulate<E>): Promise<void>;
|
|
138
146
|
private fillToManyThroughRelation;
|
|
139
147
|
private fillToManyOneToMany;
|
|
148
|
+
/**
|
|
149
|
+
* The children of a whole page of parents, however the relation asked for them: one bounded branch
|
|
150
|
+
* per parent when it wants a share of its own, otherwise a single flat statement over an `IN (...)`
|
|
151
|
+
* list, which is both correct and cheaper.
|
|
152
|
+
*
|
|
153
|
+
* The one place that decision is made - a one-to-many and the junction of a many-to-many differ in
|
|
154
|
+
* what they query, never in how the page is spread over its parents.
|
|
155
|
+
*/
|
|
156
|
+
private findChildrenOf;
|
|
140
157
|
protected putChildrenInParents<E>(parents: E[], children: RawRow[], joins: readonly ParentJoin[], relKey: keyof E & string): void;
|
|
141
158
|
protected insertRelations<E extends object>(entity: Type<E>, payload: E[]): Promise<void>;
|
|
142
159
|
protected updateRelations<E extends object>(entity: Type<E>, q: QuerySearch<E>, payload: UpdatePayload<E>, opts?: QueryOptions): Promise<void>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { assertSoleId, getMeta, idOf, soleIdOf } from '../entity/index.js';
|
|
2
|
-
import { asSelectMap, augmentWhere, childrenOf, clone, filterPersistableRelationKeys, forEachRequestedRelation, getKeys, getRelationRequestSummary, idOnlyQuery, isScalarId, joinedColumns, joinedRowKey, LoggerWrapper, parentJoins, parentRowKey,
|
|
2
|
+
import { asSelectMap, augmentWhere, childrenOf, clone, filterPersistableRelationKeys, forEachRequestedRelation, getKeys, getRelationRequestSummary, idOnlyQuery, isScalarId, joinedColumns, joinedRowKey, LoggerWrapper, isBoundedPerParent, parentJoins, parentRowKey, queryChildrenOfAll, parseRelationAtKey, parseRelationQueryValue, runHooks, someKey, targetKeyColumns, withoutSoftDeleteFilter, } from '../util/index.js';
|
|
3
3
|
import { enrichError } from './queryError.js';
|
|
4
4
|
import { fillRelationCounts, withIdForCounts } from './relationCount.js';
|
|
5
5
|
/**
|
|
@@ -299,22 +299,44 @@ export class AbstractQuerier {
|
|
|
299
299
|
const throughEntity = relOpts.through();
|
|
300
300
|
const throughMeta = getMeta(throughEntity);
|
|
301
301
|
const targetRelKey = getKeys(throughMeta.relations).find((key) => throughMeta.relations[key]?.references.some(({ local }) => local === targetColumn));
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
302
|
+
if (!targetRelKey) {
|
|
303
|
+
// Asserted rather than assumed: used as a key regardless, it spells the literal string
|
|
304
|
+
// `undefined`, and the statement asks the junction for a relation of that name.
|
|
305
|
+
throw new TypeError(`'${meta.name}.${relKey}' goes through '${throughMeta.name}', which declares no relation on its ` +
|
|
306
|
+
`'${targetColumn}' column. Give it one, so the target's rows can be read through it.`);
|
|
307
|
+
}
|
|
308
|
+
// A relation query names the target's columns, not the junction's, so its projection and filter
|
|
309
|
+
// belong on the populate below, resolved against the entity that has them. Spread onto the
|
|
310
|
+
// junction query instead they asked `ItemTag` for `Tag`'s columns and failed with "no such
|
|
311
|
+
// column".
|
|
312
|
+
//
|
|
313
|
+
// Ordering and paging split the other way: they describe the statement with one row per pairing,
|
|
314
|
+
// which is the junction's. Left on the populate they reached a to-one join, which rejects all
|
|
315
|
+
// four by name - so a many-to-many carrying any of them threw rather than paging.
|
|
316
|
+
//
|
|
317
|
+
// Those four are not a coincidence: they are exactly the clauses a joined relation rejects, for
|
|
318
|
+
// the same reason - each needs a statement with many rows per parent, which only the junction's
|
|
319
|
+
// is. The `satisfies` ties the two lists together, so a fifth clause added there fails to compile
|
|
320
|
+
// here rather than quietly staying on the populate and throwing again.
|
|
321
|
+
const { $sort, $limit, $skip, $distinct, ...targetQuery } = relationQuery;
|
|
322
|
+
const junctionClauses = {
|
|
323
|
+
$limit,
|
|
324
|
+
$skip,
|
|
325
|
+
$distinct,
|
|
326
|
+
// Qualified by the relation that reaches them, since the columns it names are the target's.
|
|
327
|
+
$sort: $sort && { [targetRelKey]: $sort },
|
|
328
|
+
};
|
|
329
|
+
const junctionQuery = {
|
|
309
330
|
$select: joinedColumns(joins),
|
|
331
|
+
...junctionClauses,
|
|
310
332
|
$populate: {
|
|
311
333
|
[targetRelKey]: {
|
|
312
|
-
...
|
|
334
|
+
...targetQuery,
|
|
313
335
|
$required: true,
|
|
314
336
|
},
|
|
315
337
|
},
|
|
316
|
-
|
|
317
|
-
|
|
338
|
+
};
|
|
339
|
+
const throughFounds = await this.findChildrenOf(throughEntity, junctionQuery, joins, payload, meta.fields);
|
|
318
340
|
// The junction's own columns carried onto the target's row, which is where `putChildrenInParents`
|
|
319
341
|
// reads them back from - a junction row holds the parent's key under `joined`, not under `parent`.
|
|
320
342
|
const founds = throughFounds.map((it) => ({
|
|
@@ -336,9 +358,23 @@ export class AbstractQuerier {
|
|
|
336
358
|
}
|
|
337
359
|
delete exclude?.[joined];
|
|
338
360
|
}
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
361
|
+
this.putChildrenInParents(payload, await this.findChildrenOf(relEntity, relationQuery, joins, payload, meta.fields), joins, relKey);
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* The children of a whole page of parents, however the relation asked for them: one bounded branch
|
|
365
|
+
* per parent when it wants a share of its own, otherwise a single flat statement over an `IN (...)`
|
|
366
|
+
* list, which is both correct and cheaper.
|
|
367
|
+
*
|
|
368
|
+
* The one place that decision is made - a one-to-many and the junction of a many-to-many differ in
|
|
369
|
+
* what they query, never in how the page is spread over its parents.
|
|
370
|
+
*/
|
|
371
|
+
async findChildrenOf(entity, query, joins, parents, parentFields) {
|
|
372
|
+
const founds = isBoundedPerParent(query)
|
|
373
|
+
? await this.internalFindManyPerParent(entity, query, { joins, parents, parentFields })
|
|
374
|
+
: await this.findMany(entity, queryChildrenOfAll(query, joins, parents));
|
|
375
|
+
// Read back as rows rather than as the entity they hydrate to: what follows regroups them by the
|
|
376
|
+
// join columns, which a projected entity type does not carry.
|
|
377
|
+
return founds;
|
|
342
378
|
}
|
|
343
379
|
putChildrenInParents(parents, children, joins, relKey) {
|
|
344
380
|
const childrenByParentId = {};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AbstractSqlDialect } from '../dialect/index.js';
|
|
2
2
|
import type { EntityData, ExtraOptions, IdValue, Query, QueryAggMap, QueryAggregate, QueryAggregateResult, QueryConflictPaths, QueryFilter, QueryGroupMap, QueryOptions, QuerySearch, QueryUpdateResult, SqlQuerier, TransactionOptions, Type, UpdatePayload } from '../type/index.js';
|
|
3
|
+
import { type ParentPartition } from '../util/index.js';
|
|
3
4
|
import type { BuildUpdateResultPayload } from '../util/sql.util.js';
|
|
4
5
|
import { AbstractQuerier } from './abstractQuerier.js';
|
|
5
6
|
export declare abstract class AbstractSqlQuerier extends AbstractQuerier implements SqlQuerier {
|
|
@@ -57,6 +58,19 @@ export declare abstract class AbstractSqlQuerier extends AbstractQuerier impleme
|
|
|
57
58
|
*/
|
|
58
59
|
private applyVectorTuning;
|
|
59
60
|
protected internalFindMany<E extends object>(entity: Type<E>, q: Query<E>, opts?: QueryOptions): Promise<E[]>;
|
|
61
|
+
/**
|
|
62
|
+
* One bounded subquery per parent, concatenated with `UNION ALL`, so each parent gets its own
|
|
63
|
+
* `$limit` rather than a share of one. Universal, and reads `parents x (skip + limit)` rows where a
|
|
64
|
+
* `ROW_NUMBER` window reads every matching child. [The design](../../../../architecture/populate-limits.md).
|
|
65
|
+
*
|
|
66
|
+
* Each branch is a wrapped derived table: SQLite rejects `ORDER BY`/`LIMIT` on a bare parenthesised
|
|
67
|
+
* compound branch, and the wrapper costs nothing elsewhere.
|
|
68
|
+
*
|
|
69
|
+
* Unlike {@link selectRows} this asserts no lock and tunes no vector search: `$lock` and
|
|
70
|
+
* `$candidates` describe the statement, and `parseRelationQueryValue` refuses both on a relation
|
|
71
|
+
* query, so neither can reach here.
|
|
72
|
+
*/
|
|
73
|
+
protected internalFindManyPerParent<E extends object>(entity: Type<E>, q: Query<E>, partition: ParentPartition): Promise<E[]>;
|
|
60
74
|
/**
|
|
61
75
|
* One statement for both: the page carries its own unpaged total in an extra column. An empty page
|
|
62
76
|
* has no row to carry it, which is the one case still needing a count of its own - a `$skip` past
|
|
@@ -104,6 +104,23 @@ export class AbstractSqlQuerier extends AbstractQuerier {
|
|
|
104
104
|
async internalFindMany(entity, q, opts) {
|
|
105
105
|
return this.hydrateRows(entity, q, await this.selectRows(entity, q, opts));
|
|
106
106
|
}
|
|
107
|
+
/**
|
|
108
|
+
* One bounded subquery per parent, concatenated with `UNION ALL`, so each parent gets its own
|
|
109
|
+
* `$limit` rather than a share of one. Universal, and reads `parents x (skip + limit)` rows where a
|
|
110
|
+
* `ROW_NUMBER` window reads every matching child. [The design](../../../../architecture/populate-limits.md).
|
|
111
|
+
*
|
|
112
|
+
* Each branch is a wrapped derived table: SQLite rejects `ORDER BY`/`LIMIT` on a bare parenthesised
|
|
113
|
+
* compound branch, and the wrapper costs nothing elsewhere.
|
|
114
|
+
*
|
|
115
|
+
* Unlike {@link selectRows} this asserts no lock and tunes no vector search: `$lock` and
|
|
116
|
+
* `$candidates` describe the statement, and `parseRelationQueryValue` refuses both on a relation
|
|
117
|
+
* query, so neither can reach here.
|
|
118
|
+
*/
|
|
119
|
+
async internalFindManyPerParent(entity, q, partition) {
|
|
120
|
+
const ctx = this.dialect.createContext();
|
|
121
|
+
this.dialect.findPerParent(ctx, entity, q, partition);
|
|
122
|
+
return this.hydrateRows(entity, q, await this.all(ctx.sql, ctx.values));
|
|
123
|
+
}
|
|
107
124
|
/**
|
|
108
125
|
* One statement for both: the page carries its own unpaged total in an extra column. An empty page
|
|
109
126
|
* has no row to carry it, which is the one case still needing a count of its own - a `$skip` past
|
|
@@ -150,7 +150,38 @@ export declare class SchemaAST implements ISchemaAST {
|
|
|
150
150
|
indexCount: number;
|
|
151
151
|
};
|
|
152
152
|
/**
|
|
153
|
-
*
|
|
154
|
-
|
|
155
|
-
|
|
153
|
+
* The schema as a plain object, for serialization and debugging. The graph links are what is left
|
|
154
|
+
* out - they are cycles, and nothing else is: listing the fields to keep instead dropped every
|
|
155
|
+
* option a column had gained since, `defaultValue` and `enum` included.
|
|
156
|
+
*/
|
|
157
|
+
toJSON(): {
|
|
158
|
+
tables: {
|
|
159
|
+
name: string;
|
|
160
|
+
columns: {
|
|
161
|
+
name: string;
|
|
162
|
+
type: import("./types.js").CanonicalType;
|
|
163
|
+
nullable: boolean;
|
|
164
|
+
defaultValue?: unknown;
|
|
165
|
+
isPrimaryKey: boolean;
|
|
166
|
+
isAutoIncrement: boolean;
|
|
167
|
+
isUnique: boolean;
|
|
168
|
+
enum?: import("./types.js").EnumValues;
|
|
169
|
+
generatedAs?: string;
|
|
170
|
+
comment?: string;
|
|
171
|
+
}[];
|
|
172
|
+
indexes: {
|
|
173
|
+
name: string;
|
|
174
|
+
columns: string[];
|
|
175
|
+
unique: boolean;
|
|
176
|
+
}[];
|
|
177
|
+
}[];
|
|
178
|
+
relationships: {
|
|
179
|
+
name: string;
|
|
180
|
+
type: RelationshipType;
|
|
181
|
+
from: string;
|
|
182
|
+
to: string;
|
|
183
|
+
onDelete: import("./types.js").ForeignKeyAction | undefined;
|
|
184
|
+
onUpdate: import("./types.js").ForeignKeyAction | undefined;
|
|
185
|
+
}[];
|
|
186
|
+
};
|
|
156
187
|
}
|
package/dist/schema/schemaAST.js
CHANGED
|
@@ -385,20 +385,15 @@ export class SchemaAST {
|
|
|
385
385
|
};
|
|
386
386
|
}
|
|
387
387
|
/**
|
|
388
|
-
*
|
|
388
|
+
* The schema as a plain object, for serialization and debugging. The graph links are what is left
|
|
389
|
+
* out - they are cycles, and nothing else is: listing the fields to keep instead dropped every
|
|
390
|
+
* option a column had gained since, `defaultValue` and `enum` included.
|
|
389
391
|
*/
|
|
390
392
|
toJSON() {
|
|
391
393
|
return {
|
|
392
394
|
tables: Array.from(this.tables.values()).map((t) => ({
|
|
393
395
|
name: t.name,
|
|
394
|
-
columns: Array.from(t.columns.values()).map((
|
|
395
|
-
name: c.name,
|
|
396
|
-
type: c.type,
|
|
397
|
-
nullable: c.nullable,
|
|
398
|
-
isPrimaryKey: c.isPrimaryKey,
|
|
399
|
-
isAutoIncrement: c.isAutoIncrement,
|
|
400
|
-
isUnique: c.isUnique,
|
|
401
|
-
})),
|
|
396
|
+
columns: Array.from(t.columns.values()).map(({ table: _table, referencedBy: _referencedBy, references: _references, ...column }) => column),
|
|
402
397
|
indexes: t.indexes.map((i) => ({
|
|
403
398
|
name: i.name,
|
|
404
399
|
columns: i.entries.map((entry) => entry.column),
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
* - Database introspection results (TableSchema[])
|
|
7
7
|
*/
|
|
8
8
|
import { getMeta, soleIdOf } from '../entity/metadata/definition.js';
|
|
9
|
+
import { ddlText } from '../util/ddlExpression.util.js';
|
|
10
|
+
import { computedExpression, isInlinedExpression } from '../util/field.util.js';
|
|
9
11
|
import { isSoleIdField } from '../util/field.util.js';
|
|
10
12
|
import { isAutoIncrement } from '../util/field.util.js';
|
|
11
13
|
import { derivedForeignKeyName, derivedIndexName, qualifyName } from '../util/sql.util.js';
|
|
@@ -82,8 +84,8 @@ function addTableFromEntity(ctx, meta) {
|
|
|
82
84
|
const field = fields[key];
|
|
83
85
|
if (!field)
|
|
84
86
|
continue;
|
|
85
|
-
//
|
|
86
|
-
if (field
|
|
87
|
+
// An inlined expression has no column; a stored one is a column like any other.
|
|
88
|
+
if (isInlinedExpression(field))
|
|
87
89
|
continue;
|
|
88
90
|
const columnName = ctx.resolveColumnName(key, field);
|
|
89
91
|
const type = resolveColumnCanonicalType(field);
|
|
@@ -99,6 +101,7 @@ function addTableFromEntity(ctx, meta) {
|
|
|
99
101
|
isPrimaryKey,
|
|
100
102
|
isAutoIncrement: isAutoIncrement(field, isSoleKey),
|
|
101
103
|
isUnique: field.unique ?? false,
|
|
104
|
+
generatedAs: ddlText(computedExpression(field), `the computed column '${columnName}'`),
|
|
102
105
|
comment: field.comment,
|
|
103
106
|
enum: field.enum,
|
|
104
107
|
table,
|