uql-orm 0.79.0 → 0.80.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 (78) hide show
  1. package/dist/browser/uql-browser.min.js.map +2 -2
  2. package/dist/cockroachdb/cockroachDialect.js +5 -1
  3. package/dist/dialect/abstractDialect.d.ts +1 -31
  4. package/dist/dialect/abstractDialect.js +3 -27
  5. package/dist/dialect/abstractSqlDialect.d.ts +25 -56
  6. package/dist/dialect/abstractSqlDialect.js +78 -145
  7. package/dist/dialect/aliases.d.ts +5 -0
  8. package/dist/dialect/aliases.js +5 -0
  9. package/dist/dialect/mysqlLikeSqlDialect.d.ts +4 -2
  10. package/dist/dialect/mysqlLikeSqlDialect.js +14 -1
  11. package/dist/dialect/operators.d.ts +66 -0
  12. package/dist/dialect/operators.js +129 -0
  13. package/dist/dialect/pgLikeSqlDialect.d.ts +4 -1
  14. package/dist/dialect/pgLikeSqlDialect.js +16 -3
  15. package/dist/entity/decorator/entity.d.ts +6 -1
  16. package/dist/entity/decorator/entity.js +12 -1
  17. package/dist/entity/index.d.ts +1 -1
  18. package/dist/entity/index.js +1 -1
  19. package/dist/entity/metadata/definition.d.ts +6 -1
  20. package/dist/entity/metadata/definition.js +19 -0
  21. package/dist/migrate/codegen/entityTypes.js +1 -2
  22. package/dist/migrate/ddl/mssqlIndexDdl.d.ts +5 -0
  23. package/dist/migrate/ddl/mssqlIndexDdl.js +10 -0
  24. package/dist/migrate/ddl/mssqlTableDdl.d.ts +2 -0
  25. package/dist/migrate/ddl/mssqlTableDdl.js +5 -0
  26. package/dist/migrate/ddl/tableDdl.d.ts +2 -0
  27. package/dist/migrate/ddl/tableDdl.js +4 -0
  28. package/dist/migrate/generator/mongoSchemaGenerator.d.ts +4 -0
  29. package/dist/migrate/generator/mongoSchemaGenerator.js +10 -0
  30. package/dist/migrate/introspection/abstractSqlSchemaIntrospector.d.ts +13 -1
  31. package/dist/migrate/introspection/abstractSqlSchemaIntrospector.js +21 -0
  32. package/dist/migrate/introspection/mongoIntrospector.d.ts +3 -1
  33. package/dist/migrate/introspection/mongoIntrospector.js +4 -0
  34. package/dist/migrate/introspection/mssqlIntrospector.d.ts +1 -0
  35. package/dist/migrate/introspection/mssqlIntrospector.js +8 -0
  36. package/dist/migrate/introspection/mysqlIntrospector.d.ts +1 -0
  37. package/dist/migrate/introspection/mysqlIntrospector.js +9 -0
  38. package/dist/migrate/introspection/postgresIntrospector.d.ts +1 -0
  39. package/dist/migrate/introspection/postgresIntrospector.js +10 -0
  40. package/dist/migrate/introspection/sqliteIntrospector.d.ts +1 -0
  41. package/dist/migrate/introspection/sqliteIntrospector.js +3 -0
  42. package/dist/migrate/migrator.d.ts +28 -1
  43. package/dist/migrate/migrator.js +88 -9
  44. package/dist/migrate/schemaGenerator.d.ts +12 -1
  45. package/dist/migrate/schemaGenerator.js +47 -5
  46. package/dist/migrate/storage/databaseStorage.d.ts +4 -0
  47. package/dist/migrate/storage/databaseStorage.js +14 -8
  48. package/dist/migrate/triggerSql.d.ts +24 -0
  49. package/dist/migrate/triggerSql.js +229 -0
  50. package/dist/mongo/mongoDialect.d.ts +0 -21
  51. package/dist/mongo/mongoDialect.js +105 -100
  52. package/dist/mongo/mongodbQuerier.js +17 -1
  53. package/dist/mssql/mssqlDialect.d.ts +18 -7
  54. package/dist/mssql/mssqlDialect.js +77 -33
  55. package/dist/mssql/mssqlQuerier.js +2 -2
  56. package/dist/schema/canonicalType.d.ts +6 -1
  57. package/dist/schema/canonicalType.js +14 -0
  58. package/dist/schema/schemaASTBuilder.d.ts +2 -8
  59. package/dist/schema/schemaASTBuilder.js +6 -20
  60. package/dist/sqlite/sqliteDialect.d.ts +1 -1
  61. package/dist/sqlite/sqliteDialect.js +12 -3
  62. package/dist/type/dialect.d.ts +69 -9
  63. package/dist/type/entity.d.ts +96 -3
  64. package/dist/type/migration.d.ts +17 -0
  65. package/dist/type/query.d.ts +13 -4
  66. package/dist/type/queryWhere.d.ts +4 -2
  67. package/dist/util/field.util.d.ts +9 -1
  68. package/dist/util/field.util.js +14 -2
  69. package/dist/util/fieldOption.util.d.ts +2 -2
  70. package/dist/util/fieldOption.util.js +2 -2
  71. package/dist/util/raw.d.ts +9 -1
  72. package/dist/util/raw.js +36 -11
  73. package/dist/util/sql.util.d.ts +12 -0
  74. package/dist/util/sql.util.js +24 -3
  75. package/dist/util/uqlError.d.ts +2 -0
  76. package/dist/util/uqlError.js +4 -0
  77. package/package.json +4 -4
  78. package/skills/uql-orm/SKILL.md +11 -6
@@ -0,0 +1,129 @@
1
+ import { VECTOR_QUERY_KEYS, } from '../type/index.js';
2
+ import { isOperatorMap } from '../util/dialect.util.js';
3
+ import { kindOf, UqlUsageError } from '../util/uqlError.js';
4
+ /**
5
+ * How each grouping operator renders: the operator joining its clauses, and whether the group is
6
+ * negated (`$not` is `NOT (a AND b)`). Total over {@link QueryGroupOp}.
7
+ */
8
+ export const GROUP_OPS = {
9
+ $and: { join: '$and', negate: false },
10
+ $or: { join: '$or', negate: false },
11
+ $not: { join: '$and', negate: true },
12
+ $nor: { join: '$or', negate: true },
13
+ };
14
+ /** Whether a `$where` key groups clauses, narrowing it for the renderers that read {@link GROUP_OPS}. */
15
+ export function isGroupOp(key) {
16
+ return Object.hasOwn(GROUP_OPS, key);
17
+ }
18
+ /**
19
+ * A group operator's clauses, rejecting what the types do not cover: `/http` casts client JSON
20
+ * straight to `Query`, so a scalar can arrive where an array belongs. Shared so both backends
21
+ * refuse the same payload rather than one throwing and the other failing further in.
22
+ */
23
+ export function groupClauses(key, val) {
24
+ if (val !== undefined && !Array.isArray(val)) {
25
+ throw new UqlUsageError(`${key} expects an array, got ${kindOf(val)}`);
26
+ }
27
+ return val ?? [];
28
+ }
29
+ /**
30
+ * A WHERE value as the operators it applies: an array is `$in`, an operator map its own entries, and
31
+ * anything else `$eq`. Each key is checked here, once, refused as `refusal` where it is no operator.
32
+ */
33
+ export function whereOperators(val, refusal) {
34
+ const ops = Array.isArray(val) ? { $in: val } : isOperatorMap(val) ? val : { $eq: val };
35
+ return Object.entries(ops).map(([op, value]) => {
36
+ if (!isFieldOp(op)) {
37
+ throw new UqlUsageError(`${refusal}: ${op}`);
38
+ }
39
+ return [op, value];
40
+ });
41
+ }
42
+ /** An `$in`/`$nin` operand, which the types require to be an array but `/http` hands over untyped. */
43
+ export function inOperands(op, value) {
44
+ if (!Array.isArray(value)) {
45
+ throw new UqlUsageError(`${op} expects an array, got ${kindOf(value)}`);
46
+ }
47
+ return value;
48
+ }
49
+ /** A `$between`'s two bounds, which `/http` hands over untyped too; one missing would bind `undefined`. */
50
+ export function betweenBounds(value) {
51
+ if (!Array.isArray(value) || value.length !== 2) {
52
+ const got = Array.isArray(value) ? `${value.length} values` : kindOf(value);
53
+ throw new UqlUsageError(`$between expects [min, max], got ${got}`);
54
+ }
55
+ const [min, max] = value;
56
+ return [min, max];
57
+ }
58
+ /**
59
+ * Every field operator as data, which is what checks a key `/http` hands over; `satisfies` fails the
60
+ * build on one {@link QueryWhereFieldOp} gains and this misses.
61
+ */
62
+ const FIELD_OPS = {
63
+ $eq: true,
64
+ $ne: true,
65
+ $not: true,
66
+ $lt: true,
67
+ $lte: true,
68
+ $gt: true,
69
+ $gte: true,
70
+ $between: true,
71
+ $startsWith: true,
72
+ $istartsWith: true,
73
+ $endsWith: true,
74
+ $iendsWith: true,
75
+ $includes: true,
76
+ $iincludes: true,
77
+ $like: true,
78
+ $ilike: true,
79
+ $regex: true,
80
+ $in: true,
81
+ $nin: true,
82
+ $isNull: true,
83
+ $isNotNull: true,
84
+ $all: true,
85
+ $size: true,
86
+ $elemMatch: true,
87
+ $near: true,
88
+ };
89
+ function isFieldOp(op) {
90
+ return Object.hasOwn(FIELD_OPS, op);
91
+ }
92
+ /** Whether a `$near` names the vector it measures from, which its type requires and `/http` does not. */
93
+ export function isVectorQuery(near) {
94
+ return Boolean(near['$vector']);
95
+ }
96
+ export const COMPARE_OPS = new Map([
97
+ ['$gt', ' > '],
98
+ ['$gte', ' >= '],
99
+ ['$lt', ' < '],
100
+ ['$lte', ' <= '],
101
+ ]);
102
+ /**
103
+ * The ordered comparisons, `QueryOrderedOp` at runtime, derived from {@link COMPARE_OPS} rather than spelled
104
+ * again: {@link QueryVectorNear}'s bounds, so `$near` never accepts one the renderer has no operator
105
+ * for, and the operators that read a JSON path as a number.
106
+ */
107
+ export const ORDERED_OPS = new Set([...COMPARE_OPS.keys(), '$between']);
108
+ export function isOrderedOp(op) {
109
+ return ORDERED_OPS.has(op);
110
+ }
111
+ /** The operators an equality compares by value, which a JSON path reads the way that value compares. */
112
+ export const EQUALITY_OPS = new Set(['$eq', '$ne', '$in', '$nin']);
113
+ /**
114
+ * Every `$like`-family operator: the pattern it wraps its value in, and whether it ignores case.
115
+ * Each case-sensitive operator is paired here with the `$i` twin that shares its pattern, so the
116
+ * two can never drift apart - and neither one decides case folding, which is
117
+ * `AbstractSqlDialect.caseInsensitiveMatch`'s single call.
118
+ */
119
+ export const LIKE_OPS = new Map([
120
+ ['$like', '$ilike', (v) => v],
121
+ ['$startsWith', '$istartsWith', (v) => `${v}%`],
122
+ ['$endsWith', '$iendsWith', (v) => `%${v}`],
123
+ ['$includes', '$iincludes', (v) => `%${v}%`],
124
+ ].flatMap(([sensitive, insensitive, pattern]) => [
125
+ [sensitive, { pattern, insensitive: false }],
126
+ [insensitive, { pattern, insensitive: true }],
127
+ ]));
128
+ /** What a `$near` says about the search itself; everything else in it is a bound. */
129
+ export const VECTOR_QUERY_KEY_SET = new Set(VECTOR_QUERY_KEYS);
@@ -1,4 +1,5 @@
1
1
  import type { IndexType } from '../schema/types.js';
2
+ import type { SqlDialectName } from '../type/index.js';
2
3
  import { type DriverCapabilities, type EntityMeta, type FieldOptions, type JsonColumnType, type Query, type QueryContext, type QueryTextSearchOptions, type SqlDialectFeatures, type VectorDistance, type VectorMetric } from '../type/index.js';
3
4
  import type { DialectOptions } from './abstractDialect.js';
4
5
  import { AbstractSqlDialect, type RelationRows } from './abstractSqlDialect.js';
@@ -13,6 +14,8 @@ export declare const PG_VECTOR_METRICS: ReadonlyMap<VectorDistance, VectorMetric
13
14
  export declare const PG_FEATURES: SqlDialectFeatures;
14
15
  /** What Postgres and CockroachDB share: JSONB, full-text search, pgvector's operators, and the upsert. */
15
16
  export declare abstract class PgLikeSqlDialect extends AbstractSqlDialect {
17
+ /** Every member of this family runs the same SQL, so a body written once serves them all. */
18
+ get dialectFamily(): SqlDialectName;
16
19
  /** How the driver binds a parameter: node-`pg`'s, unless the pool states its own. */
17
20
  readonly driverCapabilities: DriverCapabilities;
18
21
  constructor(options?: PgLikeDialectOptions);
@@ -96,7 +99,7 @@ export declare abstract class PgLikeSqlDialect extends AbstractSqlDialect {
96
99
  jsonPathExpr(escapedColumn: string, path: string, mode: JsonAccessMode): string;
97
100
  protected get regexpOp(): string;
98
101
  protected readonly caseInsensitiveMatch = "ilike";
99
- protected get neOp(): string;
102
+ neExpr(field: string, ph: string): string;
100
103
  /**
101
104
  * One array parameter, which a context that inlines values has none of: it lists them instead. The
102
105
  * array takes its type from `operand`, so it needs none of the casts `bind` would give each value.
@@ -17,7 +17,6 @@ export const PG_VECTOR_METRICS = new Map([
17
17
  const HNSW_EF_SEARCH = 'hnsw.ef_search';
18
18
  /** What the Postgres-wire engines have. */
19
19
  export const PG_FEATURES = {
20
- ifNotExists: true,
21
20
  indexIfNotExists: true,
22
21
  schemas: true,
23
22
  dropTableCascade: true,
@@ -41,9 +40,23 @@ export const PG_FEATURES = {
41
40
  narrowVectorTypes: false,
42
41
  vectorTuningNeedsTransaction: true,
43
42
  serialDeclaresPrimaryKey: false,
43
+ triggers: {
44
+ preamble: '',
45
+ assignsRow: true,
46
+ body: 'function',
47
+ guards: 'clause',
48
+ layout: 'timingFirst',
49
+ rows: 'row',
50
+ scope: 'table',
51
+ before: true,
52
+ },
44
53
  };
45
54
  /** What Postgres and CockroachDB share: JSONB, full-text search, pgvector's operators, and the upsert. */
46
55
  export class PgLikeSqlDialect extends AbstractSqlDialect {
56
+ /** Every member of this family runs the same SQL, so a body written once serves them all. */
57
+ get dialectFamily() {
58
+ return 'postgres';
59
+ }
47
60
  /** How the driver binds a parameter: node-`pg`'s, unless the pool states its own. */
48
61
  driverCapabilities;
49
62
  constructor(options = {}) {
@@ -213,8 +226,8 @@ export class PgLikeSqlDialect extends AbstractSqlDialect {
213
226
  return '~';
214
227
  }
215
228
  caseInsensitiveMatch = 'ilike';
216
- get neOp() {
217
- return 'IS DISTINCT FROM';
229
+ neExpr(field, ph) {
230
+ return `${field} IS DISTINCT FROM ${ph}`;
218
231
  }
219
232
  /**
220
233
  * One array parameter, which a context that inlines values has none of: it lists them instead. The
@@ -1,4 +1,4 @@
1
- import type { EntityIndexColumnInput, EntityIndexOptions, EntityOptions, FilterName, FilterOptions, RefMap, Type } from '../../type/index.js';
1
+ import type { EntityIndexColumnInput, EntityIndexOptions, EntityOptions, FilterName, FilterOptions, RefMap, TriggerOptions, Type } from '../../type/index.js';
2
2
  /**
3
3
  * Marks a class as an entity and finalizes its metadata, draining `context.metadata`: the class gets
4
4
  * `Symbol.metadata` only after its decorators return.
@@ -16,3 +16,8 @@ export declare function Filter<E, N extends string>(name: FilterName<N>, opts: F
16
16
  * @example `@Index((user) => [user.lastName, raw`lower(${user.email})`], { unique: true })`
17
17
  */
18
18
  export declare function Index<E>(columns: (refs: RefMap<E>) => readonly EntityIndexColumnInput<E>[], options?: EntityIndexOptions<E>): (entity: Type<E>) => void;
19
+ /**
20
+ * Declares triggers the database runs, in the order written. Stacks, so several may sit above one class.
21
+ * @example ``@Trigger({ on: 'beforeUpdate', of: (post) => [post.body], run: (newRow) => raw`...` })``
22
+ */
23
+ export declare function Trigger<E>(...triggers: readonly TriggerOptions<E>[]): (entity: Type<E>) => void;
@@ -1,4 +1,4 @@
1
- import { applyMembers, defineEntity, defineFilter, defineIndex } from '../metadata/definition.js';
1
+ import { applyMembers, defineEntity, defineFilter, defineIndex, defineTrigger } from '../metadata/definition.js';
2
2
  import { drainRegistrations } from './bag.js';
3
3
  // The class-level decorators. Unlike the member ones they receive the class, so each is a direct call
4
4
  // into the registry with no bag in between.
@@ -32,3 +32,14 @@ export function Index(columns, options = {}) {
32
32
  defineIndex(entity, { ...options, columns });
33
33
  };
34
34
  }
35
+ /**
36
+ * Declares triggers the database runs, in the order written. Stacks, so several may sit above one class.
37
+ * @example ``@Trigger({ on: 'beforeUpdate', of: (post) => [post.body], run: (newRow) => raw`...` })``
38
+ */
39
+ export function Trigger(...triggers) {
40
+ return (entity) => {
41
+ for (const trigger of triggers) {
42
+ defineTrigger(entity, trigger);
43
+ }
44
+ };
45
+ }
@@ -1,3 +1,3 @@
1
1
  export * from './decorator/entity.js';
2
2
  export * from './decorator/members.js';
3
- export { defineEntity, defineField, defineFilter, defineHook, defineId, defineIndex, defineRelation, getEntities, getMeta, removeEntity, assertSoleId, fieldOf, idOf, namesKey, relationOf, soleIdOf, } from './metadata/definition.js';
3
+ export { defineEntity, defineField, defineFilter, defineHook, defineId, defineIndex, defineRelation, defineTrigger, getEntities, getMeta, removeEntity, assertSoleId, fieldOf, idOf, namesKey, relationOf, soleIdOf, } from './metadata/definition.js';
@@ -1,3 +1,3 @@
1
1
  export * from './decorator/entity.js';
2
2
  export * from './decorator/members.js';
3
- export { defineEntity, defineField, defineFilter, defineHook, defineId, defineIndex, defineRelation, getEntities, getMeta, removeEntity, assertSoleId, fieldOf, idOf, namesKey, relationOf, soleIdOf, } from './metadata/definition.js';
3
+ export { defineEntity, defineField, defineFilter, defineHook, defineId, defineIndex, defineRelation, defineTrigger, getEntities, getMeta, removeEntity, assertSoleId, fieldOf, idOf, namesKey, relationOf, soleIdOf, } from './metadata/definition.js';
@@ -1,4 +1,4 @@
1
- import type { EntityIndexInput, EntityMembers, EntityMeta, EntityOptions, FieldMeta, FieldOptions, FilterName, FilterOptions, HookEvent, IdKey, RelationKey, RelationMeta, RelationOptions, RelationRegistration, Type, WrittenId } from '../../type/index.js';
1
+ import type { EntityIndexInput, EntityMembers, EntityMeta, EntityOptions, FieldMeta, FieldOptions, FilterName, FilterOptions, HookEvent, IdKey, RelationKey, RelationMeta, RelationOptions, RelationRegistration, TriggerOptions, Type, WrittenId } from '../../type/index.js';
2
2
  export declare function defineField<E>(entity: Type<E>, key: string, opts?: FieldOptions): EntityMeta<E>;
3
3
  export declare function defineId<E>(entity: Type<E>, key: string, opts: FieldOptions): EntityMeta<E>;
4
4
  /** `T` is the relation's target, independent of the owner `E`. */
@@ -15,6 +15,11 @@ export declare function defineHook<E>(entity: Type<E>, methodName: string, event
15
15
  * sugar are normalized here, which is what lets the dialects render one shape instead of re-parsing it.
16
16
  */
17
17
  export declare function defineIndex<E>(entity: Type<E>, index: EntityIndexInput<E>): EntityMeta<E>;
18
+ /**
19
+ * Registers a trigger, its watched columns read off the entity's refs so a rename reaches them and a
20
+ * column the entity has not got does not compile. Appended, since triggers fire in the order written.
21
+ */
22
+ export declare function defineTrigger<E>(entity: Type<E>, trigger: TriggerOptions<E>): EntityMeta<E>;
18
23
  export declare function defineFilter<E, N extends string>(entity: Type<E>, name: FilterName<N>, opts: FilterOptions<E>): EntityMeta<E>;
19
24
  /**
20
25
  * Feeds fields, relations and hooks into the `define*` primitives, so the decorators and the imperative
@@ -112,6 +112,22 @@ export function defineIndex(entity, index) {
112
112
  });
113
113
  return meta;
114
114
  }
115
+ /**
116
+ * Registers a trigger, its watched columns read off the entity's refs so a rename reaches them and a
117
+ * column the entity has not got does not compile. Appended, since triggers fire in the order written.
118
+ */
119
+ export function defineTrigger(entity, trigger) {
120
+ const meta = ensureWritableMeta(entity);
121
+ // The type already refuses an empty map; this is the same answer for plain JavaScript.
122
+ if (typeof trigger.run !== 'function' && !hasKeys(trigger.run)) {
123
+ throw new TypeError(`'${entity.name}' has a trigger whose body names at least one engine to run on`);
124
+ }
125
+ if (trigger.name && meta.triggers?.some((it) => it.name === trigger.name)) {
126
+ throw new TypeError(`'${entity.name}' already has a trigger named '${trigger.name}'`);
127
+ }
128
+ (meta.triggers ??= []).push({ ...trigger, of: trigger.of?.(memberRefs()).map((ref) => ref.key) });
129
+ return meta;
130
+ }
115
131
  export function defineFilter(entity, name, opts) {
116
132
  const meta = ensureWritableMeta(entity);
117
133
  if (name === SOFT_DELETE_FILTER) {
@@ -178,6 +194,9 @@ export function defineEntity(entity, opts = {}) {
178
194
  for (const index of opts.indexes ?? []) {
179
195
  defineIndex(entity, index);
180
196
  }
197
+ for (const trigger of opts.triggers ?? []) {
198
+ defineTrigger(entity, trigger);
199
+ }
181
200
  for (const [name, filter] of definedEntries(opts.filters ?? {})) {
182
201
  defineFilter(entity, name, filter);
183
202
  }
@@ -1,6 +1,5 @@
1
1
  import { getMeta } from '../../entity/index.js';
2
- import { canonicalToTypeScript } from '../../schema/canonicalType.js';
3
- import { resolveColumnCanonicalType } from '../../schema/schemaASTBuilder.js';
2
+ import { canonicalToTypeScript, resolveColumnCanonicalType } from '../../schema/canonicalType.js';
4
3
  import { isToManyRelation, upperFirst } from '../../util/index.js';
5
4
  import { isIdentifierName } from './sourceLiteral.js';
6
5
  /**
@@ -1,3 +1,4 @@
1
+ import type { IndexSchema } from '../../type/index.js';
1
2
  import { IndexDdl } from './indexDdl.js';
2
3
  /**
3
4
  * SQL Server's `CREATE INDEX` is the portable form minus what 2025 rejects: an expression (Msg 16216),
@@ -7,4 +8,8 @@ import { IndexDdl } from './indexDdl.js';
7
8
  export declare class MsSqlIndexDdl extends IndexDdl {
8
9
  protected readonly indexFeatures: Set<"expression" | "include" | "jsonArray" | "jsonPath" | "nullsOrder" | "opsClass" | "partial" | "prefixLength">;
9
10
  protected readonly indexTypes: Set<"brin" | "btree" | "fulltext" | "gin" | "gist" | "hash" | "hnsw" | "ivfflat" | "vector" | "vectorSearch">;
11
+ /** T-SQL has no `IF NOT EXISTS` on an index, so the create is guarded by a lookup in the same statement. */
12
+ getCreateIndexStatement(tableName: string, index: IndexSchema, opts?: {
13
+ ifNotExists?: boolean;
14
+ }): string;
10
15
  }
@@ -7,4 +7,14 @@ import { IndexDdl } from './indexDdl.js';
7
7
  export class MsSqlIndexDdl extends IndexDdl {
8
8
  indexFeatures = new Set(['partial']);
9
9
  indexTypes = new Set(['btree']);
10
+ /** T-SQL has no `IF NOT EXISTS` on an index, so the create is guarded by a lookup in the same statement. */
11
+ getCreateIndexStatement(tableName, index, opts = {}) {
12
+ const create = super.getCreateIndexStatement(tableName, index, { ifNotExists: false });
13
+ if (!(opts.ifNotExists ?? this.dialect.features.indexIfNotExists)) {
14
+ return create;
15
+ }
16
+ const table = this.dialect.escape(this.dialect.escapeId(tableName));
17
+ const name = this.dialect.escape(index.name);
18
+ return `IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = ${name} AND object_id = OBJECT_ID(${table})) ${create}`;
19
+ }
10
20
  }
@@ -6,6 +6,8 @@ import { TableDdl } from './tableDdl.js';
6
6
  * databases name them alike. Renames are `sp_rename`, T-SQL having no `RENAME` clause.
7
7
  */
8
8
  export declare class MsSqlTableDdl extends TableDdl {
9
+ /** T-SQL has no `IF NOT EXISTS` on a table, so the create is guarded by a lookup in the same statement. */
10
+ createTable(target: string, ifNotExists: boolean): string;
9
11
  /** T-SQL rejects the optional `COLUMN` keyword after `ADD`. */
10
12
  addColumn(table: string, definition: string): string;
11
13
  /** Its constraints go with the column, as they do on every other engine. */
@@ -16,6 +16,11 @@ const CONSTRAINTS_ON = {
16
16
  * databases name them alike. Renames are `sp_rename`, T-SQL having no `RENAME` clause.
17
17
  */
18
18
  export class MsSqlTableDdl extends TableDdl {
19
+ /** T-SQL has no `IF NOT EXISTS` on a table, so the create is guarded by a lookup in the same statement. */
20
+ createTable(target, ifNotExists) {
21
+ const guard = ifNotExists ? `IF OBJECT_ID(${this.dialect.escape(target)}, N'U') IS NULL ` : '';
22
+ return `${guard}${super.createTable(target, false)}`;
23
+ }
19
24
  /** T-SQL rejects the optional `COLUMN` keyword after `ADD`. */
20
25
  addColumn(table, definition) {
21
26
  return /*sql*/ `ALTER TABLE ${this.dialect.escapeId(table)} ADD ${definition};`;
@@ -12,6 +12,8 @@ export declare function sizedType(column: Pick<ColumnSchema, 'type' | 'length' |
12
12
  export declare class TableDdl {
13
13
  protected readonly dialect: AbstractSqlDialect;
14
14
  constructor(dialect: AbstractSqlDialect);
15
+ /** A `CREATE TABLE` up to its column list, a no-op where `ifNotExists` and the table is already there. */
16
+ createTable(target: string, ifNotExists: boolean): string;
15
17
  addColumn(table: string, definition: string): string;
16
18
  dropColumn(table: string, column: string): string[];
17
19
  /**
@@ -22,6 +22,10 @@ export class TableDdl {
22
22
  constructor(dialect) {
23
23
  this.dialect = dialect;
24
24
  }
25
+ /** A `CREATE TABLE` up to its column list, a no-op where `ifNotExists` and the table is already there. */
26
+ createTable(target, ifNotExists) {
27
+ return `CREATE TABLE ${ifNotExists ? 'IF NOT EXISTS ' : ''}${target}`;
28
+ }
25
29
  addColumn(table, definition) {
26
30
  return `ALTER TABLE ${this.dialect.escapeId(table)} ADD COLUMN ${definition};`;
27
31
  }
@@ -33,6 +33,10 @@ export declare class MongoSchemaGenerator extends MongoDialect implements Schema
33
33
  generateDropTable(tableName: string): string;
34
34
  generateAlterTable(diff: SchemaDiff): string[];
35
35
  generateAlterTableDown(diff: SchemaDiff): string[];
36
+ /** MongoDB has no triggers, and a write to an entity declaring one is refused, so there is none to reconcile. */
37
+ generateTriggers(): string[];
38
+ generateTriggersDown(): string[];
39
+ generateTriggerDrops(): string[];
36
40
  private dropIndexCommand;
37
41
  /** An index as MongoDB's key spec (`-1` descending, `'text'` full-text), refusing the SQL-only options. */
38
42
  generateCreateIndex(tableName: string, index: IndexSchema): string;
@@ -118,6 +118,16 @@ export class MongoSchemaGenerator extends MongoDialect {
118
118
  ...(diff.indexesToDrop ?? []).map((index) => this.generateCreateIndex(diff.tableName, index)),
119
119
  ];
120
120
  }
121
+ /** MongoDB has no triggers, and a write to an entity declaring one is refused, so there is none to reconcile. */
122
+ generateTriggers() {
123
+ return [];
124
+ }
125
+ generateTriggersDown() {
126
+ return [];
127
+ }
128
+ generateTriggerDrops() {
129
+ return [];
130
+ }
121
131
  dropIndexCommand(tableName, index) {
122
132
  return index.type === 'vectorSearch'
123
133
  ? serializeMongoCommand({ action: 'dropSearchIndex', collection: tableName, name: index.name })
@@ -1,5 +1,5 @@
1
1
  import { type ForeignKeyAction } from '../../schema/types.js';
2
- import type { ColumnSchema, ForeignKeySchema, IndexSchema, QuerierPool, RawRow, SchemaIntrospector, SqlQuerier, TableSchema } from '../../type/index.js';
2
+ import type { ColumnSchema, InstalledTriggers, ForeignKeySchema, IndexSchema, QuerierPool, RawRow, SchemaIntrospector, SqlQuerier, TableSchema } from '../../type/index.js';
3
3
  import { BaseSqlIntrospector } from './baseSqlIntrospector.js';
4
4
  /**
5
5
  * Reads the rows of one statement while introspecting a table.
@@ -38,6 +38,12 @@ export declare abstract class AbstractSqlSchemaIntrospector extends BaseSqlIntro
38
38
  protected readonly defaultSchemaExpr: string;
39
39
  getTableSchema(tableName: string): Promise<TableSchema | undefined>;
40
40
  getTableNames(): Promise<string[]>;
41
+ /**
42
+ * Every trigger uql installed in this schema, by table and then by name, with the statements recreating
43
+ * it: the whole schema in one read, since a table whose entity stopped declaring one still has one to
44
+ * drop. Ownership is matched here rather than with `LIKE`, whose `_` wildcard would take in a hand-written one.
45
+ */
46
+ ownedTriggers(): Promise<Map<string, InstalledTriggers>>;
41
47
  tableExists(tableName: string): Promise<boolean>;
42
48
  /**
43
49
  * Introspection reads, so `withQuerier` rather than `transaction`: the pool owns the release either
@@ -79,6 +85,12 @@ export declare abstract class AbstractSqlSchemaIntrospector extends BaseSqlIntro
79
85
  protected abstract getForeignKeysQuery(tableName: string): string;
80
86
  /** SQL query to get primary key columns. Parameter: tableName (for PRAGMA-style). */
81
87
  protected abstract getPrimaryKeyQuery(tableName: string): string;
88
+ /**
89
+ * SQL listing every trigger in the schema as a `table`, a `name`, the engine's reprint of it as a
90
+ * `definition`, and where the body lives apart, the `requires` recreated first: what is installed, not
91
+ * what uql wrote, which is exactly what a rollback puts back.
92
+ */
93
+ protected abstract triggersQuery(): string;
82
94
  /**
83
95
  * Extract table name from a row returned by getTableNamesQuery.
84
96
  *
@@ -1,5 +1,6 @@
1
1
  import { FOREIGN_KEY_ACTIONS } from '../../schema/types.js';
2
2
  import { isSqlQuerier } from '../../type/index.js';
3
+ import { isOwnedName } from '../../util/sql.util.js';
3
4
  import { BaseSqlIntrospector } from './baseSqlIntrospector.js';
4
5
  /** A SQL introspector: an engine states its catalogue queries (`get*Query`) and how their rows map (`map*Result`). */
5
6
  export class AbstractSqlSchemaIntrospector extends BaseSqlIntrospector {
@@ -50,6 +51,26 @@ export class AbstractSqlSchemaIntrospector extends BaseSqlIntrospector {
50
51
  return results.map((row) => this.mapTableNameRow(row));
51
52
  });
52
53
  }
54
+ /**
55
+ * Every trigger uql installed in this schema, by table and then by name, with the statements recreating
56
+ * it: the whole schema in one read, since a table whose entity stopped declaring one still has one to
57
+ * drop. Ownership is matched here rather than with `LIKE`, whose `_` wildcard would take in a hand-written one.
58
+ */
59
+ async ownedTriggers() {
60
+ return this.withSqlQuerier(async (querier) => {
61
+ const byTable = new Map();
62
+ for (const row of await querier.all(this.triggersQuery())) {
63
+ const name = String(row['name']);
64
+ if (!isOwnedName(name)) {
65
+ continue;
66
+ }
67
+ const table = String(row['table']);
68
+ const statements = [row['requires'], row['definition']].filter(Boolean).map((sql) => String(sql));
69
+ byTable.set(table, (byTable.get(table) ?? new Map()).set(name, statements));
70
+ }
71
+ return byTable;
72
+ });
73
+ }
53
74
  async tableExists(tableName) {
54
75
  return this.withSqlQuerier((querier) => this.tableExistsInternal(createTableRowReader(querier), tableName));
55
76
  }
@@ -1,6 +1,6 @@
1
1
  import type { IndexFacet } from '../../schema/indexDifferences.js';
2
2
  import { SchemaAST } from '../../schema/schemaAST.js';
3
- import { type QuerierPool, type SchemaIntrospector, type TableSchema } from '../../type/index.js';
3
+ import { type InstalledTriggers, type QuerierPool, type SchemaIntrospector, type TableSchema } from '../../type/index.js';
4
4
  /**
5
5
  * MongoDB schema introspector.
6
6
  * MongoDB doesn't have a fixed schema, so this primarily focuses on collections and indexes.
@@ -10,6 +10,8 @@ export declare class MongoSchemaIntrospector implements SchemaIntrospector {
10
10
  /** `listIndexes` reports keys, uniqueness and text weights; a `partialFilterExpression` is no SQL predicate. */
11
11
  readonly indexFacets: ReadonlySet<IndexFacet>;
12
12
  constructor(pool: QuerierPool);
13
+ /** MongoDB has no triggers, so none is ever installed. */
14
+ ownedTriggers(): Promise<Map<string, InstalledTriggers>>;
13
15
  introspect(tables?: readonly string[]): Promise<SchemaAST>;
14
16
  getTableSchema(tableName: string): Promise<TableSchema | undefined>;
15
17
  /** Collections only, the way a SQL engine lists its base tables: no view, nor the `system.views` behind one. */
@@ -14,6 +14,10 @@ export class MongoSchemaIntrospector {
14
14
  constructor(pool) {
15
15
  this.pool = pool;
16
16
  }
17
+ /** MongoDB has no triggers, so none is ever installed. */
18
+ async ownedTriggers() {
19
+ return new Map();
20
+ }
17
21
  async introspect(tables) {
18
22
  const tableNames = tables ?? (await this.getTableNames());
19
23
  const ast = new SchemaAST();
@@ -10,6 +10,7 @@ import { AbstractSqlSchemaIntrospector, type JoinedForeignKeyRow, type TableRowR
10
10
  */
11
11
  export declare class MsSqlSchemaIntrospector extends AbstractSqlSchemaIntrospector {
12
12
  protected readonly defaultSchemaExpr = "SCHEMA_NAME()";
13
+ protected triggersQuery(): string;
13
14
  protected getTableNamesQuery(): string;
14
15
  protected tableExistsQuery(): string;
15
16
  protected parseTableExistsResult([row]: {
@@ -9,6 +9,14 @@ import { AbstractSqlSchemaIntrospector, } from './abstractSqlSchemaIntrospector.
9
9
  */
10
10
  export class MsSqlSchemaIntrospector extends AbstractSqlSchemaIntrospector {
11
11
  defaultSchemaExpr = 'SCHEMA_NAME()';
12
+ triggersQuery() {
13
+ return /*sql*/ `
14
+ SELECT OBJECT_NAME(t.parent_id) AS [table], t.name AS name, m.definition AS definition
15
+ FROM sys.triggers t
16
+ JOIN sys.sql_modules m ON m.object_id = t.object_id
17
+ WHERE t.parent_id <> 0 AND OBJECT_SCHEMA_NAME(t.parent_id) = ${this.schemaExpr}
18
+ `;
19
+ }
12
20
  getTableNamesQuery() {
13
21
  return /*sql*/ `
14
22
  SELECT TABLE_NAME as table_name
@@ -6,6 +6,7 @@ import { AbstractSqlSchemaIntrospector, type JoinedForeignKeyRow, type TableRowR
6
6
  */
7
7
  export declare class MysqlSchemaIntrospector extends AbstractSqlSchemaIntrospector {
8
8
  protected readonly defaultSchemaExpr = "DATABASE()";
9
+ protected triggersQuery(): string;
9
10
  protected getTableNamesQuery(): string;
10
11
  protected tableExistsQuery(): string;
11
12
  protected parseTableExistsResult([row]: {
@@ -7,6 +7,15 @@ import { AbstractSqlSchemaIntrospector, } from './abstractSqlSchemaIntrospector.
7
7
  export class MysqlSchemaIntrospector extends AbstractSqlSchemaIntrospector {
8
8
  // A MySQL "schema" is a database, so the connection's own is what `DATABASE()` reports.
9
9
  defaultSchemaExpr = 'DATABASE()';
10
+ triggersQuery() {
11
+ return /*sql*/ `
12
+ SELECT EVENT_OBJECT_TABLE AS \`table\`, TRIGGER_NAME AS name,
13
+ CONCAT('CREATE TRIGGER \`', TRIGGER_SCHEMA, '\`.\`', TRIGGER_NAME, '\` ', ACTION_TIMING, ' ', EVENT_MANIPULATION,
14
+ ' ON \`', EVENT_OBJECT_SCHEMA, '\`.\`', EVENT_OBJECT_TABLE, '\` FOR EACH ROW ', ACTION_STATEMENT) AS definition
15
+ FROM information_schema.TRIGGERS
16
+ WHERE TRIGGER_SCHEMA = ${this.schemaExpr}
17
+ `;
18
+ }
10
19
  getTableNamesQuery() {
11
20
  return /*sql*/ `
12
21
  SELECT TABLE_NAME as table_name
@@ -11,6 +11,7 @@ export declare class PostgresSchemaIntrospector extends AbstractSqlSchemaIntrosp
11
11
  * database reprints in its own words, so they are not comparable and are not claimed here.
12
12
  */
13
13
  readonly indexFacets: ReadonlySet<IndexFacet>;
14
+ protected triggersQuery(): string;
14
15
  protected getTableNamesQuery(): string;
15
16
  protected tableExistsQuery(): string;
16
17
  protected parseTableExistsResult([row]: RawRow[]): boolean;
@@ -16,6 +16,16 @@ export class PostgresSchemaIntrospector extends AbstractSqlSchemaIntrospector {
16
16
  'accessMethod',
17
17
  'include',
18
18
  ]);
19
+ triggersQuery() {
20
+ return /*sql*/ `
21
+ SELECT c.relname AS "table", t.tgname AS name, pg_get_triggerdef(t.oid) AS definition,
22
+ pg_get_functiondef(t.tgfoid) AS requires
23
+ FROM pg_trigger t
24
+ JOIN pg_class c ON c.oid = t.tgrelid
25
+ JOIN pg_namespace n ON n.oid = c.relnamespace
26
+ WHERE NOT t.tgisinternal AND n.nspname = ${this.schemaExpr}
27
+ `;
28
+ }
19
29
  getTableNamesQuery() {
20
30
  return /*sql*/ `
21
31
  SELECT table_name
@@ -8,6 +8,7 @@ export declare class SqliteSchemaIntrospector extends AbstractSqlSchemaIntrospec
8
8
  /** Whether an index is libSQL's vector index, where the engine has one; elsewhere a declared one is built plain. */
9
9
  readonly indexFacets: ReadonlySet<IndexFacet>;
10
10
  /** Not SQLite's own tables, nor the ones libSQL keeps a vector index in: its metadata and `<index>_shadow`. */
11
+ protected triggersQuery(): string;
11
12
  protected getTableNamesQuery(): string;
12
13
  protected tableExistsQuery(): string;
13
14
  protected parseTableExistsResult([row]: SqliteCountRow[]): boolean;
@@ -7,6 +7,9 @@ export class SqliteSchemaIntrospector extends AbstractSqlSchemaIntrospector {
7
7
  /** Whether an index is libSQL's vector index, where the engine has one; elsewhere a declared one is built plain. */
8
8
  indexFacets = new Set(this.dialect.hasVectorIndex() ? ['vector'] : []);
9
9
  /** Not SQLite's own tables, nor the ones libSQL keeps a vector index in: its metadata and `<index>_shadow`. */
10
+ triggersQuery() {
11
+ return /*sql*/ `SELECT tbl_name AS \`table\`, name, sql AS definition FROM sqlite_master WHERE type = 'trigger'`;
12
+ }
10
13
  getTableNamesQuery() {
11
14
  return /*sql*/ `
12
15
  SELECT name