uql-orm 0.42.0 → 0.43.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/dist/browser/querier/httpQuerier.d.ts +3 -3
- package/dist/browser/type/clientQuerier.d.ts +3 -3
- package/dist/browser/uql-browser.min.js +2 -2
- package/dist/browser/uql-browser.min.js.map +5 -5
- package/dist/dialect/abstractSqlDialect.d.ts +41 -14
- package/dist/dialect/abstractSqlDialect.js +75 -44
- package/dist/dialect/jsonSql.d.ts +3 -2
- package/dist/dialect/jsonSql.js +7 -5
- package/dist/dialect/mysqlLikeSqlDialect.d.ts +2 -1
- package/dist/dialect/mysqlLikeSqlDialect.js +3 -1
- package/dist/dialect/pgLikeSqlDialect.d.ts +1 -1
- package/dist/dialect/pgLikeSqlDialect.js +2 -1
- package/dist/dialect/vectorCast.d.ts +0 -6
- package/dist/dialect/vectorCast.js +0 -8
- package/dist/entity/decorator/entity.d.ts +1 -1
- package/dist/entity/decorator/entity.js +1 -1
- package/dist/entity/decorator/members.d.ts +5 -2
- package/dist/entity/metadata/definition.js +29 -12
- package/dist/maria/mariaDialect.js +4 -3
- package/dist/migrate/builder/tableBuilder.js +5 -4
- package/dist/migrate/drift/driftDetector.js +21 -1
- package/dist/migrate/generator/mongoSchemaGenerator.d.ts +8 -1
- package/dist/migrate/generator/mongoSchemaGenerator.js +25 -29
- package/dist/migrate/introspection/abstractSqlSchemaIntrospector.d.ts +9 -1
- package/dist/migrate/introspection/abstractSqlSchemaIntrospector.js +11 -2
- package/dist/migrate/introspection/baseSqlIntrospector.js +6 -3
- package/dist/migrate/introspection/postgresIntrospector.js +1 -1
- package/dist/migrate/introspection/sqliteIntrospector.js +7 -3
- package/dist/migrate/migrator.js +6 -0
- package/dist/migrate/schemaGenerator.d.ts +43 -37
- package/dist/migrate/schemaGenerator.js +163 -150
- package/dist/mongo/mongoDialect.js +22 -8
- package/dist/postgres/postgresDialect.js +1 -1
- package/dist/querier/abstractQuerier.js +18 -7
- package/dist/querier/relationCount.js +9 -7
- package/dist/schema/canonicalType.d.ts +19 -4
- package/dist/schema/canonicalType.js +114 -164
- package/dist/schema/indexDifferences.d.ts +28 -0
- package/dist/schema/indexDifferences.js +46 -0
- package/dist/schema/schemaASTBuilder.js +27 -33
- package/dist/schema/schemaASTDiffer.d.ts +27 -1
- package/dist/schema/schemaASTDiffer.js +59 -19
- package/dist/schema/types.d.ts +46 -7
- package/dist/sqlite/sqliteDialect.d.ts +2 -1
- package/dist/sqlite/sqliteDialect.js +4 -1
- package/dist/type/dialect.d.ts +6 -0
- package/dist/type/entity.d.ts +23 -6
- package/dist/type/migration.d.ts +20 -1
- package/dist/type/query.d.ts +2 -6
- package/dist/util/field.util.d.ts +28 -7
- package/dist/util/field.util.js +56 -48
- package/dist/util/fieldOption.util.d.ts +79 -0
- package/dist/util/fieldOption.util.js +84 -0
- package/dist/util/index.d.ts +1 -0
- package/dist/util/index.js +1 -0
- package/dist/util/object.util.js +11 -3
- package/dist/util/relationQuery.util.d.ts +10 -0
- package/dist/util/relationQuery.util.js +22 -2
- package/dist/util/sql.util.d.ts +28 -7
- package/dist/util/sql.util.js +79 -10
- package/package.json +2 -2
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
import { getMeta, soleIdOf } from '../entity/index.js';
|
|
2
2
|
import { parseQueryLock, QueryRaw, RAW_ALIAS, RAW_VALUE, VECTOR_QUERY_KEYS, } from '../type/index.js';
|
|
3
|
-
import { asSelectMap, assertNonNegativeInteger, buildQueryWhereAsMap, escapeSqlId, fillOnFields, filterFieldKeys, getInsertFieldKeys, getKeys, getSoftDeleteValue, hasKeys,
|
|
3
|
+
import { asSelectMap, assertNonNegativeInteger, buildQueryWhereAsMap, escapeSqlId, fillOnFields, filterFieldKeys, getInsertFieldKeys, getKeys, getSoftDeleteValue, hasKeys, columnFamily, isJsonUpdateOp, isOperatorMap, isOperatorObject, isOperatorOnlyObject, isVectorSearch, normalizeScalarFieldSelection, parentJoins, targetKeyColumns, parseGroupMap, parseRelationSize, parseSortByCount, populatesRelations, raw, someValue, throwUnknownAggregateColumn, withoutSoftDeleteFilter, } from '../util/index.js';
|
|
4
4
|
import { escapeAnsiSqlLiteral, escapeSingleQuotes } from '../util/sqlLiteral.js';
|
|
5
5
|
import { COUNT_ALIAS, DISTINCT_DERIVED_ALIAS, JSON_ELEM_ALIAS_PREFIX } from './aliases.js';
|
|
6
6
|
import { buildElemMatchConditions } from './jsonArrayElemMatchUtils.js';
|
|
7
7
|
import { isJsonbOp, jsonCompareMode, jsonElemExists } from './jsonSql.js';
|
|
8
8
|
import { SqlQueryContext } from './queryContext.js';
|
|
9
9
|
import { NO_JOINS, resolveQueryJoins, resolveSortableJoin, } from './queryJoins.js';
|
|
10
|
-
import {
|
|
10
|
+
import { resolveVectorCast } from './vectorCast.js';
|
|
11
11
|
import { VectorSqlDialect } from './vectorSqlDialect.js';
|
|
12
12
|
export class AbstractSqlDialect extends VectorSqlDialect {
|
|
13
|
+
/**
|
|
14
|
+
* Whether {@link serialType} states `PRIMARY KEY` itself, so the table must not state it again.
|
|
15
|
+
*
|
|
16
|
+
* True on SQLite alone, where `AUTOINCREMENT` is legal only in the exact phrase
|
|
17
|
+
* `INTEGER PRIMARY KEY AUTOINCREMENT` - the key cannot be lifted out of the column there.
|
|
18
|
+
*/
|
|
19
|
+
serialDeclaresPrimaryKey = false;
|
|
13
20
|
/**
|
|
14
21
|
* How this engine declares a namespace, so a generated migration creates the schemas its tables
|
|
15
22
|
* need before creating them. Only reached where {@link DialectFeatures.schemas} is on. MySQL and
|
|
@@ -22,6 +29,11 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
22
29
|
alterColumnStrategy = 'single-statement';
|
|
23
30
|
alterColumnSyntax = 'ALTER COLUMN';
|
|
24
31
|
dropForeignKeySyntax = 'DROP CONSTRAINT';
|
|
32
|
+
/**
|
|
33
|
+
* `DROP CONSTRAINT <name>` where a primary key is a named constraint like any other; MySQL spells
|
|
34
|
+
* it `DROP PRIMARY KEY` and takes no name, since a table's key is always called `PRIMARY` there.
|
|
35
|
+
*/
|
|
36
|
+
dropPrimaryKeySyntax = 'DROP CONSTRAINT';
|
|
25
37
|
dropIndexSyntax = 'standalone';
|
|
26
38
|
renameTableSyntax = 'alter-table';
|
|
27
39
|
booleanLiteral = 'native';
|
|
@@ -90,9 +102,22 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
90
102
|
placeholder(_index) {
|
|
91
103
|
return '?';
|
|
92
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* `RETURNING <id column> AS id`, or nothing at all for a composite key.
|
|
107
|
+
*
|
|
108
|
+
* The alias names one column, and every column of a composite came from the caller, so there is no
|
|
109
|
+
* id the statement could report that the payload does not already carry - the same "no id to give"
|
|
110
|
+
* a `firstId` dialect already answers with. Empty rather than a refusal, so an insert and an upsert
|
|
111
|
+
* of a composite row both run; `idOf(meta, row)` names such a row.
|
|
112
|
+
*/
|
|
93
113
|
returningId(meta) {
|
|
94
|
-
const
|
|
95
|
-
return `RETURNING ${
|
|
114
|
+
const expression = this.returningIdExpression(meta);
|
|
115
|
+
return expression ? `RETURNING ${expression}` : '';
|
|
116
|
+
}
|
|
117
|
+
/** `<id column> AS id` on its own, for a statement composing a `RETURNING` list of several items. */
|
|
118
|
+
returningIdExpression(meta) {
|
|
119
|
+
const [idKey] = meta.ids;
|
|
120
|
+
return meta.ids.length === 1 ? `${this.escapeId(this.columnOf(meta, idKey))} ${this.escapeId('id')}` : '';
|
|
96
121
|
}
|
|
97
122
|
search(ctx, entity, q = {}, opts = {}, joins = NO_JOINS) {
|
|
98
123
|
const meta = getMeta(entity);
|
|
@@ -965,12 +990,13 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
965
990
|
insert(ctx, entity, payload, opts) {
|
|
966
991
|
this.appendInsertValues(ctx, entity, payload);
|
|
967
992
|
// Every engine whose ids come back from the statement itself wants the same clause, so it is
|
|
968
|
-
// appended once here instead of in an identical `insert` override per dialect.
|
|
969
|
-
//
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
993
|
+
// appended once here instead of in an identical `insert` override per dialect. `returningId` is
|
|
994
|
+
// empty on a composite key, which has no id to ask for.
|
|
995
|
+
if (this.insertIdSource === 'returning') {
|
|
996
|
+
const returning = this.returningId(getMeta(entity));
|
|
997
|
+
if (returning) {
|
|
998
|
+
ctx.append(` ${returning}`);
|
|
999
|
+
}
|
|
974
1000
|
}
|
|
975
1001
|
}
|
|
976
1002
|
/**
|
|
@@ -1063,14 +1089,19 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
1063
1089
|
* {@link PgLikeSqlDialect} overrides this: `$N` placeholders make array order irrelevant, so it can
|
|
1064
1090
|
* bind into the main context and skip the second one.
|
|
1065
1091
|
*/
|
|
1066
|
-
upsert(ctx, entity, conflictPaths, payload,
|
|
1092
|
+
upsert(ctx, entity, conflictPaths, payload,
|
|
1093
|
+
/** One more `RETURNING` item, as a bare expression: this joins the list and adds the keyword. */
|
|
1094
|
+
extraReturning = '') {
|
|
1067
1095
|
const meta = getMeta(entity);
|
|
1068
1096
|
const updateCtx = this.upsertUpdateBindsInPlace ? ctx : this.createContext();
|
|
1069
1097
|
const update = this.getUpsertUpdateAssignments(updateCtx, meta, conflictPaths, payload, this.upsertExcluded);
|
|
1070
1098
|
const keys = this.getUpsertConflictPathsStr(meta, conflictPaths);
|
|
1071
1099
|
const onConflict = update ? `DO UPDATE SET ${update}` : 'DO NOTHING';
|
|
1100
|
+
// Composed rather than concatenated: a composite key contributes no id item, and a dialect's own
|
|
1101
|
+
// item (Postgres's `_created`) still has to be the *first* thing after the keyword when it is.
|
|
1102
|
+
const returning = [this.returningIdExpression(meta), extraReturning].filter(Boolean).join(', ');
|
|
1072
1103
|
this.appendInsertValues(ctx, entity, payload);
|
|
1073
|
-
ctx.append(` ON CONFLICT (${keys}) ${onConflict}
|
|
1104
|
+
ctx.append(` ON CONFLICT (${keys}) ${onConflict}${returning ? ` RETURNING ${returning}` : ''}`);
|
|
1074
1105
|
if (updateCtx !== ctx) {
|
|
1075
1106
|
ctx.pushValue(...updateCtx.values);
|
|
1076
1107
|
}
|
|
@@ -1156,22 +1187,19 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
1156
1187
|
}
|
|
1157
1188
|
/**
|
|
1158
1189
|
* How a column's values are written. A function of the column, not of the value, so a bulk insert
|
|
1159
|
-
* classifies each column once instead of re-deciding per row: a 20-row, 6-column insert
|
|
1160
|
-
*
|
|
1190
|
+
* classifies each column once instead of re-deciding per row: a 20-row, 6-column insert used to ask
|
|
1191
|
+
* 120 times to get the same six answers.
|
|
1161
1192
|
*/
|
|
1162
1193
|
persistKind(field) {
|
|
1163
|
-
const
|
|
1164
|
-
|
|
1165
|
-
return 'json';
|
|
1166
|
-
}
|
|
1167
|
-
return isVectorFieldType(type) ? 'vector' : 'plain';
|
|
1194
|
+
const family = columnFamily(field?.type);
|
|
1195
|
+
return family === 'json' || family === 'vector' ? family : 'plain';
|
|
1168
1196
|
}
|
|
1169
1197
|
/**
|
|
1170
1198
|
* Which of an entity's columns need decoding on READ, and how: the inverse of {@link persistKind},
|
|
1171
1199
|
* cached per entity for the same reason it classifies per column. A 1000-row read of a 10-field
|
|
1172
|
-
* entity otherwise asks
|
|
1173
|
-
* the same ten answers. Most entities land here for their numeric columns alone, where the
|
|
1174
|
-
* cost is one `typeof` against a value the driver usually decoded already.
|
|
1200
|
+
* entity otherwise asks {@link columnFamily} (which lowercases a string on every call) 10,000 times
|
|
1201
|
+
* to get the same ten answers. Most entities land here for their numeric columns alone, where the
|
|
1202
|
+
* per-row cost is one `typeof` against a value the driver usually decoded already.
|
|
1175
1203
|
*
|
|
1176
1204
|
* Dialect-aware exactly like {@link supportedVectorType}, because it has to be: a `sparsevec` field
|
|
1177
1205
|
* is written as a plain dense vector everywhere but Postgres, so reading it back by the field's own
|
|
@@ -1183,23 +1211,26 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
1183
1211
|
* the field says it was meant as a number; and `type: BigInt` shares BIGINT with `type: Number`, so
|
|
1184
1212
|
* the wire decode has to be undone for it. All are no-ops where the driver already decoded.
|
|
1185
1213
|
*
|
|
1186
|
-
* Classified through the same
|
|
1187
|
-
*
|
|
1188
|
-
*
|
|
1214
|
+
* Classified through the same {@link columnFamily} the rest of the library uses, not against the
|
|
1215
|
+
* constructors: `type` accepts a string logical type for every one of these (`@Field({ type:
|
|
1216
|
+
* 'decimal' })`), and matching `=== Number` alone left those reading back as text.
|
|
1189
1217
|
*/
|
|
1190
1218
|
hydratableFields(entity) {
|
|
1219
|
+
const meta = getMeta(entity);
|
|
1191
1220
|
const cached = this.hydratable.get(entity);
|
|
1192
|
-
|
|
1193
|
-
|
|
1221
|
+
// Against the revision, not merely present: a field added to an entity already read - a content
|
|
1222
|
+
// type the admin extended - would otherwise decode by the list its columns are missing from.
|
|
1223
|
+
if (cached?.[0] === meta.revision) {
|
|
1224
|
+
return cached[1];
|
|
1194
1225
|
}
|
|
1195
1226
|
const decoded = [];
|
|
1196
|
-
for (const [key, field] of Object.entries(
|
|
1227
|
+
for (const [key, field] of Object.entries(meta.fields)) {
|
|
1197
1228
|
const kind = this.hydrateKind(field);
|
|
1198
1229
|
if (kind) {
|
|
1199
1230
|
decoded.push([key, kind]);
|
|
1200
1231
|
}
|
|
1201
1232
|
}
|
|
1202
|
-
this.hydratable.set(entity, decoded);
|
|
1233
|
+
this.hydratable.set(entity, [meta.revision, decoded]);
|
|
1203
1234
|
return decoded;
|
|
1204
1235
|
}
|
|
1205
1236
|
/**
|
|
@@ -1233,26 +1264,26 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
1233
1264
|
/**
|
|
1234
1265
|
* The mirror of {@link persistKind}: what one column decodes as, or nothing if it needs no decode.
|
|
1235
1266
|
*
|
|
1236
|
-
*
|
|
1237
|
-
*
|
|
1238
|
-
* `BigInt` as well as `Number`: swap them and every `type: BigInt` property silently decodes to a
|
|
1239
|
-
* JS number again.
|
|
1267
|
+
* `BigInt` is asked first because it shares the numeric family with `Number`: let the switch answer
|
|
1268
|
+
* it and every `type: BigInt` property silently decodes to a JS number again.
|
|
1240
1269
|
*/
|
|
1241
1270
|
hydrateKind(field) {
|
|
1242
1271
|
const type = field?.type;
|
|
1243
|
-
if (isJsonType(type)) {
|
|
1244
|
-
return 'json';
|
|
1245
|
-
}
|
|
1246
|
-
if (isVectorFieldType(type)) {
|
|
1247
|
-
return this.supportedVectorType(resolveVectorCast(field));
|
|
1248
|
-
}
|
|
1249
|
-
if (isBooleanType(type)) {
|
|
1250
|
-
return 'boolean';
|
|
1251
|
-
}
|
|
1252
1272
|
if (type === BigInt) {
|
|
1253
1273
|
return 'bigint';
|
|
1254
1274
|
}
|
|
1255
|
-
|
|
1275
|
+
switch (columnFamily(type)) {
|
|
1276
|
+
case 'json':
|
|
1277
|
+
return 'json';
|
|
1278
|
+
case 'vector':
|
|
1279
|
+
return this.supportedVectorType(resolveVectorCast(field));
|
|
1280
|
+
case 'boolean':
|
|
1281
|
+
return 'boolean';
|
|
1282
|
+
case 'numeric':
|
|
1283
|
+
return 'number';
|
|
1284
|
+
default:
|
|
1285
|
+
return undefined;
|
|
1286
|
+
}
|
|
1256
1287
|
}
|
|
1257
1288
|
hydratable = new WeakMap();
|
|
1258
1289
|
/** The one type dispatch for a persisted value, over a column kind decided by the caller. */
|
|
@@ -1350,7 +1381,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
1350
1381
|
}
|
|
1351
1382
|
const root = key.slice(0, dotIndex);
|
|
1352
1383
|
const field = meta.fields[root];
|
|
1353
|
-
if (!field ||
|
|
1384
|
+
if (!field || columnFamily(field.type) !== 'json') {
|
|
1354
1385
|
return undefined;
|
|
1355
1386
|
}
|
|
1356
1387
|
const colName = this.resolveColumnName(root, field);
|
|
@@ -45,8 +45,9 @@ export declare function jsonCompareMode(value: unknown): JsonAccessMode;
|
|
|
45
45
|
* operand. An index over a JSON path is only reachable by a comparison that extracts it the same way,
|
|
46
46
|
* so the two have to answer alike - which is why they are one pair over one vocabulary.
|
|
47
47
|
*
|
|
48
|
-
* Reads the type through `util/field.util`'s
|
|
49
|
-
* it through `schema/canonicalType` instead pulls that whole module into every consumer
|
|
48
|
+
* Reads the type through `util/field.util`'s own classifier, which the dialects already carry:
|
|
49
|
+
* resolving it through `schema/canonicalType` instead pulls that whole module into every consumer
|
|
50
|
+
* bundle.
|
|
50
51
|
*/
|
|
51
52
|
export declare function jsonTypeMode(type: FieldType): JsonAccessMode;
|
|
52
53
|
/**
|
package/dist/dialect/jsonSql.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { columnFamily } from '../util/field.util.js';
|
|
2
2
|
import { escapeSingleQuotes } from '../util/sqlLiteral.js';
|
|
3
3
|
/**
|
|
4
4
|
* A `'$.a.b'` JSON path literal, each dot-separated segment escaped. `suffix` appends an accessor
|
|
@@ -62,14 +62,16 @@ export function jsonCompareMode(value) {
|
|
|
62
62
|
* operand. An index over a JSON path is only reachable by a comparison that extracts it the same way,
|
|
63
63
|
* so the two have to answer alike - which is why they are one pair over one vocabulary.
|
|
64
64
|
*
|
|
65
|
-
* Reads the type through `util/field.util`'s
|
|
66
|
-
* it through `schema/canonicalType` instead pulls that whole module into every consumer
|
|
65
|
+
* Reads the type through `util/field.util`'s own classifier, which the dialects already carry:
|
|
66
|
+
* resolving it through `schema/canonicalType` instead pulls that whole module into every consumer
|
|
67
|
+
* bundle.
|
|
67
68
|
*/
|
|
68
69
|
export function jsonTypeMode(type) {
|
|
69
|
-
|
|
70
|
+
const family = columnFamily(type);
|
|
71
|
+
if (family === 'numeric') {
|
|
70
72
|
return 'numeric';
|
|
71
73
|
}
|
|
72
|
-
return
|
|
74
|
+
return family === 'boolean' ? 'json' : 'text';
|
|
73
75
|
}
|
|
74
76
|
/**
|
|
75
77
|
* Whether the operator reads the JSON *value* instead of its text form. The array operators always
|
|
@@ -24,7 +24,7 @@ export declare abstract class MysqlLikeSqlDialect extends AbstractSqlDialect {
|
|
|
24
24
|
estimatedCount<E>(ctx: QueryContext, entity: Type<E>): void;
|
|
25
25
|
/** `OFFSET` is only legal after a `LIMIT` here, so a bare `$skip` needs one. */
|
|
26
26
|
pager(ctx: QueryContext, opts: QueryPager): void;
|
|
27
|
-
readonly
|
|
27
|
+
readonly serialType = "BIGINT UNSIGNED AUTO_INCREMENT";
|
|
28
28
|
readonly escapeIdChar = "`";
|
|
29
29
|
readonly tableOptions = "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4";
|
|
30
30
|
readonly beginTransactionCommand = "START TRANSACTION";
|
|
@@ -32,6 +32,7 @@ export declare abstract class MysqlLikeSqlDialect extends AbstractSqlDialect {
|
|
|
32
32
|
readonly rollbackTransactionCommand = "ROLLBACK";
|
|
33
33
|
readonly isolationLevelStrategy = "set-before";
|
|
34
34
|
readonly dropForeignKeySyntax = "DROP FOREIGN KEY";
|
|
35
|
+
readonly dropPrimaryKeySyntax = "DROP PRIMARY KEY";
|
|
35
36
|
readonly dropIndexSyntax = "on-table";
|
|
36
37
|
readonly renameTableSyntax = "rename-table";
|
|
37
38
|
readonly alterColumnSyntax = "MODIFY COLUMN";
|
|
@@ -31,6 +31,7 @@ export class MysqlLikeSqlDialect extends AbstractSqlDialect {
|
|
|
31
31
|
dropTableCascade: false,
|
|
32
32
|
renameColumn: true,
|
|
33
33
|
foreignKeyAlter: true,
|
|
34
|
+
primaryKeyAlter: true,
|
|
34
35
|
columnComment: true,
|
|
35
36
|
vectorIndexRequiresNotNull: false,
|
|
36
37
|
vectorSupportsLength: false,
|
|
@@ -62,7 +63,7 @@ export class MysqlLikeSqlDialect extends AbstractSqlDialect {
|
|
|
62
63
|
}
|
|
63
64
|
super.pager(ctx, opts);
|
|
64
65
|
}
|
|
65
|
-
|
|
66
|
+
serialType = 'BIGINT UNSIGNED AUTO_INCREMENT';
|
|
66
67
|
escapeIdChar = '`';
|
|
67
68
|
tableOptions = 'ENGINE=InnoDB DEFAULT CHARSET=utf8mb4';
|
|
68
69
|
beginTransactionCommand = 'START TRANSACTION';
|
|
@@ -70,6 +71,7 @@ export class MysqlLikeSqlDialect extends AbstractSqlDialect {
|
|
|
70
71
|
rollbackTransactionCommand = 'ROLLBACK';
|
|
71
72
|
isolationLevelStrategy = 'set-before';
|
|
72
73
|
dropForeignKeySyntax = 'DROP FOREIGN KEY';
|
|
74
|
+
dropPrimaryKeySyntax = 'DROP PRIMARY KEY';
|
|
73
75
|
dropIndexSyntax = 'on-table';
|
|
74
76
|
renameTableSyntax = 'rename-table';
|
|
75
77
|
alterColumnSyntax = 'MODIFY COLUMN';
|
|
@@ -15,7 +15,7 @@ export declare abstract class PgLikeSqlDialect extends AbstractSqlDialect {
|
|
|
15
15
|
/** Default {@link DialectFeatures} for Postgres-wire dialects. */
|
|
16
16
|
protected readonly featureDefaults: DialectFeatures;
|
|
17
17
|
readonly escapeIdChar = "\"";
|
|
18
|
-
readonly
|
|
18
|
+
readonly serialType: string;
|
|
19
19
|
readonly tableOptions = "";
|
|
20
20
|
readonly beginTransactionCommand = "BEGIN";
|
|
21
21
|
readonly commitTransactionCommand = "COMMIT";
|
|
@@ -28,6 +28,7 @@ export class PgLikeSqlDialect extends AbstractSqlDialect {
|
|
|
28
28
|
dropTableCascade: true,
|
|
29
29
|
renameColumn: true,
|
|
30
30
|
foreignKeyAlter: true,
|
|
31
|
+
primaryKeyAlter: true,
|
|
31
32
|
columnComment: false,
|
|
32
33
|
vectorIndexRequiresNotNull: false,
|
|
33
34
|
vectorSupportsLength: true,
|
|
@@ -39,7 +40,7 @@ export class PgLikeSqlDialect extends AbstractSqlDialect {
|
|
|
39
40
|
// under heavy concurrent insert load (writes concentrate on one range); this default still beats
|
|
40
41
|
// `SERIAL` (CockroachDB's `unique_rowid()`, a ~64-bit value that overflows JS's safe-integer
|
|
41
42
|
// range). High-throughput CockroachDB users should override this per-entity with a UUID PK.
|
|
42
|
-
|
|
43
|
+
serialType = 'BIGINT GENERATED BY DEFAULT AS IDENTITY';
|
|
43
44
|
tableOptions = '';
|
|
44
45
|
beginTransactionCommand = 'BEGIN';
|
|
45
46
|
commitTransactionCommand = 'COMMIT';
|
|
@@ -4,12 +4,6 @@
|
|
|
4
4
|
*/
|
|
5
5
|
/** Vector cast types supported by pgvector. */
|
|
6
6
|
export type VectorCast = 'vector' | 'halfvec' | 'sparsevec';
|
|
7
|
-
/**
|
|
8
|
-
* Whether a declared field type is a vector of any width. Every dialect that treats vectors specially
|
|
9
|
-
* has to answer this for all three, not just `vector`: matching that one alone left `halfvec` and
|
|
10
|
-
* `sparsevec` fields binding as plain arrays on insert and reading back raw.
|
|
11
|
-
*/
|
|
12
|
-
export declare function isVectorFieldType(type: unknown): boolean;
|
|
13
7
|
/** Resolves the effective cast from field options, `columnType` taking priority over `type`. */
|
|
14
8
|
export declare function resolveVectorCast(field: {
|
|
15
9
|
type?: unknown;
|
|
@@ -2,14 +2,6 @@
|
|
|
2
2
|
* Kept out of `schema/canonicalType.ts`: importing one function from that migration/codegen module
|
|
3
3
|
* pulled all ~18 KB of its type-mapping tables into every consumer's bundle.
|
|
4
4
|
*/
|
|
5
|
-
/**
|
|
6
|
-
* Whether a declared field type is a vector of any width. Every dialect that treats vectors specially
|
|
7
|
-
* has to answer this for all three, not just `vector`: matching that one alone left `halfvec` and
|
|
8
|
-
* `sparsevec` fields binding as plain arrays on insert and reading back raw.
|
|
9
|
-
*/
|
|
10
|
-
export function isVectorFieldType(type) {
|
|
11
|
-
return type === 'vector' || type === 'halfvec' || type === 'sparsevec';
|
|
12
|
-
}
|
|
13
5
|
/** Resolves the effective cast from field options, `columnType` taking priority over `type`. */
|
|
14
6
|
export function resolveVectorCast(field) {
|
|
15
7
|
const raw = field?.columnType ?? field?.type;
|
|
@@ -21,7 +21,7 @@ export declare function Filter<E>(name: string, opts: FilterOptions<E>): (entity
|
|
|
21
21
|
* `E` is inferred from the class the returned decorator is applied to, which is what lets the column
|
|
22
22
|
* names be checked against it: `@Index(['nope'])` does not compile.
|
|
23
23
|
*
|
|
24
|
-
* @example `@Index(['lastName', 'firstName'], { name: '
|
|
24
|
+
* @example `@Index(['lastName', 'firstName'], { name: 'users_fullname_idx' })`
|
|
25
25
|
* @example `@Index(['email'], { unique: true })`
|
|
26
26
|
* @example `@Index(['status'], { where: "status = 'active'" })`
|
|
27
27
|
*/
|
|
@@ -33,7 +33,7 @@ export function Filter(name, opts) {
|
|
|
33
33
|
* `E` is inferred from the class the returned decorator is applied to, which is what lets the column
|
|
34
34
|
* names be checked against it: `@Index(['nope'])` does not compile.
|
|
35
35
|
*
|
|
36
|
-
* @example `@Index(['lastName', 'firstName'], { name: '
|
|
36
|
+
* @example `@Index(['lastName', 'firstName'], { name: 'users_fullname_idx' })`
|
|
37
37
|
* @example `@Index(['email'], { unique: true })`
|
|
38
38
|
* @example `@Index(['status'], { where: "status = 'active'" })`
|
|
39
39
|
*/
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { EntityGetter, FieldOptions, FieldType, IdValue, RelationManyToManyOptions, RelationManyToOneOptions, RelationOneToManyOptions, RelationOneToOneOptions, TsTypeOf } from '../../type/index.js';
|
|
2
|
+
import type { RejectIncompatible } from '../../util/index.js';
|
|
2
3
|
/** A member decorator that also constrains the property it may be applied to. */
|
|
3
4
|
type MemberDecorator<V> = (value: undefined, context: ClassFieldDecoratorContext<unknown, V>) => void;
|
|
4
5
|
/**
|
|
@@ -51,7 +52,7 @@ export declare function Field<O extends FieldOptions<DeclaredValue<O>> & ({
|
|
|
51
52
|
type: FieldType;
|
|
52
53
|
} | {
|
|
53
54
|
references: EntityGetter;
|
|
54
|
-
}) & RejectUnknown<O, FieldOptions>>(opts: O): MemberDecorator<DeclaredValue<O> | undefined>;
|
|
55
|
+
}) & RejectUnknown<O, FieldOptions> & RejectIncompatible<O>>(opts: O): MemberDecorator<DeclaredValue<O> | undefined>;
|
|
55
56
|
/**
|
|
56
57
|
* Declares the primary key, checked the same way as `@Field`.
|
|
57
58
|
*
|
|
@@ -60,7 +61,9 @@ export declare function Field<O extends FieldOptions<DeclaredValue<O>> & ({
|
|
|
60
61
|
*/
|
|
61
62
|
export declare function Id<O extends FieldOptions<DeclaredValue<O>> & {
|
|
62
63
|
type: FieldType;
|
|
63
|
-
} & RejectUnknown<O, FieldOptions
|
|
64
|
+
} & RejectUnknown<O, FieldOptions> & RejectIncompatible<O> & {
|
|
65
|
+
readonly nullable?: false;
|
|
66
|
+
}>(opts: O): MemberDecorator<DeclaredValue<O> | undefined>;
|
|
64
67
|
/**
|
|
65
68
|
* `E` comes from the mandatory `entity` getter, so the context can insist the property really holds that
|
|
66
69
|
* entity: `@ManyToOne({ entity: () => Other })` on a `Company` field stops compiling, and a to-many
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SOFT_DELETE_FILTER } from '../../type/index.js';
|
|
2
|
-
import { getKeys, ddlText, hasKeys, isToManyRelation, lowerFirst, normalizeIndexColumn, upperFirst, } from '../../util/index.js';
|
|
2
|
+
import { fieldOptionConflict, getKeys, ddlText, hasKeys, isToManyRelation, lowerFirst, normalizeIndexColumn, upperFirst, } from '../../util/index.js';
|
|
3
3
|
import { ownRegistrations } from '../decorator/bag.js';
|
|
4
4
|
// Held on `globalThis` via the global symbol registry so a single metadata map survives multiple
|
|
5
5
|
// evaluations of this module (HMR, duplicated/federated bundles, ESM+CJS dual-loading). Version-suffixed
|
|
@@ -10,11 +10,15 @@ const metaKey = Symbol.for('uql-orm/entity/metadata/v1');
|
|
|
10
10
|
const metas = holder[metaKey] ?? new Map();
|
|
11
11
|
holder[metaKey] = metas;
|
|
12
12
|
export function defineField(entity, key, opts = {}) {
|
|
13
|
-
const meta =
|
|
13
|
+
const meta = ensureWritableMeta(entity);
|
|
14
14
|
if (!opts.type && !opts.references && !opts.virtual) {
|
|
15
15
|
throw new TypeError(`'${entity.name}.${key}' needs a 'type'. Declare it - '@Field({ type: String })' - or point the field ` +
|
|
16
16
|
"at another entity with 'references', which resolves the column type from its primary key.");
|
|
17
17
|
}
|
|
18
|
+
const conflict = fieldOptionConflict(opts);
|
|
19
|
+
if (conflict) {
|
|
20
|
+
throw new TypeError(`'${entity.name}.${key}' ${conflict}.`);
|
|
21
|
+
}
|
|
18
22
|
const fieldKey = key;
|
|
19
23
|
// Flagged when the author gave `references` but no `type`, so schema generation knows to resolve the
|
|
20
24
|
// column from the referenced primary key (picking up its `columnType`, length and chained keys)
|
|
@@ -32,7 +36,7 @@ export function defineRelation(entity, key, opts) {
|
|
|
32
36
|
if (!opts.entity) {
|
|
33
37
|
throw new TypeError(`'${entity.name}.${key}' needs an 'entity' getter, e.g. '@ManyToOne({ entity: () => Company })'.`);
|
|
34
38
|
}
|
|
35
|
-
const meta =
|
|
39
|
+
const meta = ensureWritableMeta(entity);
|
|
36
40
|
// Registration writes the authored shape into a map declared as resolved: `getMeta` runs
|
|
37
41
|
// `fillRelations`, which settles `entity`, `references` and `mappedBy` or throws. Bridging the two
|
|
38
42
|
// shapes here is what lets every consumer read `RelationMeta` without asserting.
|
|
@@ -41,7 +45,7 @@ export function defineRelation(entity, key, opts) {
|
|
|
41
45
|
return meta;
|
|
42
46
|
}
|
|
43
47
|
export function defineHook(entity, methodName, event) {
|
|
44
|
-
const meta =
|
|
48
|
+
const meta = ensureWritableMeta(entity);
|
|
45
49
|
if (!meta.hooks)
|
|
46
50
|
meta.hooks = {};
|
|
47
51
|
if (!meta.hooks[event])
|
|
@@ -54,7 +58,7 @@ export function defineHook(entity, methodName, event) {
|
|
|
54
58
|
* lets the dialects render one shape instead of re-parsing it.
|
|
55
59
|
*/
|
|
56
60
|
export function defineIndex(entity, index) {
|
|
57
|
-
const meta =
|
|
61
|
+
const meta = ensureWritableMeta(entity);
|
|
58
62
|
if (!meta.indexes)
|
|
59
63
|
meta.indexes = [];
|
|
60
64
|
meta.indexes.push({
|
|
@@ -66,7 +70,7 @@ export function defineIndex(entity, index) {
|
|
|
66
70
|
return meta;
|
|
67
71
|
}
|
|
68
72
|
export function defineFilter(entity, name, opts) {
|
|
69
|
-
const meta =
|
|
73
|
+
const meta = ensureWritableMeta(entity);
|
|
70
74
|
if (name === SOFT_DELETE_FILTER) {
|
|
71
75
|
throw TypeError(`'${entity.name}' filter name '${SOFT_DELETE_FILTER}' is reserved; it is auto-registered from @Field({ softDelete })`);
|
|
72
76
|
}
|
|
@@ -112,7 +116,7 @@ export function defineEntity(entity, opts = {}) {
|
|
|
112
116
|
throw new TypeError(`'${entity.name}' has a dotted name '${opts.name}'. Name the schema separately as ` +
|
|
113
117
|
`{ schema: '${schema}', name: '${rest.join('.')}' }.`);
|
|
114
118
|
}
|
|
115
|
-
const meta =
|
|
119
|
+
const meta = ensureWritableMeta(entity);
|
|
116
120
|
// Covers `defineEntity(Decorated)` called on a class whose members carry decorators. `@Entity()`
|
|
117
121
|
// drains `context.metadata` itself, because TypeScript only attaches `Symbol.metadata` to the class
|
|
118
122
|
// after class decorators return; draining empties the bag, so whichever runs second is a no-op.
|
|
@@ -132,8 +136,9 @@ export function defineEntity(entity, opts = {}) {
|
|
|
132
136
|
if (!hasKeys(meta.fields)) {
|
|
133
137
|
throw TypeError(`'${entity.name}' must have fields`);
|
|
134
138
|
}
|
|
135
|
-
|
|
136
|
-
meta.
|
|
139
|
+
// A later call composes onto the entity, so saying nothing about the table retracts nothing.
|
|
140
|
+
meta.name = opts.name ?? meta.name ?? entity.name;
|
|
141
|
+
meta.schema = opts.schema ?? meta.schema;
|
|
137
142
|
let proto = Object.getPrototypeOf(entity.prototype);
|
|
138
143
|
while (proto.constructor !== Object) {
|
|
139
144
|
const parent = proto.constructor;
|
|
@@ -211,12 +216,21 @@ export function getEntities() {
|
|
|
211
216
|
return acc;
|
|
212
217
|
}, []);
|
|
213
218
|
}
|
|
219
|
+
/**
|
|
220
|
+
* The metadata of `entity`, marked as changed. Every `define*` goes through this, and nothing outside
|
|
221
|
+
* this file writes to a meta, so it is the one place a derived cache can be told it has gone stale.
|
|
222
|
+
*/
|
|
223
|
+
function ensureWritableMeta(entity) {
|
|
224
|
+
const meta = ensureMeta(entity);
|
|
225
|
+
meta.revision++;
|
|
226
|
+
return meta;
|
|
227
|
+
}
|
|
214
228
|
function ensureMeta(entity) {
|
|
215
229
|
let meta = metas.get(entity);
|
|
216
230
|
if (meta) {
|
|
217
231
|
return meta;
|
|
218
232
|
}
|
|
219
|
-
meta = { entity, ids: [], fields: {}, relations: {} };
|
|
233
|
+
meta = { entity, ids: [], fields: {}, relations: {}, revision: 0 };
|
|
220
234
|
metas.set(entity, meta);
|
|
221
235
|
return meta;
|
|
222
236
|
}
|
|
@@ -225,10 +239,13 @@ export function getMeta(entity) {
|
|
|
225
239
|
if (!meta) {
|
|
226
240
|
throw TypeError(`'${entity.name}' is not an entity`);
|
|
227
241
|
}
|
|
228
|
-
if (meta.
|
|
242
|
+
if (meta.processedAt === meta.revision) {
|
|
229
243
|
return meta;
|
|
230
244
|
}
|
|
231
|
-
|
|
245
|
+
// Stamped before finalizing, not after: `fillInverseSide` reads the other side through `getMeta`,
|
|
246
|
+
// and with each side mapped by the other that recursion has to find this half-filled meta rather
|
|
247
|
+
// than run again. Finalizing twice is harmless anyway - every step of it skips what it settled.
|
|
248
|
+
meta.processedAt = meta.revision;
|
|
232
249
|
return fillRelations(meta);
|
|
233
250
|
}
|
|
234
251
|
function fillRelations(meta) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsonPath } from '../dialect/jsonSql.js';
|
|
2
2
|
import { MysqlLikeSqlDialect } from '../dialect/mysqlLikeSqlDialect.js';
|
|
3
|
-
import { isVectorFieldType } from '../dialect/vectorCast.js';
|
|
4
3
|
import { getMeta } from '../entity/index.js';
|
|
4
|
+
import { columnFamily } from '../util/field.util.js';
|
|
5
5
|
import { MARIA_VECTOR_METRICS } from './mariaVectorMetrics.js';
|
|
6
6
|
export class MariaDialect extends MysqlLikeSqlDialect {
|
|
7
7
|
dialectName = 'mariadb';
|
|
@@ -19,7 +19,8 @@ export class MariaDialect extends MysqlLikeSqlDialect {
|
|
|
19
19
|
indexIfNotExists: true,
|
|
20
20
|
};
|
|
21
21
|
upsertReturning(meta) {
|
|
22
|
-
|
|
22
|
+
const returning = this.returningId(meta);
|
|
23
|
+
return returning ? ` ${returning}` : '';
|
|
23
24
|
}
|
|
24
25
|
/**
|
|
25
26
|
* MariaDB supports neither MySQL's `->`/`->>` shorthand nor the base's chained form. `JSON_VALUE`
|
|
@@ -75,6 +76,6 @@ export class MariaDialect extends MysqlLikeSqlDialect {
|
|
|
75
76
|
}
|
|
76
77
|
/** The reverse: selecting a `VECTOR` column raw yields that blob, so it is read back as text. */
|
|
77
78
|
selectFieldExpr(escapedColumn, field) {
|
|
78
|
-
return
|
|
79
|
+
return columnFamily(field.type) === 'vector' ? `VEC_ToText(${escapedColumn})` : escapedColumn;
|
|
79
80
|
}
|
|
80
81
|
}
|
|
@@ -154,10 +154,10 @@ export class TableBuilder {
|
|
|
154
154
|
return this;
|
|
155
155
|
}
|
|
156
156
|
unique(columns, options) {
|
|
157
|
-
return this.addIndex(
|
|
157
|
+
return this.addIndex(columns, options, true);
|
|
158
158
|
}
|
|
159
159
|
index(columns, options) {
|
|
160
|
-
return this.addIndex(
|
|
160
|
+
return this.addIndex(columns, options, false);
|
|
161
161
|
}
|
|
162
162
|
/**
|
|
163
163
|
* `@Index` and `table.index(...)` differ only in how the name is defaulted, so both normalize their
|
|
@@ -165,12 +165,13 @@ export class TableBuilder {
|
|
|
165
165
|
* form, and anything left as a bare string would reach it as a column literally named `[object
|
|
166
166
|
* Object]`.
|
|
167
167
|
*/
|
|
168
|
-
addIndex(
|
|
168
|
+
addIndex(columns, options, unique) {
|
|
169
169
|
const { name, ...rest } = typeof options === 'string' ? { name: options } : (options ?? {});
|
|
170
170
|
const entries = columns.map(normalizeIndexColumn);
|
|
171
171
|
this._indexes.push({
|
|
172
172
|
...rest,
|
|
173
|
-
name: name ??
|
|
173
|
+
name: name ??
|
|
174
|
+
derivedIndexName(this._name, entries.map((entry) => entry.column), unique),
|
|
174
175
|
where: ddlText(rest.where, 'a partial-index predicate'),
|
|
175
176
|
entries,
|
|
176
177
|
unique,
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Detects schema drift between expected schema (from entities) and
|
|
5
5
|
* actual database schema.
|
|
6
6
|
*/
|
|
7
|
-
import { canonicalToSql } from '../../schema/canonicalType.js';
|
|
7
|
+
import { canonicalToSql, engineType } from '../../schema/canonicalType.js';
|
|
8
8
|
import { diffSchemas } from '../../schema/schemaASTDiffer.js';
|
|
9
9
|
function resolveOptions(options) {
|
|
10
10
|
return {
|
|
@@ -24,16 +24,21 @@ function resolveOptions(options) {
|
|
|
24
24
|
*/
|
|
25
25
|
export function detectDrift(expectedAST, actualAST, options = {}) {
|
|
26
26
|
const opts = resolveOptions(options);
|
|
27
|
+
const { dialect } = opts;
|
|
27
28
|
const diff = diffSchemas(expectedAST, actualAST, {
|
|
28
29
|
compareIndexes: opts.checkIndexes,
|
|
29
30
|
indexFacets: opts.indexFacets,
|
|
30
31
|
compareRelationships: opts.checkForeignKeys,
|
|
31
32
|
excludeTables: opts.excludeTables,
|
|
33
|
+
// Without a dialect there is no engine to compare through, and `formatType` below then reports no
|
|
34
|
+
// type drift at all.
|
|
35
|
+
...(dialect && { normalizeType: engineType(dialect) }),
|
|
32
36
|
});
|
|
33
37
|
const drifts = [
|
|
34
38
|
...detectTableDrifts(diff),
|
|
35
39
|
...detectColumnDrifts(diff, opts),
|
|
36
40
|
...detectIndexDrifts(diff),
|
|
41
|
+
...detectPrimaryKeyDrifts(diff),
|
|
37
42
|
...detectRelationshipDrifts(diff),
|
|
38
43
|
];
|
|
39
44
|
return {
|
|
@@ -43,6 +48,21 @@ export function detectDrift(expectedAST, actualAST, options = {}) {
|
|
|
43
48
|
generatedAt: new Date(),
|
|
44
49
|
};
|
|
45
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* A table whose key holds different columns than the entity declares.
|
|
53
|
+
*
|
|
54
|
+
* Critical, and reported as a `constraint_mismatch` like any other: rows the database will accept
|
|
55
|
+
* are not the rows the ORM believes are unique, so it addresses by a key nothing enforces.
|
|
56
|
+
*/
|
|
57
|
+
function detectPrimaryKeyDrifts(diff) {
|
|
58
|
+
return diff.primaryKeyDiffs.map((pkDiff) => ({
|
|
59
|
+
type: 'constraint_mismatch',
|
|
60
|
+
severity: 'critical',
|
|
61
|
+
table: pkDiff.table,
|
|
62
|
+
details: `Primary key of "${pkDiff.table}" is (${pkDiff.actual.join(', ') || 'none'}) in the database but (${pkDiff.expected.join(', ') || 'none'}) in the entity`,
|
|
63
|
+
suggestion: 'Generate a migration to change the primary key',
|
|
64
|
+
}));
|
|
65
|
+
}
|
|
46
66
|
/**
|
|
47
67
|
* Detect table-level drifts (missing/unexpected tables).
|
|
48
68
|
*/
|