uql-orm 0.73.1 → 0.74.1
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/dist/browser/uql-browser.min.js +2 -2
- package/dist/browser/uql-browser.min.js.map +3 -3
- package/dist/dialect/abstractSqlDialect.d.ts +3 -2
- package/dist/dialect/abstractSqlDialect.js +46 -38
- package/dist/dialect/aliases.d.ts +5 -4
- package/dist/dialect/aliases.js +5 -5
- package/dist/dialect/queryJoins.d.ts +14 -1
- package/dist/dialect/queryJoins.js +32 -3
- package/dist/dialect/vectorSqlDialect.d.ts +6 -8
- package/dist/dialect/vectorSqlDialect.js +10 -13
- package/dist/migrate/drift/driftDetector.js +2 -2
- package/dist/migrate/generator/mongoSchemaGenerator.d.ts +1 -0
- package/dist/migrate/generator/mongoSchemaGenerator.js +20 -8
- package/dist/migrate/migrator.js +11 -4
- package/dist/migrate/schemaGenerator.d.ts +6 -9
- package/dist/migrate/schemaGenerator.js +46 -74
- package/dist/mongo/mongoDialect.d.ts +4 -4
- package/dist/mongo/mongoDialect.js +52 -41
- package/dist/mongo/vectorDistance.d.ts +8 -0
- package/dist/mongo/vectorDistance.js +26 -0
- package/dist/schema/indexDifferences.d.ts +11 -1
- package/dist/schema/indexDifferences.js +34 -2
- package/dist/type/dialect.d.ts +4 -1
- package/dist/type/migration.d.ts +2 -1
- package/dist/type/query.d.ts +18 -5
- package/dist/util/dialect.util.d.ts +6 -1
- package/dist/util/dialect.util.js +11 -1
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { findVectorIndex, findVectorSort, vectorCandidates } from '../util/dialect.util.js';
|
|
1
|
+
import { unsupportedVectorMetric } from '../type/vector.js';
|
|
2
|
+
import { findVectorIndex, findVectorSort, vectorCandidates, vectorDistanceOf } from '../util/dialect.util.js';
|
|
3
3
|
import { AbstractDialect } from './abstractDialect.js';
|
|
4
4
|
import { encodeFloat32s } from './vectorCast.js';
|
|
5
5
|
/**
|
|
@@ -44,15 +44,10 @@ export class VectorSqlDialect extends AbstractDialect {
|
|
|
44
44
|
hasVectorIndex() {
|
|
45
45
|
return [...this.vectorMetrics.values()].some((metric) => metric.index);
|
|
46
46
|
}
|
|
47
|
-
/**
|
|
48
|
-
* What a distance expression reads, for a `$sort` and a `$near` alike. The metric falls back to the
|
|
49
|
-
* field's, then its index's, which serves no other, then cosine.
|
|
50
|
-
*/
|
|
47
|
+
/** What a distance expression reads, for a `$sort` and a `$near` alike. */
|
|
51
48
|
resolveVectorDistance(meta, key, search) {
|
|
52
49
|
const field = meta.fields[key];
|
|
53
|
-
|
|
54
|
-
const distance = search.$distance ?? field?.distance ?? findVectorIndex(meta, key)?.distance ?? DEFAULT_VECTOR_DISTANCE;
|
|
55
|
-
return { colName, distance, field };
|
|
50
|
+
return { colName: this.resolveColumnName(key, field), distance: vectorDistanceOf(meta, key, search), field };
|
|
56
51
|
}
|
|
57
52
|
/**
|
|
58
53
|
* Binds a vector, both as a persisted value and as the query vector of a distance expression, so a
|
|
@@ -70,9 +65,10 @@ export class VectorSqlDialect extends AbstractDialect {
|
|
|
70
65
|
}
|
|
71
66
|
/**
|
|
72
67
|
* The distance expression, in whichever of the two shapes this dialect spells it. One method for
|
|
73
|
-
* both, so the metric lookup and its refusal exist once rather than per shape.
|
|
68
|
+
* both, so the metric lookup and its refusal exist once rather than per shape. The column is read
|
|
69
|
+
* under `prefix`, the alias in scope, since a joined table may have a column of the same name.
|
|
74
70
|
*/
|
|
75
|
-
appendVectorDistance(ctx, meta, key, search) {
|
|
71
|
+
appendVectorDistance(ctx, meta, key, search, prefix) {
|
|
76
72
|
if (this.vectorMetrics.size === 0) {
|
|
77
73
|
throw new TypeError(`${this.dialectName} does not support vector similarity search. Use raw() for vector queries.`);
|
|
78
74
|
}
|
|
@@ -81,14 +77,15 @@ export class VectorSqlDialect extends AbstractDialect {
|
|
|
81
77
|
if (!metric) {
|
|
82
78
|
throw unsupportedVectorMetric(this.dialectName, distance);
|
|
83
79
|
}
|
|
80
|
+
const column = this.escapeId(prefix, true, true) + this.escapeId(colName);
|
|
84
81
|
if ('fn' in metric) {
|
|
85
82
|
const leading = metric.metricArg === undefined ? '' : `'${metric.metricArg}', `;
|
|
86
|
-
ctx.append(`${metric.fn}(${leading}${
|
|
83
|
+
ctx.append(`${metric.fn}(${leading}${column}, `);
|
|
87
84
|
this.appendVectorValue(ctx, search.$vector, field);
|
|
88
85
|
ctx.append(')');
|
|
89
86
|
return;
|
|
90
87
|
}
|
|
91
|
-
ctx.append(`${
|
|
88
|
+
ctx.append(`${column} ${metric.op} `);
|
|
92
89
|
this.appendVectorValue(ctx, search.$vector, field);
|
|
93
90
|
}
|
|
94
91
|
}
|
|
@@ -199,7 +199,7 @@ function detectIndexDrifts(diff) {
|
|
|
199
199
|
table: idxDiff.table,
|
|
200
200
|
index: idxDiff.name,
|
|
201
201
|
details: `Index "${idxDiff.name}" exists in database but not defined in entity`,
|
|
202
|
-
suggestion: '
|
|
202
|
+
suggestion: 'Declare it, or drop it via migration: generate:entities drops one uql named',
|
|
203
203
|
});
|
|
204
204
|
}
|
|
205
205
|
else {
|
|
@@ -211,7 +211,7 @@ function detectIndexDrifts(diff) {
|
|
|
211
211
|
table: idxDiff.table,
|
|
212
212
|
index: idxDiff.name,
|
|
213
213
|
details: `Index "${idxDiff.name}" differs from the entity (${idxDiff.description})`,
|
|
214
|
-
suggestion: '
|
|
214
|
+
suggestion: 'Recreate it via migration, which generate:entities writes',
|
|
215
215
|
});
|
|
216
216
|
}
|
|
217
217
|
}
|
|
@@ -33,6 +33,7 @@ export declare class MongoSchemaGenerator extends MongoDialect implements Schema
|
|
|
33
33
|
generateDropTable(tableName: string): string;
|
|
34
34
|
generateAlterTable(diff: SchemaDiff): string[];
|
|
35
35
|
generateAlterTableDown(diff: SchemaDiff): string[];
|
|
36
|
+
private dropIndexCommand;
|
|
36
37
|
/** An index as MongoDB's key spec (`-1` descending, `'text'` full-text), refusing the SQL-only options. */
|
|
37
38
|
generateCreateIndex(tableName: string, index: IndexSchema): string;
|
|
38
39
|
/** An Atlas vector search index: its vector field first, then each field a `$vectorSearch` pre-filters on. */
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { getMeta } from '../../entity/index.js';
|
|
2
2
|
import { MongoDialect } from '../../mongo/mongoDialect.js';
|
|
3
3
|
import { textLanguage } from '../../mongo/textLanguage.js';
|
|
4
|
+
import { indexChanges } from '../../schema/indexDifferences.js';
|
|
4
5
|
import { QueryRaw, } from '../../type/index.js';
|
|
5
6
|
import { indexDistance, unsupportedVectorMetric } from '../../type/vector.js';
|
|
6
7
|
import { declaredIndexes, declaredIndexName, renderIndexColumn } from '../../util/ddlExpression.util.js';
|
|
@@ -8,6 +9,7 @@ import { fulltextConfig, fulltextWeights } from '../../util/dialect.util.js';
|
|
|
8
9
|
import { assertIndexFeatures, assertIndexType } from '../ddl/indexDdl.js';
|
|
9
10
|
import { assertIndexPredicate, refusedIndexPredicate } from '../indexPredicate.js';
|
|
10
11
|
import { renderIndexDefinition } from './definitionToNode.js';
|
|
12
|
+
import { indexNodeToSchema } from './indexNodeToSchema.js';
|
|
11
13
|
import { serializeMongoCommand } from './mongoCommand.js';
|
|
12
14
|
/** The index types a key spec can say, a plain key or `'text'`, and Atlas's vector search index. */
|
|
13
15
|
const MONGO_INDEX_TYPES = new Set(['btree', 'fulltext', 'vectorSearch']);
|
|
@@ -105,12 +107,21 @@ export class MongoSchemaGenerator extends MongoDialect {
|
|
|
105
107
|
return serializeMongoCommand({ action: 'dropCollection', name: tableName });
|
|
106
108
|
}
|
|
107
109
|
generateAlterTable(diff) {
|
|
108
|
-
return
|
|
110
|
+
return [
|
|
111
|
+
...(diff.indexesToDrop ?? []).map((index) => this.dropIndexCommand(diff.tableName, index)),
|
|
112
|
+
...(diff.indexesToAdd ?? []).map((index) => this.generateCreateIndex(diff.tableName, index)),
|
|
113
|
+
];
|
|
109
114
|
}
|
|
110
115
|
generateAlterTableDown(diff) {
|
|
111
|
-
return
|
|
112
|
-
|
|
113
|
-
|
|
116
|
+
return [
|
|
117
|
+
...(diff.indexesToAdd ?? []).map((index) => this.dropIndexCommand(diff.tableName, index)),
|
|
118
|
+
...(diff.indexesToDrop ?? []).map((index) => this.generateCreateIndex(diff.tableName, index)),
|
|
119
|
+
];
|
|
120
|
+
}
|
|
121
|
+
dropIndexCommand(tableName, index) {
|
|
122
|
+
return index.type === 'vectorSearch'
|
|
123
|
+
? serializeMongoCommand({ action: 'dropSearchIndex', collection: tableName, name: index.name })
|
|
124
|
+
: this.generateDropIndex(tableName, index.name);
|
|
114
125
|
}
|
|
115
126
|
/** An index as MongoDB's key spec (`-1` descending, `'text'` full-text), refusing the SQL-only options. */
|
|
116
127
|
generateCreateIndex(tableName, index) {
|
|
@@ -200,15 +211,16 @@ export class MongoSchemaGenerator extends MongoDialect {
|
|
|
200
211
|
if (!currentTable) {
|
|
201
212
|
return { tableName: collectionName, type: 'create' };
|
|
202
213
|
}
|
|
203
|
-
|
|
204
|
-
const
|
|
205
|
-
if (
|
|
214
|
+
// By name: MongoDB lists a text index's fields alphabetically, so a shape would not match its own.
|
|
215
|
+
const { toAdd, toDrop } = indexChanges(collectionName, this.indexesOf(meta, collectionName), currentTable.indexes, (index) => index.name);
|
|
216
|
+
if (!toAdd.length && !toDrop.length) {
|
|
206
217
|
return undefined;
|
|
207
218
|
}
|
|
208
219
|
return {
|
|
209
220
|
tableName: collectionName,
|
|
210
221
|
type: 'alter',
|
|
211
|
-
indexesToAdd,
|
|
222
|
+
indexesToAdd: toAdd.length ? toAdd : undefined,
|
|
223
|
+
indexesToDrop: toDrop.length ? toDrop.map(indexNodeToSchema) : undefined,
|
|
212
224
|
};
|
|
213
225
|
}
|
|
214
226
|
}
|
package/dist/migrate/migrator.js
CHANGED
|
@@ -189,15 +189,16 @@ export class Migrator {
|
|
|
189
189
|
this.logger.logInfo('No schema changes detected.');
|
|
190
190
|
return '';
|
|
191
191
|
}
|
|
192
|
+
// Diff by diff in reverse, each rolled back in the order its generator wrote it.
|
|
192
193
|
const down = [
|
|
193
|
-
...
|
|
194
|
-
...
|
|
194
|
+
...altered.toReversed().flatMap((diff) => generator.generateAlterTableDown(diff)),
|
|
195
|
+
...created.toReversed().map((tableName) => generator.generateDropTable(tableName, { ifExists: true })),
|
|
195
196
|
];
|
|
196
197
|
const { emit } = this.target.source;
|
|
197
198
|
const filePath = await this.writeMigration(name, {
|
|
198
199
|
docExtraLines: ['Generated from entity definitions'],
|
|
199
200
|
upInner: emit(up),
|
|
200
|
-
downInner: emit(down
|
|
201
|
+
downInner: emit(down),
|
|
201
202
|
});
|
|
202
203
|
this.logger.logInfo(`Created migration from entities: ${filePath}`);
|
|
203
204
|
return filePath;
|
|
@@ -345,7 +346,13 @@ export class Migrator {
|
|
|
345
346
|
this.logger.logSkippedMigration(`[AutoSync] Skipped altering ${filteredDiff.foreignKeysToAlter.length} foreign keys in table '${diff.tableName}': ${filteredDiff.foreignKeysToAlter.map((fk) => fk.to.name).join(', ')} (safe mode active). Use a migration or { safe: false } to apply.`);
|
|
346
347
|
delete filteredDiff.foreignKeysToAlter;
|
|
347
348
|
}
|
|
348
|
-
|
|
349
|
+
if (filteredDiff.indexesToDrop?.length) {
|
|
350
|
+
// An index recreated under its old name is a drop and an add, held back together.
|
|
351
|
+
const dropped = new Set(filteredDiff.indexesToDrop.map((index) => index.name));
|
|
352
|
+
this.logger.logSkippedMigration(`[AutoSync] Skipped dropping ${dropped.size} indexes in table '${diff.tableName}': ${[...dropped].join(', ')} (safe mode active). Use a migration or { safe: false } to apply.`);
|
|
353
|
+
filteredDiff.indexesToAdd = filteredDiff.indexesToAdd?.filter((index) => !dropped.has(index.name));
|
|
354
|
+
delete filteredDiff.indexesToDrop;
|
|
355
|
+
}
|
|
349
356
|
delete filteredDiff.foreignKeysToDrop;
|
|
350
357
|
}
|
|
351
358
|
if (!options.drop && filteredDiff.columnsToDrop?.length) {
|
|
@@ -62,12 +62,16 @@ export declare class SqlSchemaGenerator implements SchemaGenerator {
|
|
|
62
62
|
private addForeignKeyStatements;
|
|
63
63
|
/** `DROP CONSTRAINT` for each of `constraintNames`, the mirror of {@link addForeignKeyStatements}. */
|
|
64
64
|
private dropForeignKeyStatements;
|
|
65
|
+
/** The `ALTER COLUMN` restating each of `columns`. */
|
|
66
|
+
private alterColumnStatements;
|
|
67
|
+
/** An index added to a table that may already have rows: its `CREATE`, then what the engine needs after. */
|
|
68
|
+
private addIndexStatements;
|
|
69
|
+
/** `DROP INDEX` for each of `indexes`, the mirror of {@link addIndexStatements}. */
|
|
70
|
+
private dropIndexStatements;
|
|
65
71
|
generateAlterTableDown(diff: SchemaDiff): string[];
|
|
66
72
|
generateCreateIndex(tableName: string, index: IndexSchema, options?: {
|
|
67
73
|
ifNotExists?: boolean;
|
|
68
74
|
}): string;
|
|
69
|
-
/** An index added to a table that may already have rows: its `CREATE`, then what the engine needs after. */
|
|
70
|
-
private addIndexStatements;
|
|
71
75
|
/**
|
|
72
76
|
* `schema` is the table's, because that is where its indexes live. MySQL takes it from the table
|
|
73
77
|
* operand instead, which is already qualified.
|
|
@@ -106,13 +110,6 @@ export declare class SqlSchemaGenerator implements SchemaGenerator {
|
|
|
106
110
|
* drift detection runs, with types normalized as the engine stores them.
|
|
107
111
|
*/
|
|
108
112
|
diffSchema(entity: Type<object>, currentTable: TableNode | undefined, desiredAst?: SchemaAST): SchemaDiff | undefined;
|
|
109
|
-
/**
|
|
110
|
-
* Indexes the entity declares that the table does not already have, in any shape.
|
|
111
|
-
*
|
|
112
|
-
* Additive only: an index the entity does not name may well have been created deliberately outside
|
|
113
|
-
* the ORM, and dropping it is a decision for a reviewed migration.
|
|
114
|
-
*/
|
|
115
|
-
private missingIndexes;
|
|
116
113
|
protected diffOptions(): DiffOptions;
|
|
117
114
|
/** Spread, not copied field by field, so a field the node gains cannot go missing here. */
|
|
118
115
|
private columnNodeToSchema;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getMeta } from '../entity/index.js';
|
|
2
2
|
import { canonicalToSql, engineType, isVectorCategory } from '../schema/canonicalType.js';
|
|
3
|
-
import {
|
|
3
|
+
import { indexChanges } from '../schema/indexDifferences.js';
|
|
4
4
|
import { buildSchemaAST, resolveColumnCanonicalType } from '../schema/schemaASTBuilder.js';
|
|
5
5
|
import { diffRelationshipNodes, diffTable } from '../schema/schemaASTDiffer.js';
|
|
6
6
|
import { isAutoIncrement, qualifyName } from '../util/index.js';
|
|
@@ -144,40 +144,19 @@ export class SqlSchemaGenerator {
|
|
|
144
144
|
...(diff.foreignKeysToDrop ?? []),
|
|
145
145
|
...(diff.foreignKeysToAlter ?? []).map((it) => constraintNameOf(diff.tableName, it.from)),
|
|
146
146
|
]));
|
|
147
|
-
//
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
statements.push(...colStatements);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
// Drop columns
|
|
164
|
-
if (diff.columnsToDrop?.length) {
|
|
165
|
-
for (const columnName of diff.columnsToDrop) {
|
|
166
|
-
statements.push(...this.tableDdl.dropColumn(diff.tableName, columnName));
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
// Add indexes
|
|
170
|
-
if (diff.indexesToAdd?.length) {
|
|
171
|
-
for (const index of diff.indexesToAdd) {
|
|
172
|
-
statements.push(...this.addIndexStatements(diff.tableName, index));
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
// Drop indexes
|
|
176
|
-
if (diff.indexesToDrop?.length) {
|
|
177
|
-
for (const indexName of diff.indexesToDrop) {
|
|
178
|
-
statements.push(this.generateDropIndex(diff.tableName, indexName, diff.schema));
|
|
179
|
-
}
|
|
180
|
-
}
|
|
147
|
+
// Before the adds, which may reuse a dropped index's name, and before the columns: some engines
|
|
148
|
+
// drop an index along with its column, which would leave nothing here to name.
|
|
149
|
+
statements.push(...this.dropIndexStatements(diff.tableName, diff.indexesToDrop, diff.schema));
|
|
150
|
+
for (const column of diff.columnsToAdd ?? []) {
|
|
151
|
+
this.assertColumnAddable(diff.tableName, column);
|
|
152
|
+
statements.push(this.tableDdl.addColumn(diff.tableName, this.generateColumnDefinitionFromSchema(column)));
|
|
153
|
+
statements.push(...this.generateColumnCommentStatement(diff.tableName, column, diff.schema));
|
|
154
|
+
}
|
|
155
|
+
statements.push(...this.alterColumnStatements(diff.tableName, (diff.columnsToAlter ?? []).map((it) => it.to)));
|
|
156
|
+
for (const columnName of diff.columnsToDrop ?? []) {
|
|
157
|
+
statements.push(...this.tableDdl.dropColumn(diff.tableName, columnName));
|
|
158
|
+
}
|
|
159
|
+
statements.push(...this.addIndexStatements(diff.tableName, diff.indexesToAdd));
|
|
181
160
|
// Last, so every column it names exists by now.
|
|
182
161
|
if (diff.primaryKey?.to.length) {
|
|
183
162
|
statements.push(this.generateAddPrimaryKeySql(diff.tableName, diff.primaryKey.to));
|
|
@@ -198,6 +177,21 @@ export class SqlSchemaGenerator {
|
|
|
198
177
|
dropForeignKeyStatements(tableName, constraintNames) {
|
|
199
178
|
return constraintNames.map((name) => this.generateDropForeignKeySql(tableName, name));
|
|
200
179
|
}
|
|
180
|
+
/** The `ALTER COLUMN` restating each of `columns`. */
|
|
181
|
+
alterColumnStatements(tableName, columns) {
|
|
182
|
+
return columns.flatMap((column) => this.generateAlterColumnStatements(tableName, column, this.generateColumnDefinitionFromSchema(column)));
|
|
183
|
+
}
|
|
184
|
+
/** An index added to a table that may already have rows: its `CREATE`, then what the engine needs after. */
|
|
185
|
+
addIndexStatements(tableName, indexes = []) {
|
|
186
|
+
return indexes.flatMap((index) => [
|
|
187
|
+
this.generateCreateIndex(tableName, index),
|
|
188
|
+
...this.indexDdl.settleStatements(tableName, index),
|
|
189
|
+
]);
|
|
190
|
+
}
|
|
191
|
+
/** `DROP INDEX` for each of `indexes`, the mirror of {@link addIndexStatements}. */
|
|
192
|
+
dropIndexStatements(tableName, indexes = [], schema) {
|
|
193
|
+
return indexes.map((index) => this.generateDropIndex(tableName, index.name, schema));
|
|
194
|
+
}
|
|
201
195
|
generateAlterTableDown(diff) {
|
|
202
196
|
const statements = [];
|
|
203
197
|
// Constraints first, mirroring the up direction: the up added them last, so the down drops them
|
|
@@ -212,44 +206,26 @@ export class SqlSchemaGenerator {
|
|
|
212
206
|
if (diff.primaryKey?.to.length) {
|
|
213
207
|
statements.push(this.generateDropPrimaryKeySql(diff.tableName, derivedPrimaryKeyName(diff.tableName, diff.primaryKey.to)));
|
|
214
208
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
for (const column of diff.columnsToAdd) {
|
|
218
|
-
statements.push(...this.tableDdl.dropColumn(diff.tableName, column.name));
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
// Reverse column alterations by restoring original schema
|
|
222
|
-
if (diff.columnsToAlter?.length) {
|
|
223
|
-
for (const { from } of diff.columnsToAlter) {
|
|
224
|
-
const colDef = this.generateColumnDefinitionFromSchema(from);
|
|
225
|
-
const colStatements = this.generateAlterColumnStatements(diff.tableName, from, colDef);
|
|
226
|
-
statements.push(...colStatements);
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
// Reverse index additions by dropping them
|
|
230
|
-
if (diff.indexesToAdd?.length) {
|
|
231
|
-
for (const index of diff.indexesToAdd) {
|
|
232
|
-
statements.push(this.generateDropIndex(diff.tableName, index.name, diff.schema));
|
|
233
|
-
}
|
|
209
|
+
for (const column of diff.columnsToAdd ?? []) {
|
|
210
|
+
statements.push(...this.tableDdl.dropColumn(diff.tableName, column.name));
|
|
234
211
|
}
|
|
212
|
+
statements.push(...this.alterColumnStatements(diff.tableName, (diff.columnsToAlter ?? []).map((it) => it.from)));
|
|
213
|
+
statements.push(...this.dropIndexStatements(diff.tableName, diff.indexesToAdd, diff.schema));
|
|
214
|
+
statements.push(...this.addIndexStatements(diff.tableName, diff.indexesToDrop));
|
|
235
215
|
if (diff.primaryKey?.from.length) {
|
|
236
216
|
statements.push(this.generateAddPrimaryKeySql(diff.tableName, diff.primaryKey.from, diff.primaryKey.fromName));
|
|
237
217
|
}
|
|
238
218
|
// The constraint the up replaced, back under the name the database had for it. A foreign key the
|
|
239
219
|
// up *dropped* is not restored: only its name survived the diff, never what it pointed at.
|
|
240
220
|
statements.push(...this.addForeignKeyStatements(diff.tableName, (diff.foreignKeysToAlter ?? []).map((it) => it.from)));
|
|
241
|
-
if (diff.columnsToDrop?.length || diff.
|
|
242
|
-
statements.push(`-- TODO: Manual reversal needed for dropped columns/
|
|
221
|
+
if (diff.columnsToDrop?.length || diff.foreignKeysToDrop?.length) {
|
|
222
|
+
statements.push(`-- TODO: Manual reversal needed for dropped columns/foreign keys`);
|
|
243
223
|
}
|
|
244
224
|
return statements;
|
|
245
225
|
}
|
|
246
226
|
generateCreateIndex(tableName, index, options = {}) {
|
|
247
227
|
return this.indexDdl.getCreateIndexStatement(tableName, index, options);
|
|
248
228
|
}
|
|
249
|
-
/** An index added to a table that may already have rows: its `CREATE`, then what the engine needs after. */
|
|
250
|
-
addIndexStatements(tableName, index) {
|
|
251
|
-
return [this.generateCreateIndex(tableName, index), ...this.indexDdl.settleStatements(tableName, index)];
|
|
252
|
-
}
|
|
253
229
|
/**
|
|
254
230
|
* `schema` is the table's, because that is where its indexes live. MySQL takes it from the table
|
|
255
231
|
* operand instead, which is already qualified.
|
|
@@ -351,10 +327,12 @@ export class SqlSchemaGenerator {
|
|
|
351
327
|
}
|
|
352
328
|
// Indexes are matched here rather than by the differ, which pairs them by name so that a changed
|
|
353
329
|
// one reads as one index that altered. A migration needs the opposite: an index already in the
|
|
354
|
-
// table, under whatever name, must not be created again, and one whose shape differs is
|
|
355
|
-
//
|
|
330
|
+
// table, under whatever name, must not be created again, and one whose shape differs is dropped
|
|
331
|
+
// and created anew - no engine alters an index's columns or uniqueness.
|
|
356
332
|
const tableDiff = diffTable(desired, currentTable, { ...this.diffOptions(), compareIndexes: false });
|
|
357
|
-
const
|
|
333
|
+
const indexes = indexChanges(currentTable.name, desired.indexes, currentTable.indexes);
|
|
334
|
+
const indexesToAdd = indexes.toAdd.map(indexNodeToSchema);
|
|
335
|
+
const indexesToDrop = indexes.toDrop.map(indexNodeToSchema);
|
|
358
336
|
const columnDiffs = tableDiff?.columnDiffs ?? [];
|
|
359
337
|
const columnsToAdd = columnDiffs.flatMap((it) => (it.type === 'add' ? [this.columnNodeToSchema(it.expected)] : []));
|
|
360
338
|
const columnsToDrop = columnDiffs.flatMap((it) => (it.type === 'drop' ? [it.column] : []));
|
|
@@ -387,6 +365,7 @@ export class SqlSchemaGenerator {
|
|
|
387
365
|
!columnsToAlter.length &&
|
|
388
366
|
!columnsToDrop.length &&
|
|
389
367
|
!indexesToAdd.length &&
|
|
368
|
+
!indexesToDrop.length &&
|
|
390
369
|
!foreignKeysToAdd.length &&
|
|
391
370
|
!foreignKeysToDrop.length &&
|
|
392
371
|
!foreignKeysToAlter.length &&
|
|
@@ -402,21 +381,12 @@ export class SqlSchemaGenerator {
|
|
|
402
381
|
columnsToAlter: columnsToAlter.length ? columnsToAlter : undefined,
|
|
403
382
|
columnsToDrop: columnsToDrop.length ? columnsToDrop : undefined,
|
|
404
383
|
indexesToAdd: indexesToAdd.length ? indexesToAdd : undefined,
|
|
384
|
+
indexesToDrop: indexesToDrop.length ? indexesToDrop : undefined,
|
|
405
385
|
foreignKeysToAdd: foreignKeysToAdd.length ? foreignKeysToAdd : undefined,
|
|
406
386
|
foreignKeysToDrop: foreignKeysToDrop.length ? foreignKeysToDrop : undefined,
|
|
407
387
|
foreignKeysToAlter: foreignKeysToAlter.length ? foreignKeysToAlter : undefined,
|
|
408
388
|
};
|
|
409
389
|
}
|
|
410
|
-
/**
|
|
411
|
-
* Indexes the entity declares that the table does not already have, in any shape.
|
|
412
|
-
*
|
|
413
|
-
* Additive only: an index the entity does not name may well have been created deliberately outside
|
|
414
|
-
* the ORM, and dropping it is a decision for a reviewed migration.
|
|
415
|
-
*/
|
|
416
|
-
missingIndexes(desired, currentTable) {
|
|
417
|
-
const present = new Set(currentTable.indexes.map(indexSignature));
|
|
418
|
-
return desired.indexes.filter((index) => !present.has(indexSignature(index))).map(indexNodeToSchema);
|
|
419
|
-
}
|
|
420
390
|
diffOptions() {
|
|
421
391
|
return {
|
|
422
392
|
normalizeType: engineType(this.dialect),
|
|
@@ -563,7 +533,9 @@ export class SqlSchemaGenerator {
|
|
|
563
533
|
case 'alterColumn':
|
|
564
534
|
return this.generateAlterColumnSql(operation.tableName, operation.columnName, operation.changes);
|
|
565
535
|
case 'createIndex':
|
|
566
|
-
return this.addIndexStatements(operation.tableName,
|
|
536
|
+
return this.addIndexStatements(operation.tableName, [
|
|
537
|
+
renderIndexDefinition(operation.index, (sql) => this.dialect.compileDdl(sql)),
|
|
538
|
+
]);
|
|
567
539
|
case 'dropIndex':
|
|
568
540
|
return [this.generateDropIndex(operation.tableName, operation.indexName)];
|
|
569
541
|
case 'addForeignKey':
|
|
@@ -130,11 +130,11 @@ export declare class MongoDialect extends AbstractDialect {
|
|
|
130
130
|
/** Walks `$sort` against the metadata of the entity each level addresses, as the SQL dialects do. */
|
|
131
131
|
private collectSort;
|
|
132
132
|
/**
|
|
133
|
-
* The stages a `$sort` by a relation's
|
|
134
|
-
* per parent, and the `$set` that lifts
|
|
135
|
-
* orders by. A parent with no related row
|
|
133
|
+
* The stages a `$sort` by a relation's aggregate needs - its size, or its row nearest a vector: one
|
|
134
|
+
* correlated `$lookup` reading it per parent, and the `$set` that lifts it onto the document as the
|
|
135
|
+
* field the `$sort` then orders by. A parent with no related row reads a zero tally and no distance.
|
|
136
136
|
*/
|
|
137
|
-
|
|
137
|
+
sortAggregateStages<E extends Document>(entity: Type<E>, sort: QuerySortMap<E> | undefined): {
|
|
138
138
|
readonly stages: MongoAggregationPipelineEntry<Document>[];
|
|
139
139
|
readonly fields: string[];
|
|
140
140
|
};
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { ObjectId } from 'mongodb';
|
|
2
2
|
import { AbstractDialect } from '../dialect/abstractDialect.js';
|
|
3
|
-
import { AGGREGATE_VALUE_ALIAS, REL_NESTED_KEY, REL_TEMP_PREFIX, SUM_COUNT_ALIAS,
|
|
4
|
-
import { aggregateColumnField, groupPathField, resolveGroupJoins, resolveQueryJoins, resolveSortableJoin, } from '../dialect/queryJoins.js';
|
|
3
|
+
import { AGGREGATE_VALUE_ALIAS, REL_NESTED_KEY, REL_TEMP_PREFIX, SUM_COUNT_ALIAS, sortAggregateField, TEXT_SCORE_ALIAS, } from '../dialect/aliases.js';
|
|
4
|
+
import { aggregateColumnField, groupPathField, resolveGroupJoins, relationSortTerms, resolveQueryJoins, resolveSortableJoin, } from '../dialect/queryJoins.js';
|
|
5
5
|
import { assertSoleId, fieldOf, getMeta, relationOf, soleIdOf } from '../entity/index.js';
|
|
6
6
|
import { COUNT_RESULT_KEY } from '../type/query.js';
|
|
7
7
|
import { QueryRaw } from '../type/queryRaw.js';
|
|
8
|
-
import { aggregateOf, asSelectMap, assertAggregateColumns, assertNonNegativeInteger, columnFamily, countedRelations, entityName, fieldUpdateOf, fillOnFields, filterFieldKeys, findVectorIndex, findVectorSort, getKeys, getRelationRequestSummary, hasKeys, isFieldUpdateOp, isJsonObject, isJsonUpdateOp, isOperatorMap, isOperatorObject, isRecord, isVectorSearch, normalizeScalarFieldSelection, parentJoins, parseGroupMap, parseRelationAtKey, parseRelationSize,
|
|
8
|
+
import { aggregateOf, asSelectMap, assertAggregateColumns, assertNonNegativeInteger, columnFamily, countedRelations, entityName, fieldUpdateOf, fillOnFields, filterFieldKeys, findVectorIndex, findVectorSort, getKeys, getRelationRequestSummary, hasKeys, isFieldUpdateOp, isJsonObject, isJsonUpdateOp, isOperatorMap, isOperatorObject, isRecord, isVectorSearch, normalizeScalarFieldSelection, parentJoins, parseGroupMap, parseRelationAtKey, parseRelationSize, rankedTextSearch, someKey, targetKeyColumns, textSortOf, vectorDistanceOf, } from '../util/index.js';
|
|
9
9
|
import { decodeBigIntsExcept } from '../util/wideNumber.js';
|
|
10
10
|
import { textLanguage } from './textLanguage.js';
|
|
11
|
+
import { vectorDistanceExpr } from './vectorDistance.js';
|
|
11
12
|
/** A scalar field's operator as the aggregation operator computing it. */
|
|
12
13
|
const MONGO_ARITHMETIC = { $inc: '$add', $mul: '$multiply' };
|
|
13
14
|
/** Default {@link DialectFeatures} for MongoDB. */
|
|
@@ -461,7 +462,8 @@ export class MongoDialect extends AbstractDialect {
|
|
|
461
462
|
*/
|
|
462
463
|
sort(entity, { $sort: sort, $populate: populate, $where: where }) {
|
|
463
464
|
const meta = getMeta(entity);
|
|
464
|
-
const
|
|
465
|
+
const nearest = {};
|
|
466
|
+
const columns = {};
|
|
465
467
|
// Refused as the SQL dialects refuse it, before MongoDB answers a missing score with its own error.
|
|
466
468
|
if (sort?.$text) {
|
|
467
469
|
rankedTextSearch(where);
|
|
@@ -469,11 +471,13 @@ export class MongoDialect extends AbstractDialect {
|
|
|
469
471
|
// The same join set the lookups are built from, so what an ordering may address and what the
|
|
470
472
|
// pipeline actually produces cannot drift apart - `$sort` contributes its own to-one joins here
|
|
471
473
|
// exactly as it does on the SQL dialects.
|
|
472
|
-
|
|
473
|
-
|
|
474
|
+
const joins = resolveQueryJoins(meta, { $populate: populate, $sort: sort });
|
|
475
|
+
this.collectSort(meta, sort, joins, '', nearest, columns);
|
|
476
|
+
// A vector distance is the primary ordering wherever it appears, as on the SQL dialects.
|
|
477
|
+
return { ...nearest, ...columns };
|
|
474
478
|
}
|
|
475
479
|
/** Walks `$sort` against the metadata of the entity each level addresses, as the SQL dialects do. */
|
|
476
|
-
collectSort(meta, sort, joins, path, out) {
|
|
480
|
+
collectSort(meta, sort, joins, path, nearest, out) {
|
|
477
481
|
for (const [key, value] of Object.entries(sort ?? {})) {
|
|
478
482
|
const relation = meta.relations[key];
|
|
479
483
|
if (key === '$text') {
|
|
@@ -485,12 +489,11 @@ export class MongoDialect extends AbstractDialect {
|
|
|
485
489
|
continue;
|
|
486
490
|
}
|
|
487
491
|
if (!relation) {
|
|
488
|
-
// The queried entity's
|
|
489
|
-
//
|
|
490
|
-
//
|
|
491
|
-
// vector column instead, which is the SQL dialects' rejection turned into a silent answer.
|
|
492
|
+
// The queried entity's first vector search is lifted out into `$vectorSearch` before this walk,
|
|
493
|
+
// so one reaching it is a second. `sortDirection` would read the operator object as "ascending"
|
|
494
|
+
// and order by the raw vector column instead, a silent answer where the caller asked for a rank.
|
|
492
495
|
if (isVectorSearch(value)) {
|
|
493
|
-
throw new TypeError(
|
|
496
|
+
throw new TypeError(`cannot $sort by a second vector '${key}' on MongoDB: $vectorSearch ranks by one`);
|
|
494
497
|
}
|
|
495
498
|
out[path + this.pathOf(meta, key)] = sortDirection(value);
|
|
496
499
|
continue;
|
|
@@ -499,39 +502,38 @@ export class MongoDialect extends AbstractDialect {
|
|
|
499
502
|
// one: ordering by a relation nothing looked up reads a field that is not there, which MongoDB
|
|
500
503
|
// ranks as all-equal rather than rejecting. The SQL dialects can add the join themselves.
|
|
501
504
|
const relPath = `${path}${key}`;
|
|
502
|
-
const
|
|
503
|
-
|
|
504
|
-
// The
|
|
505
|
+
const { aggregates, rest } = relationSortTerms(key, relPath, value);
|
|
506
|
+
for (const { spec, direction } of aggregates) {
|
|
507
|
+
// The value rides on a field {@link sortAggregateStages} adds, which only the queried entity's
|
|
505
508
|
// own pipeline has: a nested one is built inside its parent's `$lookup`, where there is no
|
|
506
509
|
// parent document left to hang it off.
|
|
507
510
|
if (path) {
|
|
508
|
-
throw new TypeError(`$sort by '${relPath}.$count' is only supported on the queried entity`);
|
|
511
|
+
throw new TypeError(`$sort by '${relPath}.${spec.field ?? '$count'}' is only supported on the queried entity`);
|
|
512
|
+
}
|
|
513
|
+
if (spec.search) {
|
|
514
|
+
nearest[sortAggregateField(spec)] = 1;
|
|
515
|
+
}
|
|
516
|
+
else {
|
|
517
|
+
out[sortAggregateField(spec)] = sortDirection(direction);
|
|
509
518
|
}
|
|
510
|
-
|
|
519
|
+
}
|
|
520
|
+
if (rest === undefined) {
|
|
511
521
|
continue;
|
|
512
522
|
}
|
|
513
|
-
const { join, sort: relationSort } = resolveSortableJoin(relation, relPath,
|
|
514
|
-
this.collectSort(join.meta, relationSort, joins, `${relPath}.`, out);
|
|
523
|
+
const { join, sort: relationSort } = resolveSortableJoin(relation, relPath, rest, joins, `cannot $sort by relation '${relPath}' on MongoDB unless it is populated: only $populate adds its fields to the document`);
|
|
524
|
+
this.collectSort(join.meta, relationSort, joins, `${relPath}.`, nearest, out);
|
|
515
525
|
}
|
|
516
526
|
}
|
|
517
527
|
/**
|
|
518
|
-
* The stages a `$sort` by a relation's
|
|
519
|
-
* per parent, and the `$set` that lifts
|
|
520
|
-
* orders by. A parent with no related row
|
|
528
|
+
* The stages a `$sort` by a relation's aggregate needs - its size, or its row nearest a vector: one
|
|
529
|
+
* correlated `$lookup` reading it per parent, and the `$set` that lifts it onto the document as the
|
|
530
|
+
* field the `$sort` then orders by. A parent with no related row reads a zero tally and no distance.
|
|
521
531
|
*/
|
|
522
|
-
|
|
532
|
+
sortAggregateStages(entity, sort) {
|
|
523
533
|
const meta = getMeta(entity);
|
|
524
|
-
const
|
|
525
|
-
const fields =
|
|
526
|
-
|
|
527
|
-
const relOpts = meta.relations[key];
|
|
528
|
-
if (!relOpts || parseSortByCount(value) === undefined) {
|
|
529
|
-
continue;
|
|
530
|
-
}
|
|
531
|
-
const temp = sortCountField(key);
|
|
532
|
-
stages.push(...this.aggregateStages(meta, { relation: key, op: '$count' }, `${REL_TEMP_PREFIX}${temp}`, temp));
|
|
533
|
-
fields.push(temp);
|
|
534
|
-
}
|
|
534
|
+
const specs = Object.entries(sort ?? {}).flatMap(([key, value]) => meta.relations[key] ? relationSortTerms(key, key, value).aggregates.map(({ spec }) => spec) : []);
|
|
535
|
+
const fields = specs.map(sortAggregateField);
|
|
536
|
+
const stages = specs.flatMap((spec, index) => this.aggregateStages(meta, spec, `${REL_TEMP_PREFIX}${fields[index]}`, fields[index]));
|
|
535
537
|
return { stages, fields };
|
|
536
538
|
}
|
|
537
539
|
/** Whether a read answers with a relation aggregate, which only the pipeline can build. */
|
|
@@ -568,15 +570,24 @@ export class MongoDialect extends AbstractDialect {
|
|
|
568
570
|
*/
|
|
569
571
|
aggregateStages(meta, spec, temp, field) {
|
|
570
572
|
const relOpts = relationOf(meta, spec.relation);
|
|
573
|
+
const relMeta = getMeta(relOpts.entity());
|
|
571
574
|
const page = spec.page ?? {};
|
|
575
|
+
// A many-to-many's lookup runs over its junction's rows, each carrying its target: read as that
|
|
576
|
+
// target, so the page's order and the aggregate reach the target's own fields.
|
|
577
|
+
const targets = relOpts.through ? [{ $replaceRoot: { newRoot: { $arrayElemAt: [`$${REL_NESTED_KEY}`, 0] } } }] : [];
|
|
572
578
|
const tail = [
|
|
579
|
+
...targets,
|
|
573
580
|
...(page.$sort ? [{ $sort: this.sort(relOpts.entity(), page) }] : []),
|
|
574
581
|
...this.pagerStages(page),
|
|
575
582
|
spec.field
|
|
576
583
|
? {
|
|
577
584
|
$group: {
|
|
578
585
|
_id: null,
|
|
579
|
-
[AGGREGATE_VALUE_ALIAS]: {
|
|
586
|
+
[AGGREGATE_VALUE_ALIAS]: {
|
|
587
|
+
[spec.op]: spec.search
|
|
588
|
+
? vectorDistanceExpr(this.columnOf(relMeta, spec.field), spec.search.$vector, vectorDistanceOf(relMeta, spec.field, spec.search))
|
|
589
|
+
: `$${this.columnOf(relMeta, spec.field)}`,
|
|
590
|
+
},
|
|
580
591
|
},
|
|
581
592
|
}
|
|
582
593
|
: { $count: AGGREGATE_VALUE_ALIAS },
|
|
@@ -708,10 +719,10 @@ export class MongoDialect extends AbstractDialect {
|
|
|
708
719
|
readStages(entity, q, extra = {}) {
|
|
709
720
|
const meta = getMeta(entity);
|
|
710
721
|
const joins = resolveQueryJoins(meta, q);
|
|
711
|
-
// The
|
|
722
|
+
// The value an ordering by a relation's aggregate reads, and the field it parks it on: both belong
|
|
712
723
|
// with the lookups, since the `$sort` right after them is what they exist for.
|
|
713
|
-
const
|
|
714
|
-
const lookups = [...this.lookupStages(meta, joins), ...
|
|
724
|
+
const aggregated = this.sortAggregateStages(entity, q.$sort);
|
|
725
|
+
const lookups = [...this.lookupStages(meta, joins), ...aggregated.stages];
|
|
715
726
|
// Each to-many and each `$count`, which neither drop nor reorder a row, so they read the page alone.
|
|
716
727
|
const related = this.relationReadStages(entity, q);
|
|
717
728
|
const sort = hasKeys(extra.sort) ? [{ $sort: extra.sort }] : [];
|
|
@@ -732,13 +743,13 @@ export class MongoDialect extends AbstractDialect {
|
|
|
732
743
|
// which is the one way this differs from a SQL join. Taken back out once the `$sort` that needed
|
|
733
744
|
// it has run, so ordering by an unpopulated relation costs the same nothing it does there.
|
|
734
745
|
const sortOnly = [...joins.values()].filter((join) => !join.projected).map((join) => join.path);
|
|
735
|
-
const dropped = [...sortOnly, ...
|
|
746
|
+
const dropped = [...sortOnly, ...aggregated.fields];
|
|
736
747
|
const unset = dropped.length ? [{ $unset: dropped }] : [];
|
|
737
748
|
// The grouping collapses rows onto the columns it projects, which leaves nothing for an ordering
|
|
738
749
|
// that reads a lookup those columns do not carry. Refused rather than answered all-equal, and in
|
|
739
750
|
// the same terms the SQL dialects refuse `SELECT DISTINCT` ordered by an unselected column.
|
|
740
|
-
if (q.$distinct &&
|
|
741
|
-
throw new TypeError(`cannot $sort by a relation's
|
|
751
|
+
if (q.$distinct && aggregated.fields.length) {
|
|
752
|
+
throw new TypeError(`cannot $sort by a relation's aggregate with $distinct: the grouping keeps only the columns it projects`);
|
|
742
753
|
}
|
|
743
754
|
if (q.$distinct && sortOnly.length) {
|
|
744
755
|
throw new TypeError(`cannot $sort by relation '${sortOnly[0]}' with $distinct unless '${sortOnly[0]}' is populated: the grouping keeps only the columns it projects`);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Document } from 'mongodb';
|
|
2
|
+
import type { VectorDistance } from '../type/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* A document's distance from `vector` as an aggregation expression, computed exactly as the SQL engines
|
|
5
|
+
* compute theirs: Atlas ranks only through its own index, which no related document reaches. `null` where
|
|
6
|
+
* the field holds no vector, or a cosine has a zero-length side, and `$min` skips a `null`.
|
|
7
|
+
*/
|
|
8
|
+
export declare function vectorDistanceExpr(column: string, vector: readonly number[], metric: VectorDistance): Document;
|