uql-orm 0.50.0 → 0.52.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/browser/uql-browser.min.js +2 -2
- package/dist/browser/uql-browser.min.js.map +2 -2
- package/dist/bunSql/bunSql.util.d.ts +33 -10
- package/dist/bunSql/bunSql.util.js +57 -42
- package/dist/bunSql/bunSqlQuerier.d.ts +13 -8
- package/dist/bunSql/bunSqlQuerier.js +17 -8
- package/dist/bunSql/bunSqlQuerierPool.d.ts +10 -2
- package/dist/bunSql/bunSqlQuerierPool.js +38 -25
- package/dist/bunSql/index.d.ts +1 -3
- package/dist/bunSql/index.js +0 -3
- package/dist/dialect/abstractSqlDialect.d.ts +61 -7
- package/dist/dialect/abstractSqlDialect.js +88 -27
- package/dist/dialect/aliases.d.ts +2 -0
- package/dist/dialect/aliases.js +2 -0
- package/dist/dialect/jsonSql.d.ts +2 -2
- package/dist/dialect/jsonSql.js +2 -2
- package/dist/dialect/mergeSqlDialect.d.ts +45 -0
- package/dist/dialect/mergeSqlDialect.js +89 -0
- package/dist/dialect/mysqlLikeSqlDialect.js +4 -1
- package/dist/dialect/pgLikeSqlDialect.d.ts +3 -3
- package/dist/dialect/pgLikeSqlDialect.js +15 -12
- package/dist/migrate/builder/expressions.js +5 -0
- package/dist/migrate/introspection/index.d.ts +2 -0
- package/dist/migrate/introspection/index.js +2 -0
- package/dist/migrate/introspection/mssqlIntrospector.d.ts +63 -0
- package/dist/migrate/introspection/mssqlIntrospector.js +198 -0
- package/dist/migrate/introspection/postgresIntrospector.js +3 -3
- package/dist/migrate/introspection/registry.d.ts +3 -0
- package/dist/migrate/introspection/registry.js +28 -0
- package/dist/migrate/migrator.js +2 -21
- package/dist/mongo/mongoDialect.js +4 -1
- package/dist/mongo/mongodbQuerier.d.ts +2 -2
- package/dist/mongo/mongodbQuerier.js +8 -6
- package/dist/mssql/index.d.ts +3 -0
- package/dist/mssql/index.js +3 -0
- package/dist/mssql/mssqlDialect.d.ts +144 -0
- package/dist/mssql/mssqlDialect.js +328 -0
- package/dist/mssql/mssqlQuerier.d.ts +23 -0
- package/dist/mssql/mssqlQuerier.js +137 -0
- package/dist/mssql/mssqlQuerierPool.d.ts +17 -0
- package/dist/mssql/mssqlQuerierPool.js +32 -0
- package/dist/mssql/mssqlWireTypes.d.ts +23 -0
- package/dist/mssql/mssqlWireTypes.js +44 -0
- package/dist/pglite/pgliteQuerier.d.ts +4 -2
- package/dist/pglite/pgliteQuerier.js +7 -2
- package/dist/postgres/pgCursorStream.d.ts +20 -0
- package/dist/postgres/pgCursorStream.js +49 -0
- package/dist/postgres/pgDialect.d.ts +1 -1
- package/dist/postgres/pgDialect.js +1 -1
- package/dist/postgres/postgresWireDriverCapabilities.d.ts +12 -10
- package/dist/postgres/postgresWireDriverCapabilities.js +12 -10
- package/dist/querier/abstractQuerier.d.ts +3 -7
- package/dist/querier/abstractQuerier.js +22 -2
- package/dist/querier/abstractQuerierPool.d.ts +3 -3
- package/dist/schema/canonicalType.js +96 -113
- package/dist/sqlite/sqliteDialect.d.ts +7 -7
- package/dist/sqlite/sqliteDialect.js +21 -18
- package/dist/type/dialect.d.ts +31 -4
- package/dist/type/migratorDialect.d.ts +1 -1
- package/dist/type/migratorDialect.js +1 -0
- package/dist/type/query.d.ts +29 -6
- package/dist/type/universalQuerier.d.ts +3 -3
- package/package.json +13 -3
- package/dist/bunSql/bunSqlCockroachDialect.d.ts +0 -12
- package/dist/bunSql/bunSqlCockroachDialect.js +0 -15
- package/dist/bunSql/bunSqlPostgresDialect.d.ts +0 -11
- package/dist/bunSql/bunSqlPostgresDialect.js +0 -14
- package/dist/bunSql/bunSqliteDialect.d.ts +0 -6
- package/dist/bunSql/bunSqliteDialect.js +0 -6
|
@@ -10,7 +10,7 @@ import { jsonSetTarget } from './jsonSql.js';
|
|
|
10
10
|
import { resolveVectorCast, toSparsevecLiteral } from './vectorCast.js';
|
|
11
11
|
/**
|
|
12
12
|
* Shared AST/quoting/JSONB/full-text-search/vector-search implementation between Postgres and
|
|
13
|
-
* CockroachDB (wire- and SQL-compatible for everything below, including `
|
|
13
|
+
* CockroachDB (wire- and SQL-compatible for everything below, including `TO_TSVECTOR`/`TO_TSQUERY`
|
|
14
14
|
* and pgvector's `<=>`/`<->`/`<#>` distance operators, which CockroachDB implements natively).
|
|
15
15
|
* `xmax`-based upsert `created` detection is Postgres-only (CockroachDB has no `xmax`/`ctid`) and
|
|
16
16
|
* stays in {@link PostgresDialect}, along with the `vectorExtension`/`vectorIndexStyle` values that
|
|
@@ -37,7 +37,10 @@ export class PgLikeSqlDialect extends AbstractSqlDialect {
|
|
|
37
37
|
vectorIndexRequiresNotNull: false,
|
|
38
38
|
vectorSupportsLength: true,
|
|
39
39
|
supportsTimestamptz: true,
|
|
40
|
-
|
|
40
|
+
stringSizing: 'bounded-text',
|
|
41
|
+
supportsUnsigned: false,
|
|
42
|
+
multipleCascadePaths: true,
|
|
43
|
+
serverSideCursors: true,
|
|
41
44
|
};
|
|
42
45
|
escapeIdChar = '"';
|
|
43
46
|
// Shared default for both dialects. CockroachDB docs flag sequential PKs as a hotspotting risk
|
|
@@ -66,7 +69,7 @@ export class PgLikeSqlDialect extends AbstractSqlDialect {
|
|
|
66
69
|
const keys = this.escapeId(ctx.nextAlias(PER_PARENT_KEYS_ALIAS));
|
|
67
70
|
const branch = this.escapeId(ctx.nextAlias(PER_PARENT_BRANCH_ALIAS));
|
|
68
71
|
const column = (index) => `${keys}.k${index}`;
|
|
69
|
-
// `
|
|
72
|
+
// `UNNEST` resolves an uncast parameter to `unknown` and refuses it ("function unnest(unknown) is
|
|
70
73
|
// not unique"), so the array says its type. It comes from the parent's key column, which always
|
|
71
74
|
// declares one, rather than the child's foreign key, which would have to be resolved through the
|
|
72
75
|
// reference it takes its own type from.
|
|
@@ -78,7 +81,7 @@ export class PgLikeSqlDialect extends AbstractSqlDialect {
|
|
|
78
81
|
const values = parents.map((it) => it[parent]);
|
|
79
82
|
return `${this.addValue(ctx.values, values)}::${canonicalToSql(fieldOptionsToCanonical(field), this)}[]`;
|
|
80
83
|
});
|
|
81
|
-
const rowSource = `
|
|
84
|
+
const rowSource = `UNNEST(${sources.join(', ')}) AS ${keys}(${joins.map((_, index) => `k${index}`).join(', ')})`;
|
|
82
85
|
// The keys come from the row source rather than as values, which is the whole point of correlating:
|
|
83
86
|
// one branch, planned once, instead of one per parent.
|
|
84
87
|
const correlated = Object.fromEntries(joins.map(({ joined }, index) => [joined, raw(({ ctx: inner }) => inner.append(column(index)))]));
|
|
@@ -141,8 +144,8 @@ export class PgLikeSqlDialect extends AbstractSqlDialect {
|
|
|
141
144
|
return `$${index}`;
|
|
142
145
|
}
|
|
143
146
|
/**
|
|
144
|
-
* `
|
|
145
|
-
* (quoted phrases, `or`, `-negation`) and never raises a syntax error, unlike `
|
|
147
|
+
* `TO_TSVECTOR(...) @@ WEBSEARCH_TO_TSQUERY(...)`. `WEBSEARCH_TO_TSQUERY` takes free-form user input
|
|
148
|
+
* (quoted phrases, `or`, `-negation`) and never raises a syntax error, unlike `TO_TSQUERY`, which
|
|
146
149
|
* rejects anything unparseable - including a plain two-word search.
|
|
147
150
|
*/
|
|
148
151
|
appendTextSearch(ctx, _entity, meta, search) {
|
|
@@ -151,7 +154,7 @@ export class PgLikeSqlDialect extends AbstractSqlDialect {
|
|
|
151
154
|
.join(` || ' ' || `);
|
|
152
155
|
// The config is bound once and its numbered placeholder reused by both calls.
|
|
153
156
|
const config = search.$config ? `${this.addValue(ctx.values, search.$config)}::regconfig, ` : '';
|
|
154
|
-
ctx.append(`
|
|
157
|
+
ctx.append(`TO_TSVECTOR(${config}${fields}) @@ WEBSEARCH_TO_TSQUERY(${config}`);
|
|
155
158
|
ctx.addValue(search.$value);
|
|
156
159
|
ctx.append(')');
|
|
157
160
|
}
|
|
@@ -159,14 +162,14 @@ export class PgLikeSqlDialect extends AbstractSqlDialect {
|
|
|
159
162
|
return `${jsonField} @> ${this.jsonVal(ctx, value)}`;
|
|
160
163
|
}
|
|
161
164
|
jsonSize(ctx, jsonField, value) {
|
|
162
|
-
return this.buildFragment(ctx, (fragmentCtx) => this.buildSizeComparison(fragmentCtx, () => fragmentCtx.append(`
|
|
165
|
+
return this.buildFragment(ctx, (fragmentCtx) => this.buildSizeComparison(fragmentCtx, () => fragmentCtx.append(`JSONB_ARRAY_LENGTH(${jsonField})`), value));
|
|
163
166
|
}
|
|
164
167
|
/**
|
|
165
168
|
* Object elements stay `jsonb` so each field can pick `->` or `->>`. Scalar elements are exploded
|
|
166
169
|
* as text unless they are compared as JSON, where `_text` would yield `text = jsonb`.
|
|
167
170
|
*/
|
|
168
171
|
jsonElemFrom(jsonField, fields, alias, asJson = false) {
|
|
169
|
-
const fn = fields.length || asJson ? '
|
|
172
|
+
const fn = fields.length || asJson ? 'JSONB_ARRAY_ELEMENTS' : 'JSONB_ARRAY_ELEMENTS_TEXT';
|
|
170
173
|
return `${fn}(${jsonField}) AS ${alias}`;
|
|
171
174
|
}
|
|
172
175
|
jsonElemRef(alias, field, asJson = false) {
|
|
@@ -209,8 +212,8 @@ export class PgLikeSqlDialect extends AbstractSqlDialect {
|
|
|
209
212
|
*/
|
|
210
213
|
jsonPullKey(ctx, expr, escapedCol, key, value) {
|
|
211
214
|
const escapedKey = escapeSingleQuotes(key);
|
|
212
|
-
const kept = `SELECT
|
|
213
|
-
return `
|
|
215
|
+
const kept = `SELECT JSONB_AGG(${JSON_PULL_ALIAS}.val ORDER BY ${JSON_PULL_ALIAS}.ord) FROM JSONB_ARRAY_ELEMENTS(${escapedCol}->'${escapedKey}') WITH ORDINALITY AS ${JSON_PULL_ALIAS}(val, ord) WHERE ${JSON_PULL_ALIAS}.val <> ${this.jsonVal(ctx, value)}`;
|
|
216
|
+
return `JSONB_SET(${expr}, '{${escapedKey}}', COALESCE((${kept}), '[]'::jsonb), false)`;
|
|
214
217
|
}
|
|
215
218
|
jsonSet(ctx, expr, set, field) {
|
|
216
219
|
return `${jsonSetTarget(expr, field, `'{}'::jsonb`)} || ${this.jsonVal(ctx, set)}`;
|
|
@@ -220,7 +223,7 @@ export class PgLikeSqlDialect extends AbstractSqlDialect {
|
|
|
220
223
|
return Object.entries(push).reduce((acc, [key, value]) => {
|
|
221
224
|
const escapedKey = escapeSingleQuotes(key);
|
|
222
225
|
const ph = this.jsonVal(ctx, value);
|
|
223
|
-
return `
|
|
226
|
+
return `JSONB_SET(${acc}, '{${escapedKey}}', COALESCE((${acc})->'${escapedKey}', '[]'::jsonb) || JSONB_BUILD_ARRAY(${ph}))`;
|
|
224
227
|
}, expr);
|
|
225
228
|
}
|
|
226
229
|
jsonUnset(ctx, expr, unset) {
|
|
@@ -26,6 +26,11 @@ export const DIALECT_DEFAULTS = {
|
|
|
26
26
|
mysql: { expressions: MYSQL, wrapTypes: MYSQL_LARGE_TYPES },
|
|
27
27
|
mariadb: { expressions: { ...MYSQL, uuidv7: 'UUID_v7()' }, wrapTypes: MYSQL_LARGE_TYPES },
|
|
28
28
|
sqlite: { expressions: ANSI },
|
|
29
|
+
// `SYSUTCDATETIME()` over `CURRENT_TIMESTAMP`, which is local time in the server's zone. No
|
|
30
|
+
// `uuidv7`: `NEWSEQUENTIALID()` is an ordered v4 GUID, so it carries no readable timestamp and
|
|
31
|
+
// does not sort the way a v7 does elsewhere - a `uuidv7()` default is refused rather than served
|
|
32
|
+
// something that only looks like one.
|
|
33
|
+
mssql: { expressions: { ...ANSI, now: 'SYSUTCDATETIME()', uuid: 'NEWID()' } },
|
|
29
34
|
};
|
|
30
35
|
/**
|
|
31
36
|
* A DDL default that is SQL rather than a literal. A class, not a plain object, so a JSON default
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export * from './abstractSqlSchemaIntrospector.js';
|
|
2
2
|
export * from './mongoIntrospector.js';
|
|
3
|
+
export * from './mssqlIntrospector.js';
|
|
3
4
|
export { MariadbSchemaIntrospector, MysqlSchemaIntrospector } from './mysqlIntrospector.js';
|
|
4
5
|
export * from './postgresIntrospector.js';
|
|
5
6
|
export * from './sqliteIntrospector.js';
|
|
7
|
+
export { introspectorFor } from './registry.js';
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export * from './abstractSqlSchemaIntrospector.js';
|
|
2
2
|
export * from './mongoIntrospector.js';
|
|
3
|
+
export * from './mssqlIntrospector.js';
|
|
3
4
|
export { MariadbSchemaIntrospector, MysqlSchemaIntrospector } from './mysqlIntrospector.js';
|
|
4
5
|
export * from './postgresIntrospector.js';
|
|
5
6
|
export * from './sqliteIntrospector.js';
|
|
7
|
+
export { introspectorFor } from './registry.js';
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { ColumnSchema, ForeignKeySchema, IndexSchema } from '../../type/index.js';
|
|
2
|
+
import { AbstractSqlSchemaIntrospector, type TableRowReader } from './abstractSqlSchemaIntrospector.js';
|
|
3
|
+
/**
|
|
4
|
+
* SQL Server schema introspector.
|
|
5
|
+
*
|
|
6
|
+
* `INFORMATION_SCHEMA` answers columns and foreign keys, but not indexes: it has no view for them at
|
|
7
|
+
* all, and its `CONSTRAINT_COLUMN_USAGE` conflates a unique index with a unique constraint. Those
|
|
8
|
+
* come from `sys.indexes` instead, which is also the only place the filtered-index predicate and the
|
|
9
|
+
* included columns are readable.
|
|
10
|
+
*/
|
|
11
|
+
export declare class MsSqlSchemaIntrospector extends AbstractSqlSchemaIntrospector {
|
|
12
|
+
protected readonly defaultSchemaExpr = "SCHEMA_NAME()";
|
|
13
|
+
protected getTableNamesQuery(): string;
|
|
14
|
+
protected tableExistsQuery(): string;
|
|
15
|
+
protected parseTableExistsResult(results: {
|
|
16
|
+
count?: number;
|
|
17
|
+
}[]): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* `is_identity` and the default come from `sys`, not `INFORMATION_SCHEMA`: the latter has no
|
|
20
|
+
* identity flag, and reports a default already wrapped in the parentheses the engine reprints it
|
|
21
|
+
* with, which is the same text either way but only nameable as a constraint through `sys`.
|
|
22
|
+
*/
|
|
23
|
+
protected getColumnsQuery(_tableName: string): string;
|
|
24
|
+
/** `is_primary_key` is excluded: the key is read separately, the way every other engine reads it. */
|
|
25
|
+
protected getIndexesQuery(_tableName: string): string;
|
|
26
|
+
protected getForeignKeysQuery(_tableName: string): string;
|
|
27
|
+
protected getPrimaryKeyQuery(_tableName: string): string;
|
|
28
|
+
/**
|
|
29
|
+
* `max_length` is in bytes, so an `NVARCHAR` column reports twice its declared length and `MAX`
|
|
30
|
+
* reports `-1`. Read as characters, every Unicode column would drift to double its width the first
|
|
31
|
+
* time it was diffed against the entity that declared it.
|
|
32
|
+
*/
|
|
33
|
+
protected mapColumnsResult(read: TableRowReader, tableName: string, results: MsSqlColumnRow[]): Promise<ColumnSchema[]>;
|
|
34
|
+
protected mapIndexesResult(_read: TableRowReader, _tableName: string, results: {
|
|
35
|
+
index_name: string;
|
|
36
|
+
columns: string;
|
|
37
|
+
is_unique: boolean;
|
|
38
|
+
}[]): Promise<IndexSchema[]>;
|
|
39
|
+
protected mapForeignKeysResult(_read: TableRowReader, _tableName: string, results: {
|
|
40
|
+
constraint_name: string;
|
|
41
|
+
columns: string;
|
|
42
|
+
referenced_table: string;
|
|
43
|
+
referenced_columns: string;
|
|
44
|
+
delete_rule: string;
|
|
45
|
+
update_rule: string;
|
|
46
|
+
}[]): Promise<ForeignKeySchema[]>;
|
|
47
|
+
/**
|
|
48
|
+
* A default is stored wrapped in at least one layer of parentheses - `((0))` for a number,
|
|
49
|
+
* `(N'x')` for a string - because the engine reprints it from its own parse tree.
|
|
50
|
+
*/
|
|
51
|
+
protected parseDefaultValue(defaultValue: string | null): unknown;
|
|
52
|
+
}
|
|
53
|
+
type MsSqlColumnRow = {
|
|
54
|
+
column_name: string;
|
|
55
|
+
data_type: string;
|
|
56
|
+
max_length: number | null;
|
|
57
|
+
numeric_precision: number | null;
|
|
58
|
+
numeric_scale: number | null;
|
|
59
|
+
is_nullable: boolean;
|
|
60
|
+
is_identity: boolean;
|
|
61
|
+
column_default: string | null;
|
|
62
|
+
};
|
|
63
|
+
export {};
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { AbstractSqlSchemaIntrospector } from './abstractSqlSchemaIntrospector.js';
|
|
2
|
+
/**
|
|
3
|
+
* SQL Server schema introspector.
|
|
4
|
+
*
|
|
5
|
+
* `INFORMATION_SCHEMA` answers columns and foreign keys, but not indexes: it has no view for them at
|
|
6
|
+
* all, and its `CONSTRAINT_COLUMN_USAGE` conflates a unique index with a unique constraint. Those
|
|
7
|
+
* come from `sys.indexes` instead, which is also the only place the filtered-index predicate and the
|
|
8
|
+
* included columns are readable.
|
|
9
|
+
*/
|
|
10
|
+
export class MsSqlSchemaIntrospector extends AbstractSqlSchemaIntrospector {
|
|
11
|
+
defaultSchemaExpr = 'SCHEMA_NAME()';
|
|
12
|
+
getTableNamesQuery() {
|
|
13
|
+
return /*sql*/ `
|
|
14
|
+
SELECT TABLE_NAME as table_name
|
|
15
|
+
FROM INFORMATION_SCHEMA.TABLES
|
|
16
|
+
WHERE TABLE_SCHEMA = ${this.schemaExpr}
|
|
17
|
+
AND TABLE_TYPE = 'BASE TABLE'
|
|
18
|
+
ORDER BY TABLE_NAME
|
|
19
|
+
`;
|
|
20
|
+
}
|
|
21
|
+
tableExistsQuery() {
|
|
22
|
+
return /*sql*/ `
|
|
23
|
+
SELECT COUNT(*) as count
|
|
24
|
+
FROM INFORMATION_SCHEMA.TABLES
|
|
25
|
+
WHERE TABLE_SCHEMA = ${this.schemaExpr}
|
|
26
|
+
AND TABLE_NAME = @p1
|
|
27
|
+
`;
|
|
28
|
+
}
|
|
29
|
+
parseTableExistsResult(results) {
|
|
30
|
+
return (this.toNumber(results[0]?.count) ?? 0) > 0;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* `is_identity` and the default come from `sys`, not `INFORMATION_SCHEMA`: the latter has no
|
|
34
|
+
* identity flag, and reports a default already wrapped in the parentheses the engine reprints it
|
|
35
|
+
* with, which is the same text either way but only nameable as a constraint through `sys`.
|
|
36
|
+
*/
|
|
37
|
+
getColumnsQuery(_tableName) {
|
|
38
|
+
return /*sql*/ `
|
|
39
|
+
SELECT
|
|
40
|
+
c.name as column_name,
|
|
41
|
+
t.name as data_type,
|
|
42
|
+
c.max_length as max_length,
|
|
43
|
+
c.precision as numeric_precision,
|
|
44
|
+
c.scale as numeric_scale,
|
|
45
|
+
c.is_nullable as is_nullable,
|
|
46
|
+
c.is_identity as is_identity,
|
|
47
|
+
d.definition as column_default
|
|
48
|
+
FROM sys.columns c
|
|
49
|
+
JOIN sys.objects o ON o.object_id = c.object_id
|
|
50
|
+
JOIN sys.schemas s ON s.schema_id = o.schema_id
|
|
51
|
+
JOIN sys.types t ON t.user_type_id = c.user_type_id
|
|
52
|
+
LEFT JOIN sys.default_constraints d ON d.object_id = c.default_object_id
|
|
53
|
+
WHERE s.name = ${this.schemaExpr} AND o.name = @p1
|
|
54
|
+
ORDER BY c.column_id
|
|
55
|
+
`;
|
|
56
|
+
}
|
|
57
|
+
/** `is_primary_key` is excluded: the key is read separately, the way every other engine reads it. */
|
|
58
|
+
getIndexesQuery(_tableName) {
|
|
59
|
+
return /*sql*/ `
|
|
60
|
+
SELECT
|
|
61
|
+
i.name as index_name,
|
|
62
|
+
i.is_unique as is_unique,
|
|
63
|
+
STRING_AGG(c.name, ',') WITHIN GROUP (ORDER BY ic.key_ordinal) as columns
|
|
64
|
+
FROM sys.indexes i
|
|
65
|
+
JOIN sys.objects o ON o.object_id = i.object_id
|
|
66
|
+
JOIN sys.schemas s ON s.schema_id = o.schema_id
|
|
67
|
+
JOIN sys.index_columns ic ON ic.object_id = i.object_id AND ic.index_id = i.index_id
|
|
68
|
+
JOIN sys.columns c ON c.object_id = ic.object_id AND c.column_id = ic.column_id
|
|
69
|
+
WHERE s.name = ${this.schemaExpr} AND o.name = @p1
|
|
70
|
+
AND i.is_primary_key = 0 AND i.name IS NOT NULL AND ic.is_included_column = 0
|
|
71
|
+
GROUP BY i.name, i.is_unique
|
|
72
|
+
ORDER BY i.name
|
|
73
|
+
`;
|
|
74
|
+
}
|
|
75
|
+
getForeignKeysQuery(_tableName) {
|
|
76
|
+
return /*sql*/ `
|
|
77
|
+
SELECT
|
|
78
|
+
fk.name as constraint_name,
|
|
79
|
+
STRING_AGG(pc.name, ',') WITHIN GROUP (ORDER BY fkc.constraint_column_id) as columns,
|
|
80
|
+
rt.name as referenced_table,
|
|
81
|
+
STRING_AGG(rc.name, ',') WITHIN GROUP (ORDER BY fkc.constraint_column_id) as referenced_columns,
|
|
82
|
+
fk.delete_referential_action_desc as delete_rule,
|
|
83
|
+
fk.update_referential_action_desc as update_rule
|
|
84
|
+
FROM sys.foreign_keys fk
|
|
85
|
+
JOIN sys.objects o ON o.object_id = fk.parent_object_id
|
|
86
|
+
JOIN sys.schemas s ON s.schema_id = o.schema_id
|
|
87
|
+
JOIN sys.foreign_key_columns fkc ON fkc.constraint_object_id = fk.object_id
|
|
88
|
+
JOIN sys.columns pc ON pc.object_id = fkc.parent_object_id AND pc.column_id = fkc.parent_column_id
|
|
89
|
+
JOIN sys.objects rt ON rt.object_id = fk.referenced_object_id
|
|
90
|
+
JOIN sys.columns rc ON rc.object_id = fkc.referenced_object_id AND rc.column_id = fkc.referenced_column_id
|
|
91
|
+
WHERE s.name = ${this.schemaExpr} AND o.name = @p1
|
|
92
|
+
GROUP BY fk.name, rt.name, fk.delete_referential_action_desc, fk.update_referential_action_desc
|
|
93
|
+
ORDER BY fk.name
|
|
94
|
+
`;
|
|
95
|
+
}
|
|
96
|
+
getPrimaryKeyQuery(_tableName) {
|
|
97
|
+
return /*sql*/ `
|
|
98
|
+
SELECT c.name as column_name, i.name as constraint_name
|
|
99
|
+
FROM sys.indexes i
|
|
100
|
+
JOIN sys.objects o ON o.object_id = i.object_id
|
|
101
|
+
JOIN sys.schemas s ON s.schema_id = o.schema_id
|
|
102
|
+
JOIN sys.index_columns ic ON ic.object_id = i.object_id AND ic.index_id = i.index_id
|
|
103
|
+
JOIN sys.columns c ON c.object_id = ic.object_id AND c.column_id = ic.column_id
|
|
104
|
+
WHERE s.name = ${this.schemaExpr} AND o.name = @p1 AND i.is_primary_key = 1
|
|
105
|
+
ORDER BY ic.key_ordinal
|
|
106
|
+
`;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* `max_length` is in bytes, so an `NVARCHAR` column reports twice its declared length and `MAX`
|
|
110
|
+
* reports `-1`. Read as characters, every Unicode column would drift to double its width the first
|
|
111
|
+
* time it was diffed against the entity that declared it.
|
|
112
|
+
*/
|
|
113
|
+
async mapColumnsResult(read, tableName, results) {
|
|
114
|
+
// One extra statement for both flags, and the reader sends it once: a column is in the key or
|
|
115
|
+
// uniquely indexed on its own, and `sys` is the only place either is readable per column.
|
|
116
|
+
const flags = await read(
|
|
117
|
+
/*sql*/ `
|
|
118
|
+
SELECT c.name as column_name, MAX(CAST(i.is_primary_key AS INT)) as is_primary_key
|
|
119
|
+
FROM sys.indexes i
|
|
120
|
+
JOIN sys.objects o ON o.object_id = i.object_id
|
|
121
|
+
JOIN sys.schemas s ON s.schema_id = o.schema_id
|
|
122
|
+
JOIN sys.index_columns ic ON ic.object_id = i.object_id AND ic.index_id = i.index_id
|
|
123
|
+
JOIN sys.columns c ON c.object_id = ic.object_id AND c.column_id = ic.column_id
|
|
124
|
+
WHERE s.name = ${this.schemaExpr} AND o.name = @p1
|
|
125
|
+
AND i.is_unique = 1 AND ic.is_included_column = 0
|
|
126
|
+
AND (SELECT COUNT(*) FROM sys.index_columns k
|
|
127
|
+
WHERE k.object_id = i.object_id AND k.index_id = i.index_id AND k.is_included_column = 0) = 1
|
|
128
|
+
GROUP BY c.name
|
|
129
|
+
`, [tableName]);
|
|
130
|
+
const unique = new Set(flags.map((row) => row.column_name));
|
|
131
|
+
const keys = new Set(flags.filter((row) => Boolean(row.is_primary_key)).map((row) => row.column_name));
|
|
132
|
+
return results.map((row) => {
|
|
133
|
+
const type = row.data_type.toUpperCase();
|
|
134
|
+
const wide = type.startsWith('N');
|
|
135
|
+
const bytes = this.toNumber(row.max_length);
|
|
136
|
+
const length = bytes === undefined || bytes < 0 || !CHARACTER_TYPES.has(type) ? undefined : wide ? bytes / 2 : bytes;
|
|
137
|
+
return {
|
|
138
|
+
name: row.column_name,
|
|
139
|
+
type: bytes === -1 && CHARACTER_TYPES.has(type) ? `${type}(MAX)` : type,
|
|
140
|
+
nullable: Boolean(row.is_nullable),
|
|
141
|
+
defaultValue: this.parseDefaultValue(row.column_default),
|
|
142
|
+
isAutoIncrement: Boolean(row.is_identity),
|
|
143
|
+
isPrimaryKey: keys.has(row.column_name),
|
|
144
|
+
isUnique: unique.has(row.column_name),
|
|
145
|
+
length,
|
|
146
|
+
precision: NUMERIC_TYPES.has(type) ? this.toNumber(row.numeric_precision) : undefined,
|
|
147
|
+
scale: NUMERIC_TYPES.has(type) ? this.toNumber(row.numeric_scale) : undefined,
|
|
148
|
+
};
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
async mapIndexesResult(_read, _tableName, results) {
|
|
152
|
+
return results.map((row) => ({
|
|
153
|
+
name: row.index_name,
|
|
154
|
+
entries: (row.columns ?? '').split(',').map((column) => ({ column })),
|
|
155
|
+
unique: Boolean(row.is_unique),
|
|
156
|
+
}));
|
|
157
|
+
}
|
|
158
|
+
async mapForeignKeysResult(_read, _tableName, results) {
|
|
159
|
+
return results.map((row) => ({
|
|
160
|
+
name: row.constraint_name,
|
|
161
|
+
columns: (row.columns || '').split(','),
|
|
162
|
+
references: { table: row.referenced_table, columns: (row.referenced_columns || '').split(',') },
|
|
163
|
+
// `sys` spells them with an underscore: `SET_NULL`, `NO_ACTION`.
|
|
164
|
+
onDelete: this.normalizeReferentialAction((row.delete_rule || '').replaceAll('_', ' ')),
|
|
165
|
+
onUpdate: this.normalizeReferentialAction((row.update_rule || '').replaceAll('_', ' ')),
|
|
166
|
+
}));
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* A default is stored wrapped in at least one layer of parentheses - `((0))` for a number,
|
|
170
|
+
* `(N'x')` for a string - because the engine reprints it from its own parse tree.
|
|
171
|
+
*/
|
|
172
|
+
parseDefaultValue(defaultValue) {
|
|
173
|
+
if (defaultValue === null || defaultValue === undefined) {
|
|
174
|
+
return undefined;
|
|
175
|
+
}
|
|
176
|
+
let text = defaultValue.trim();
|
|
177
|
+
while (text.startsWith('(') && text.endsWith(')')) {
|
|
178
|
+
text = text.slice(1, -1).trim();
|
|
179
|
+
}
|
|
180
|
+
if (text.toUpperCase() === 'NULL') {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
const unicode = text.startsWith("N'") ? text.slice(1) : text;
|
|
184
|
+
if (unicode.startsWith("'") && unicode.endsWith("'")) {
|
|
185
|
+
return unicode.slice(1, -1).replaceAll("''", "'");
|
|
186
|
+
}
|
|
187
|
+
if (/^-?\d+$/.test(text)) {
|
|
188
|
+
return Number.parseInt(text, 10);
|
|
189
|
+
}
|
|
190
|
+
if (/^-?\d+\.\d+$/.test(text)) {
|
|
191
|
+
return Number.parseFloat(text);
|
|
192
|
+
}
|
|
193
|
+
return text;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/** The types whose `max_length` is a width rather than a fixed storage size. */
|
|
197
|
+
const CHARACTER_TYPES = new Set(['CHAR', 'NCHAR', 'VARCHAR', 'NVARCHAR', 'BINARY', 'VARBINARY']);
|
|
198
|
+
const NUMERIC_TYPES = new Set(['DECIMAL', 'NUMERIC']);
|
|
@@ -109,7 +109,7 @@ export class PostgresSchemaIntrospector extends AbstractSqlSchemaIntrospector {
|
|
|
109
109
|
JOIN pg_class i ON i.oid = ix.indexrelid
|
|
110
110
|
JOIN pg_am am ON am.oid = i.relam
|
|
111
111
|
JOIN pg_namespace n ON n.oid = t.relnamespace
|
|
112
|
-
CROSS JOIN LATERAL
|
|
112
|
+
CROSS JOIN LATERAL UNNEST(ix.indkey) WITH ORDINALITY AS k(attnum, n)
|
|
113
113
|
LEFT JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = k.attnum AND k.attnum > 0
|
|
114
114
|
LEFT JOIN pg_opclass op ON op.oid = ix.indclass[k.n - 1]
|
|
115
115
|
WHERE t.relname = $1
|
|
@@ -136,9 +136,9 @@ export class PostgresSchemaIntrospector extends AbstractSqlSchemaIntrospector {
|
|
|
136
136
|
return /*sql*/ `
|
|
137
137
|
SELECT
|
|
138
138
|
tc.constraint_name,
|
|
139
|
-
|
|
139
|
+
ARRAY_TO_JSON(ARRAY_AGG(kcu.column_name ORDER BY kcu.ordinal_position)) AS columns,
|
|
140
140
|
ccu.table_name AS referenced_table,
|
|
141
|
-
|
|
141
|
+
ARRAY_TO_JSON(ARRAY_AGG(ccu.column_name ORDER BY kcu.ordinal_position)) AS referenced_columns,
|
|
142
142
|
rc.delete_rule,
|
|
143
143
|
rc.update_rule
|
|
144
144
|
FROM information_schema.table_constraints tc
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { QuerierPool, SchemaIntrospector } from '../../type/index.js';
|
|
2
|
+
/** The introspector for `dialectName`, or `undefined` where the migrator has none for it. */
|
|
3
|
+
export declare function introspectorFor(dialectName: string, pool: QuerierPool, schema?: string): SchemaIntrospector | undefined;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { MongoSchemaIntrospector } from './mongoIntrospector.js';
|
|
2
|
+
import { MsSqlSchemaIntrospector } from './mssqlIntrospector.js';
|
|
3
|
+
import { MariadbSchemaIntrospector, MysqlSchemaIntrospector } from './mysqlIntrospector.js';
|
|
4
|
+
import { CockroachSchemaIntrospector, PostgresSchemaIntrospector } from './postgresIntrospector.js';
|
|
5
|
+
import { SqliteSchemaIntrospector } from './sqliteIntrospector.js';
|
|
6
|
+
/**
|
|
7
|
+
* Which introspector each engine gets. A table rather than a `switch` so `Migrator` names no
|
|
8
|
+
* constructor and adding an engine touches one line, not a control flow.
|
|
9
|
+
*
|
|
10
|
+
* Every entry is statically imported, so `uql-orm/migrate` still carries all of them; making the
|
|
11
|
+
* table's values dynamic imports would shrink that entry, at the cost of an async
|
|
12
|
+
* `createIntrospector` the constructor cannot await.
|
|
13
|
+
*/
|
|
14
|
+
const INTROSPECTORS = {
|
|
15
|
+
postgres: (pool, schema) => new PostgresSchemaIntrospector(pool, schema),
|
|
16
|
+
cockroachdb: (pool, schema) => new CockroachSchemaIntrospector(pool, schema),
|
|
17
|
+
mysql: (pool, schema) => new MysqlSchemaIntrospector(pool, schema),
|
|
18
|
+
mariadb: (pool, schema) => new MariadbSchemaIntrospector(pool, schema),
|
|
19
|
+
mssql: (pool, schema) => new MsSqlSchemaIntrospector(pool, schema),
|
|
20
|
+
// Neither has schemas to read: SQLite attaches database files and MongoDB takes its database from
|
|
21
|
+
// the connection, so both ignore the argument rather than filtering on it.
|
|
22
|
+
sqlite: (pool) => new SqliteSchemaIntrospector(pool),
|
|
23
|
+
mongodb: (pool) => new MongoSchemaIntrospector(pool),
|
|
24
|
+
};
|
|
25
|
+
/** The introspector for `dialectName`, or `undefined` where the migrator has none for it. */
|
|
26
|
+
export function introspectorFor(dialectName, pool, schema) {
|
|
27
|
+
return INTROSPECTORS[dialectName]?.(pool, schema);
|
|
28
|
+
}
|
package/dist/migrate/migrator.js
CHANGED
|
@@ -8,7 +8,7 @@ import { LoggerWrapper } from '../util/index.js';
|
|
|
8
8
|
import { withQuerierForMigrations, withSqlQuerierForMigrations } from './acquireQuerierForMigrations.js';
|
|
9
9
|
import { buildSqlQuerierMigrationModule, EMPTY_MANUAL_MIGRATION_DOWN_INNER, EMPTY_MANUAL_MIGRATION_UP_INNER, emitSqlRunCalls, } from './codegen/migrationFile.js';
|
|
10
10
|
import { runMongoCommand } from './generator/mongoCommand.js';
|
|
11
|
-
import {
|
|
11
|
+
import { introspectorFor } from './introspection/registry.js';
|
|
12
12
|
import { createSchemaGenerator } from './schemaGenerator.js';
|
|
13
13
|
import { createSchemaGeneratorAsync } from './schemaGeneratorAsync.js';
|
|
14
14
|
import { DatabaseMigrationStorage } from './storage/databaseStorage.js';
|
|
@@ -73,26 +73,7 @@ export class Migrator {
|
|
|
73
73
|
}
|
|
74
74
|
/** `schema` reads one namespace instead of the connection's own; see {@link BaseSqlIntrospector.schema}. */
|
|
75
75
|
createIntrospector(schema) {
|
|
76
|
-
|
|
77
|
-
if (!isKnownMigratorDialect(d)) {
|
|
78
|
-
return undefined;
|
|
79
|
-
}
|
|
80
|
-
switch (d) {
|
|
81
|
-
case 'postgres':
|
|
82
|
-
return new PostgresSchemaIntrospector(this.pool, schema);
|
|
83
|
-
case 'cockroachdb':
|
|
84
|
-
return new CockroachSchemaIntrospector(this.pool, schema);
|
|
85
|
-
case 'mysql':
|
|
86
|
-
return new MysqlSchemaIntrospector(this.pool, schema);
|
|
87
|
-
case 'mariadb':
|
|
88
|
-
return new MariadbSchemaIntrospector(this.pool, schema);
|
|
89
|
-
case 'sqlite':
|
|
90
|
-
return new SqliteSchemaIntrospector(this.pool);
|
|
91
|
-
case 'mongodb':
|
|
92
|
-
return new MongoSchemaIntrospector(this.pool);
|
|
93
|
-
default:
|
|
94
|
-
return undefined;
|
|
95
|
-
}
|
|
76
|
+
return introspectorFor(this.dialectName, this.pool, schema);
|
|
96
77
|
}
|
|
97
78
|
createGenerator() {
|
|
98
79
|
if (!isKnownMigratorDialect(this.dialectName)) {
|
|
@@ -22,7 +22,10 @@ export const mongoDialectFeatures = {
|
|
|
22
22
|
vectorIndexRequiresNotNull: false,
|
|
23
23
|
vectorSupportsLength: false,
|
|
24
24
|
supportsTimestamptz: false,
|
|
25
|
-
|
|
25
|
+
stringSizing: 'bounded-text',
|
|
26
|
+
supportsUnsigned: false,
|
|
27
|
+
multipleCascadePaths: true,
|
|
28
|
+
serverSideCursors: false,
|
|
26
29
|
};
|
|
27
30
|
/** What `toWireId` converts: the hex spelling of an `ObjectId`, and nothing looser. */
|
|
28
31
|
const HEX_24 = /^[0-9a-f]{24}$/i;
|
|
@@ -75,8 +75,8 @@ export declare class MongodbQuerier extends AbstractQuerier {
|
|
|
75
75
|
firstId?: undefined;
|
|
76
76
|
} | {
|
|
77
77
|
changes: number;
|
|
78
|
-
ids: PrimaryKey[];
|
|
79
|
-
firstId: PrimaryKey;
|
|
78
|
+
ids: (PrimaryKey | undefined)[];
|
|
79
|
+
firstId: PrimaryKey | undefined;
|
|
80
80
|
}>;
|
|
81
81
|
protected internalDeleteMany<E extends Document>(entity: Type<E>, qm: QuerySearch<E>, opts?: QueryOptions): Promise<number>;
|
|
82
82
|
get hasOpenTransaction(): boolean;
|
|
@@ -359,12 +359,14 @@ export class MongodbQuerier extends AbstractQuerier {
|
|
|
359
359
|
});
|
|
360
360
|
const res = await this.execute((session) => this.collection(entity).bulkWrite(operations, { session }));
|
|
361
361
|
const changes = (res.upsertedCount ?? 0) + (res.modifiedCount ?? 0);
|
|
362
|
-
// `upsertedIds` only covers newly-inserted documents
|
|
363
|
-
// updated document's `_id`
|
|
364
|
-
//
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
362
|
+
// `upsertedIds` only covers newly-inserted documents, keyed by operation index: a matched and
|
|
363
|
+
// updated document's `_id` is not in the response. Read by that index rather than flattened,
|
|
364
|
+
// so each id lands on the row it belongs to and the gaps stay gaps.
|
|
365
|
+
const ids = payload.map((_, index) => {
|
|
366
|
+
const id = res.upsertedIds[index];
|
|
367
|
+
return id === undefined ? undefined : this.dialect.fromWireId(id);
|
|
368
|
+
});
|
|
369
|
+
return { changes, ids, firstId: ids.find((id) => id !== undefined) };
|
|
368
370
|
});
|
|
369
371
|
}
|
|
370
372
|
async internalDeleteMany(entity, qm, opts = {}) {
|