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.
Files changed (70) hide show
  1. package/README.md +1 -1
  2. package/dist/browser/uql-browser.min.js +2 -2
  3. package/dist/browser/uql-browser.min.js.map +2 -2
  4. package/dist/bunSql/bunSql.util.d.ts +33 -10
  5. package/dist/bunSql/bunSql.util.js +57 -42
  6. package/dist/bunSql/bunSqlQuerier.d.ts +13 -8
  7. package/dist/bunSql/bunSqlQuerier.js +17 -8
  8. package/dist/bunSql/bunSqlQuerierPool.d.ts +10 -2
  9. package/dist/bunSql/bunSqlQuerierPool.js +38 -25
  10. package/dist/bunSql/index.d.ts +1 -3
  11. package/dist/bunSql/index.js +0 -3
  12. package/dist/dialect/abstractSqlDialect.d.ts +61 -7
  13. package/dist/dialect/abstractSqlDialect.js +88 -27
  14. package/dist/dialect/aliases.d.ts +2 -0
  15. package/dist/dialect/aliases.js +2 -0
  16. package/dist/dialect/jsonSql.d.ts +2 -2
  17. package/dist/dialect/jsonSql.js +2 -2
  18. package/dist/dialect/mergeSqlDialect.d.ts +45 -0
  19. package/dist/dialect/mergeSqlDialect.js +89 -0
  20. package/dist/dialect/mysqlLikeSqlDialect.js +4 -1
  21. package/dist/dialect/pgLikeSqlDialect.d.ts +3 -3
  22. package/dist/dialect/pgLikeSqlDialect.js +15 -12
  23. package/dist/migrate/builder/expressions.js +5 -0
  24. package/dist/migrate/introspection/index.d.ts +2 -0
  25. package/dist/migrate/introspection/index.js +2 -0
  26. package/dist/migrate/introspection/mssqlIntrospector.d.ts +63 -0
  27. package/dist/migrate/introspection/mssqlIntrospector.js +198 -0
  28. package/dist/migrate/introspection/postgresIntrospector.js +3 -3
  29. package/dist/migrate/introspection/registry.d.ts +3 -0
  30. package/dist/migrate/introspection/registry.js +28 -0
  31. package/dist/migrate/migrator.js +2 -21
  32. package/dist/mongo/mongoDialect.js +4 -1
  33. package/dist/mongo/mongodbQuerier.d.ts +2 -2
  34. package/dist/mongo/mongodbQuerier.js +8 -6
  35. package/dist/mssql/index.d.ts +3 -0
  36. package/dist/mssql/index.js +3 -0
  37. package/dist/mssql/mssqlDialect.d.ts +144 -0
  38. package/dist/mssql/mssqlDialect.js +328 -0
  39. package/dist/mssql/mssqlQuerier.d.ts +23 -0
  40. package/dist/mssql/mssqlQuerier.js +137 -0
  41. package/dist/mssql/mssqlQuerierPool.d.ts +17 -0
  42. package/dist/mssql/mssqlQuerierPool.js +32 -0
  43. package/dist/mssql/mssqlWireTypes.d.ts +23 -0
  44. package/dist/mssql/mssqlWireTypes.js +44 -0
  45. package/dist/pglite/pgliteQuerier.d.ts +4 -2
  46. package/dist/pglite/pgliteQuerier.js +7 -2
  47. package/dist/postgres/pgCursorStream.d.ts +20 -0
  48. package/dist/postgres/pgCursorStream.js +49 -0
  49. package/dist/postgres/pgDialect.d.ts +1 -1
  50. package/dist/postgres/pgDialect.js +1 -1
  51. package/dist/postgres/postgresWireDriverCapabilities.d.ts +12 -10
  52. package/dist/postgres/postgresWireDriverCapabilities.js +12 -10
  53. package/dist/querier/abstractQuerier.d.ts +3 -7
  54. package/dist/querier/abstractQuerier.js +22 -2
  55. package/dist/querier/abstractQuerierPool.d.ts +3 -3
  56. package/dist/schema/canonicalType.js +96 -113
  57. package/dist/sqlite/sqliteDialect.d.ts +7 -7
  58. package/dist/sqlite/sqliteDialect.js +21 -18
  59. package/dist/type/dialect.d.ts +31 -4
  60. package/dist/type/migratorDialect.d.ts +1 -1
  61. package/dist/type/migratorDialect.js +1 -0
  62. package/dist/type/query.d.ts +29 -6
  63. package/dist/type/universalQuerier.d.ts +3 -3
  64. package/package.json +13 -3
  65. package/dist/bunSql/bunSqlCockroachDialect.d.ts +0 -12
  66. package/dist/bunSql/bunSqlCockroachDialect.js +0 -15
  67. package/dist/bunSql/bunSqlPostgresDialect.d.ts +0 -11
  68. package/dist/bunSql/bunSqlPostgresDialect.js +0 -14
  69. package/dist/bunSql/bunSqliteDialect.d.ts +0 -6
  70. 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 `to_tsvector`/`to_tsquery`
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
- defaultStringAsText: true,
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
- // `unnest` resolves an uncast parameter to `unknown` and refuses it ("function unnest(unknown) is
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 = `unnest(${sources.join(', ')}) AS ${keys}(${joins.map((_, index) => `k${index}`).join(', ')})`;
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
- * `to_tsvector(...) @@ websearch_to_tsquery(...)`. `websearch_to_tsquery` takes free-form user input
145
- * (quoted phrases, `or`, `-negation`) and never raises a syntax error, unlike `to_tsquery`, which
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(`to_tsvector(${config}${fields}) @@ websearch_to_tsquery(${config}`);
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(`jsonb_array_length(${jsonField})`), value));
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 ? 'jsonb_array_elements' : 'jsonb_array_elements_text';
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 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)}`;
213
- return `jsonb_set(${expr}, '{${escapedKey}}', COALESCE((${kept}), '[]'::jsonb), false)`;
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 `jsonb_set(${acc}, '{${escapedKey}}', COALESCE((${acc})->'${escapedKey}', '[]'::jsonb) || jsonb_build_array(${ph}))`;
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 unnest(ix.indkey) WITH ORDINALITY AS k(attnum, n)
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
- array_to_json(array_agg(kcu.column_name ORDER BY kcu.ordinal_position)) AS columns,
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
- array_to_json(array_agg(ccu.column_name ORDER BY kcu.ordinal_position)) AS referenced_columns,
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
+ }
@@ -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 { CockroachSchemaIntrospector, MariadbSchemaIntrospector, MongoSchemaIntrospector, MysqlSchemaIntrospector, PostgresSchemaIntrospector, SqliteSchemaIntrospector, } from './introspection/index.js';
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
- const d = this.dialectName;
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
- defaultStringAsText: false,
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 (keyed by operation index); a matched and
363
- // updated document's `_id` isn't in the response, so it's simply not represented here - same
364
- // "exact where knowable, absent otherwise" convention `RETURNING`-based SQL dialects use for
365
- // rows that hit `DO NOTHING`.
366
- const ids = Object.values(res.upsertedIds).map((id) => this.dialect.fromWireId(id));
367
- return { changes, ids, firstId: ids[0] };
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 = {}) {
@@ -0,0 +1,3 @@
1
+ export * from './mssqlDialect.js';
2
+ export * from './mssqlQuerier.js';
3
+ export * from './mssqlQuerierPool.js';
@@ -0,0 +1,3 @@
1
+ export * from './mssqlDialect.js';
2
+ export * from './mssqlQuerier.js';
3
+ export * from './mssqlQuerierPool.js';