uql-orm 0.87.0 → 0.89.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 (44) hide show
  1. package/dist/browser/uql-browser.min.js +2 -2
  2. package/dist/browser/uql-browser.min.js.map +2 -2
  3. package/dist/cockroachdb/cockroachDialect.js +1 -1
  4. package/dist/dialect/abstractSqlDialect.d.ts +4 -4
  5. package/dist/dialect/abstractSqlDialect.js +8 -4
  6. package/dist/dialect/mysqlLikeSqlDialect.js +2 -5
  7. package/dist/dialect/pgLikeSqlDialect.js +2 -5
  8. package/dist/migrate/ddl/tableDdl.d.ts +5 -0
  9. package/dist/migrate/ddl/tableDdl.js +14 -5
  10. package/dist/migrate/ddl/tableRebuild.d.ts +17 -0
  11. package/dist/migrate/ddl/tableRebuild.js +56 -0
  12. package/dist/migrate/introspection/abstractSqlSchemaIntrospector.d.ts +3 -1
  13. package/dist/migrate/introspection/abstractSqlSchemaIntrospector.js +7 -1
  14. package/dist/migrate/introspection/baseSqlIntrospector.js +4 -1
  15. package/dist/migrate/introspection/sqliteIntrospector.d.ts +3 -3
  16. package/dist/migrate/introspection/sqliteIntrospector.js +9 -7
  17. package/dist/migrate/migrationTarget.js +27 -1
  18. package/dist/migrate/migrator.d.ts +7 -1
  19. package/dist/migrate/migrator.js +49 -6
  20. package/dist/migrate/schemaChange.d.ts +12 -1
  21. package/dist/migrate/schemaChange.js +27 -0
  22. package/dist/migrate/schemaGenerator.d.ts +16 -4
  23. package/dist/migrate/schemaGenerator.js +76 -37
  24. package/dist/migrate/triggerSql.js +41 -47
  25. package/dist/mongo/mongoDialect.js +1 -3
  26. package/dist/mssql/mssqlDialect.js +2 -5
  27. package/dist/schema/indexDifferences.d.ts +2 -1
  28. package/dist/schema/indexDifferences.js +2 -1
  29. package/dist/schema/schemaAST.js +1 -0
  30. package/dist/schema/types.d.ts +5 -1
  31. package/dist/sqlite/sqliteDialect.d.ts +0 -1
  32. package/dist/sqlite/sqliteDialect.js +2 -6
  33. package/dist/type/dialect.d.ts +19 -21
  34. package/dist/type/migration.d.ts +21 -0
  35. package/dist/type/queryRaw.d.ts +11 -4
  36. package/dist/type/queryRaw.js +7 -0
  37. package/dist/type/queryWhere.d.ts +16 -6
  38. package/dist/util/index.d.ts +1 -1
  39. package/dist/util/index.js +1 -1
  40. package/dist/util/raw.js +4 -2
  41. package/dist/util/triggerWrite.d.ts +3 -1
  42. package/dist/util/triggerWrite.js +4 -3
  43. package/package.json +1 -1
  44. package/skills/uql-orm/SKILL.md +2 -2
@@ -17,9 +17,7 @@ const MSSQL_FEATURES = {
17
17
  indexIfNotExists: true,
18
18
  schemas: true,
19
19
  dropTableCascade: false,
20
- foreignKeyAlter: true,
21
- primaryKeyAlter: true,
22
- generatedColumnAdd: true,
20
+ rebuildsTables: false,
23
21
  // Extended properties are out-of-band metadata with their own procedures, not comments.
24
22
  commentSyntax: 'none',
25
23
  vectorIndexRequiresNotNull: false,
@@ -42,9 +40,8 @@ const MSSQL_FEATURES = {
42
40
  preamble: 'SET NOCOUNT ON;',
43
41
  assignsRow: false,
44
42
  body: 'inline',
45
- guards: 'beginEnd',
43
+ fires: 'eachStatement',
46
44
  layout: 'tableFirst',
47
- rows: 'set',
48
45
  scope: 'schema',
49
46
  before: false,
50
47
  },
@@ -30,7 +30,7 @@ export declare function pairIndexes<S extends ComparableIndex, T extends Compara
30
30
  /**
31
31
  * The indexes a table lacks, the ones it no longer needs, and the ones to rebuild, differing in what
32
32
  * `facets` let the engine report. Only an unpaired index uql named, or whose name the entity claims, is
33
- * dropped: any other may have been made outside the ORM.
33
+ * dropped: any other may have been made outside the ORM, so it is `kept`.
34
34
  */
35
35
  export declare function indexChanges<I extends IndexSchema>(table: string, declared: readonly I[], current: readonly IndexNode[], facets: ReadonlySet<IndexFacet>): {
36
36
  toAdd: I[];
@@ -39,6 +39,7 @@ export declare function indexChanges<I extends IndexSchema>(table: string, decla
39
39
  from: IndexNode;
40
40
  to: I;
41
41
  }[];
42
+ kept: IndexNode[];
42
43
  };
43
44
  /**
44
45
  * What two indexes differ by, comparing only what both sides state structurally: an expression, a JSON
@@ -38,7 +38,7 @@ export function pairIndexes(source, target, normalizeName = (name) => name) {
38
38
  /**
39
39
  * The indexes a table lacks, the ones it no longer needs, and the ones to rebuild, differing in what
40
40
  * `facets` let the engine report. Only an unpaired index uql named, or whose name the entity claims, is
41
- * dropped: any other may have been made outside the ORM.
41
+ * dropped: any other may have been made outside the ORM, so it is `kept`.
42
42
  */
43
43
  export function indexChanges(table, declared, current, facets) {
44
44
  const { created, dropped, matched } = pairIndexes(declared, current);
@@ -47,6 +47,7 @@ export function indexChanges(table, declared, current, facets) {
47
47
  return {
48
48
  toAdd: created,
49
49
  toDrop: dropped.filter(owned),
50
+ kept: dropped.filter((index) => !owned(index)),
50
51
  toAlter: matched.flatMap(([to, from]) => (describeIndexDifferences(to, from, facets).length ? [{ from, to }] : [])),
51
52
  };
52
53
  }
@@ -9,6 +9,7 @@ export function createTableNode(name, schema, indexFacets = new Set()) {
9
9
  columns: new Map(),
10
10
  indexes: [],
11
11
  checks: [],
12
+ externalForeignKeys: [],
12
13
  incomingRelations: [],
13
14
  outgoingRelations: [],
14
15
  };
@@ -1,4 +1,4 @@
1
- import type { IndexSchema, PrimaryKeySchema } from '../type/migration.js';
1
+ import type { ForeignKeySchema, IndexSchema, PrimaryKeySchema, StoredDefinition } from '../type/migration.js';
2
2
  import type { IndexFacet } from './indexDifferences.js';
3
3
  /**
4
4
  * Type categories universal across SQL dialects.
@@ -123,6 +123,10 @@ export interface TableNode {
123
123
  readonly checks: CheckSchema[];
124
124
  /** Optional table comment */
125
125
  readonly comment?: string;
126
+ /** The statements the engine keeps for the table, where it keeps them; none on a table built from entities. */
127
+ definition?: readonly StoredDefinition[];
128
+ /** The foreign keys to tables this AST does not hold, which a rebuild keeps as they are. */
129
+ readonly externalForeignKeys: ForeignKeySchema[];
126
130
  /** Relationships pointing TO this table (other tables referencing this one) */
127
131
  incomingRelations: RelationshipNode[];
128
132
  /** Relationships pointing FROM this table (this table referencing others) */
@@ -13,7 +13,6 @@ export declare class SqliteDialect extends AbstractSqlDialect {
13
13
  readonly commitTransactionCommand = "COMMIT";
14
14
  readonly rollbackTransactionCommand = "ROLLBACK";
15
15
  readonly isolationLevelStrategy = "none";
16
- readonly alterColumnSyntax = "none";
17
16
  readonly booleanLiteral = "integer";
18
17
  /** SQLite's own cap on a function call before 3.48, which libSQL and `bun:sqlite`'s build still have. */
19
18
  readonly maxFunctionArgs: number;
@@ -21,9 +21,7 @@ export const SQLITE_FEATURES = {
21
21
  indexIfNotExists: true,
22
22
  schemas: false, // SQLite's namespaces are attached database files, not declared objects
23
23
  dropTableCascade: false,
24
- foreignKeyAlter: false, // SQLite does not support adding FKs to existing tables
25
- primaryKeyAlter: false, // nor changing a key: the only route is rebuilding the table
26
- generatedColumnAdd: false, // accepted in a CREATE TABLE, rejected in an ALTER
24
+ rebuildsTables: true,
27
25
  commentSyntax: 'none',
28
26
  vectorIndexRequiresNotNull: false,
29
27
  vectorSupportsLength: true,
@@ -45,9 +43,8 @@ export const SQLITE_FEATURES = {
45
43
  preamble: '',
46
44
  assignsRow: false,
47
45
  body: 'inline',
48
- guards: 'clause',
46
+ fires: 'eachRowWhen',
49
47
  layout: 'timingFirst',
50
- rows: 'row',
51
48
  scope: 'schema',
52
49
  before: true,
53
50
  },
@@ -62,7 +59,6 @@ export class SqliteDialect extends AbstractSqlDialect {
62
59
  commitTransactionCommand = 'COMMIT';
63
60
  rollbackTransactionCommand = 'ROLLBACK';
64
61
  isolationLevelStrategy = 'none';
65
- alterColumnSyntax = 'none';
66
62
  booleanLiteral = 'integer';
67
63
  /** SQLite's own cap on a function call before 3.48, which libSQL and `bun:sqlite`'s build still have. */
68
64
  maxFunctionArgs = 127;
@@ -87,18 +87,12 @@ export interface DialectFeatures {
87
87
  */
88
88
  readonly schemas: boolean;
89
89
  readonly dropTableCascade: boolean;
90
- readonly foreignKeyAlter: boolean;
91
90
  /**
92
- * Whether a table's primary key can be changed on an existing table. False on SQLite, whose only
93
- * route is rebuilding the table - so a migration that would change one is refused by name rather
94
- * than emitting DDL the engine rejects.
91
+ * Whether the engine changes a column, a key or a foreign key, or adds a stored generated column, only
92
+ * by rebuilding the table, as SQLite does: a generated migration copies the table into a new one, and
93
+ * the migration builder refuses the change by name.
95
94
  */
96
- readonly primaryKeyAlter: boolean;
97
- /**
98
- * Whether a stored generated column can be added to an existing table. SQLite takes one only in a
99
- * `CREATE TABLE`, so a sync that would add one is refused by name.
100
- */
101
- readonly generatedColumnAdd: boolean;
95
+ readonly rebuildsTables: boolean;
102
96
  /** Where a comment goes: in the declaration (MySQL family), a `COMMENT ON` of its own (Postgres family), or nowhere. */
103
97
  readonly commentSyntax: 'inline' | 'statement' | 'none';
104
98
  /**
@@ -192,16 +186,12 @@ export interface TriggerFeatures {
192
186
  */
193
187
  readonly body: 'function' | 'inline';
194
188
  /**
195
- * How it states which rows it fires for: `UPDATE OF` beside a `WHEN` (`'clause'`), or, where there is
196
- * no usable `WHEN` - the MySQL family, CockroachDB, SQL Server - the same condition wrapping the body,
197
- * as `IF c THEN ... END IF;` (`'thenEndIf'`) or T-SQL's `IF c BEGIN ... END` (`'beginEnd'`).
198
- */
199
- readonly guards: 'clause' | 'thenEndIf' | 'beginEnd';
200
- /**
201
- * Whether it fires once per row, with a row on each side, or once per statement over the set it
202
- * touched. SQL Server is the only one here that is set-based, reading `inserted` and `deleted`.
189
+ * How it fires, and so how it keeps to the rows `of` and `where` select: once per row, behind
190
+ * `UPDATE OF` and a `WHEN` (`'eachRowWhen'`) or, with no usable `WHEN` (the MySQL family, CockroachDB),
191
+ * an `IF c THEN ... END IF;` around the body (`'eachRowIf'`); or once per statement (`'eachStatement'`,
192
+ * SQL Server), narrowing the `inserted` and `deleted` tables its writes read.
203
193
  */
204
- readonly rows: 'row' | 'set';
194
+ readonly fires: 'eachRowWhen' | 'eachRowIf' | 'eachStatement';
205
195
  /**
206
196
  * Where a trigger's name is unique, and so what a `DROP` has to name: per table on the Postgres
207
197
  * family, which spells `DROP TRIGGER x ON t`, and per schema everywhere else, which spells
@@ -232,6 +222,14 @@ export interface TriggerFeatures {
232
222
  */
233
223
  readonly before: boolean;
234
224
  }
225
+ /**
226
+ * The rows a set-based trigger's writes read: the tables `from` names, `inserted` and the like, narrowed
227
+ * by `where` to the ones the trigger selects.
228
+ */
229
+ export type TriggerRows = {
230
+ readonly from: string;
231
+ readonly where?: string;
232
+ };
235
233
  /** Where DDL's SQL sits: the row a trigger's predicate reads, as its prefix, and a set-based body's rows. */
236
234
  export type DdlRenderOptions = Pick<QueryComparisonOptions, 'escapedPrefix' | 'operand'> & Pick<QueryRawRenderOptions, 'rows'>;
237
235
  /**
@@ -282,8 +280,8 @@ export interface SqlQueryDialect {
282
280
  update<E>(ctx: QueryContext, entity: Type<E>, q: QuerySearch<E>, payload: UpdatePayload<E>, opts?: QueryRenderOptions): void;
283
281
  /** An upsert of one record or many by their conflict paths. */
284
282
  upsert<E>(ctx: QueryContext, entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: E | E[]): void;
285
- /** A write in a trigger's body; `rows` is where a set-based engine's body reads its rows from. */
286
- triggerWrite(ctx: QueryContext, write: TriggerWrite, rows?: string): void;
283
+ /** A write in a trigger's body; `rows` are what a set-based engine's body reads, narrowed to the ones it fires for. */
284
+ triggerWrite(ctx: QueryContext, write: TriggerWrite, rows?: TriggerRows): void;
287
285
  /** A delete of the records the query matches, a soft delete where the entity has one. */
288
286
  delete<E>(ctx: QueryContext, entity: Type<E>, q: QuerySearch<E>, opts?: QueryRenderOptions): void;
289
287
  /**
@@ -122,7 +122,20 @@ export interface TableSchema {
122
122
  readonly primaryKey?: PrimaryKeySchema;
123
123
  readonly indexes?: IndexSchema[];
124
124
  readonly foreignKeys?: ForeignKeySchema[];
125
+ /** The statements the engine keeps for the table, where it keeps them: SQLite's `sqlite_master`. */
126
+ readonly definition?: readonly StoredDefinition[];
125
127
  }
128
+ /** A statement exactly as the engine keeps it, which only it can say all of: a `CHECK`, an index over an expression. */
129
+ export type StoredDefinition = {
130
+ readonly kind: 'table' | 'index' | 'trigger';
131
+ readonly name: string;
132
+ readonly sql: string;
133
+ };
134
+ /** One side of a rebuilt table: its `CREATE TABLE` and what goes back on it, and the columns holding stored values, under this side's names. */
135
+ export type RebuiltTable = {
136
+ readonly statements: readonly string[];
137
+ readonly columns: readonly string[];
138
+ };
126
139
  /**
127
140
  * Represents an index in a database table
128
141
  */
@@ -216,6 +229,14 @@ export interface SchemaDiff {
216
229
  readonly foreignKeys?: readonly Change<ForeignKeySchema>[];
217
230
  /** Columns renamed in place, `from` the database's name `to` the entity's, which the other changes already use. */
218
231
  readonly renamedColumns?: readonly Rename[];
232
+ /**
233
+ * The table copied into a new one, which is how an engine that {@link DialectFeatures.rebuildsTables}
234
+ * applies the changes above. Its column renames are carried by the copy.
235
+ */
236
+ readonly rebuild?: {
237
+ readonly from: RebuiltTable;
238
+ readonly to: RebuiltTable;
239
+ };
219
240
  }
220
241
  /**
221
242
  * What every sync entry point takes: `safe` keeps it additive, `drop` lets it remove a column, and
@@ -1,4 +1,4 @@
1
- import type { QueryContext, RelationAggregateSpec, SqlQueryDialect } from './dialect.js';
1
+ import type { QueryContext, RelationAggregateSpec, SqlQueryDialect, TriggerRows } from './dialect.js';
2
2
  import type { Type } from './utility.js';
3
3
  /** What a `raw` callback receives. See {@link QueryRawFn}. */
4
4
  export type QueryRawRenderOptions = {
@@ -16,10 +16,10 @@ export type QueryRawRenderOptions = {
16
16
  */
17
17
  entity?: Type<unknown>;
18
18
  /**
19
- * The `FROM` a set-based trigger's body reads its rows through, `FROM inserted` and the like, which a
20
- * write in it names. Absent where the body reads `NEW` and `OLD` bare, and outside a trigger.
19
+ * The rows a set-based trigger's writes read. Absent where the body reads `NEW` and `OLD` bare, and
20
+ * outside a trigger.
21
21
  */
22
- rows?: string;
22
+ rows?: TriggerRows;
23
23
  };
24
24
  /** {@link QueryRawRenderOptions} as the callers along the way fill them in, every one still optional. */
25
25
  export type QueryRawFnOptions = Partial<QueryRawRenderOptions>;
@@ -83,3 +83,10 @@ export declare class RelationAggregate<V = unknown, Storable extends boolean = b
83
83
  /** What it reads, kept beside the SQL so a read decodes the value the way the target's field does. */
84
84
  spec: RelationAggregateSpec, value: QueryRawFn);
85
85
  }
86
+ /**
87
+ * A write `insertInto`, `updateTable` or `deleteFrom` renders, or several joined in one `raw`. It reads a
88
+ * set-based trigger's rows through {@link QueryRawRenderOptions.rows}, which is how `of` and `where` narrow
89
+ * them there; SQL of its own reads `inserted` and `deleted` whole.
90
+ */
91
+ export declare class TriggerWriteRaw extends QueryRaw {
92
+ }
@@ -57,3 +57,10 @@ export class RelationAggregate extends QueryRaw {
57
57
  this.spec = spec;
58
58
  }
59
59
  }
60
+ /**
61
+ * A write `insertInto`, `updateTable` or `deleteFrom` renders, or several joined in one `raw`. It reads a
62
+ * set-based trigger's rows through {@link QueryRawRenderOptions.rows}, which is how `of` and `where` narrow
63
+ * them there; SQL of its own reads `inserted` and `deleted` whole.
64
+ */
65
+ export class TriggerWriteRaw extends QueryRaw {
66
+ }
@@ -1,4 +1,4 @@
1
- import type { FieldKey, JsonFieldPaths, JsonFieldPathValue, RelationKey, RelationTarget } from './entity.js';
1
+ import type { FieldKey, FieldKeyOf, JsonFieldPaths, JsonFieldPathValue, RelationKey, RelationTarget } from './entity.js';
2
2
  import type { QuerySelect } from './query.js';
3
3
  import type { QueryRaw, RawFor } from './queryRaw.js';
4
4
  import type { AtLeastOne, ExpandScalar, IsMany, QueryComparableScalar, Scalar } from './utility.js';
@@ -12,9 +12,9 @@ export type QueryTextSearchOptions<E> = {
12
12
  */
13
13
  $value: string;
14
14
  /**
15
- * the fields to search, `{ title: true, body: true }`, in the order a MySQL `FULLTEXT` index lists them.
15
+ * the string fields to search, `{ title: true, body: true }`, in the order a MySQL `FULLTEXT` index lists them.
16
16
  */
17
- $fields?: QuerySelect<E>;
17
+ $fields?: QuerySelect<E, FieldKeyOf<E, string>>;
18
18
  /**
19
19
  * The language the search is parsed in (e.g. `'english'`, or `'simple'` for no stemming), else that of
20
20
  * the fulltext index over its fields: the Postgres family's text-search config, MongoDB's `$language`.
@@ -28,18 +28,28 @@ export type QueryTextSearchOptions<E> = {
28
28
  * reported on its key: ids go through `{ id: 1 }` or the by-id methods.
29
29
  */
30
30
  export type QueryWhere<E, Raw = QueryRaw, K extends keyof E = FieldKey<E> | RelationKey<E>> = QueryWhereRootOperator<E, Raw> & {
31
- [P in K]?: P extends FieldKey<E> ? QueryWhereFieldValue<E[P], Raw> : QueryWhere<RelationTarget<E[P]>, Raw> | QueryRelationSizeFilter;
31
+ [P in K]?: P extends FieldKey<E> ? QueryWhereFieldValue<E[P], Raw> : QueryWhereRelation<RelationTarget<E[P]>, Raw>;
32
32
  } & ([JsonFieldPaths<E>] extends [never] ? unknown : {
33
33
  [P in JsonFieldPaths<E>]?: QueryWhereFieldValue<JsonFieldPathValue<E, P>, Raw>;
34
34
  });
35
35
  /**
36
- * Filter a to-many relation by its row count.
36
+ * Filter a to-many relation by its row count, and by nothing beside it: the target's keys and the root
37
+ * operators are refused, as the engine refuses them at run time.
37
38
  * @example { users: { $size: 2 } }
38
39
  * @example { users: { $size: { $gte: 2 } } }
39
40
  */
40
- export type QueryRelationSizeFilter = {
41
+ export type QueryRelationSizeFilter<T = object> = {
41
42
  readonly $size: number | QuerySizeComparisonOps;
43
+ } & {
44
+ readonly [K in (keyof T & string) | keyof QueryWhereRootOperator<object>]?: never;
42
45
  };
46
+ /**
47
+ * A relation key's filter: a where over the target's rows, or its row count, never both. A target keyed
48
+ * by an index signature declares no keys to refuse, so there the run-time check alone answers a mix.
49
+ */
50
+ export type QueryWhereRelation<T, Raw = QueryRaw> = string extends keyof T ? QueryWhere<T, Raw> | QueryRelationSizeFilter : (QueryWhere<T, Raw> & {
51
+ readonly $size?: never;
52
+ }) | QueryRelationSizeFilter<T>;
43
53
  export type QueryWhereRootOperator<E, Raw = QueryRaw> = {
44
54
  /**
45
55
  * joins query clauses with a logical `AND`, returns records that match all the clauses.
@@ -7,7 +7,7 @@ export * from './ddlExpression.util.js';
7
7
  export * from './logger.js';
8
8
  export * from './object.util.js';
9
9
  export * from './raw.js';
10
- export * from './triggerWrite.js';
10
+ export { deleteFrom, insertInto, updateTable } from './triggerWrite.js';
11
11
  export * from './rowKey.util.js';
12
12
  export * from './relationQuery.util.js';
13
13
  export * from './sql.util.js';
@@ -7,7 +7,7 @@ export * from './ddlExpression.util.js';
7
7
  export * from './logger.js';
8
8
  export * from './object.util.js';
9
9
  export * from './raw.js';
10
- export * from './triggerWrite.js';
10
+ export { deleteFrom, insertInto, updateTable } from './triggerWrite.js';
11
11
  export * from './rowKey.util.js';
12
12
  export * from './relationQuery.util.js';
13
13
  export * from './sql.util.js';
package/dist/util/raw.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { getMeta } from '../entity/metadata/definition.js';
2
- import { ColumnRef, QueryRaw, RAW_TEXT, RelationAggregate, } from '../type/index.js';
2
+ import { ColumnRef, QueryRaw, RAW_TEXT, RelationAggregate, TriggerWriteRaw, } from '../type/index.js';
3
3
  import { aggregateOf, isInlinedExpression } from './field.util.js';
4
4
  import { entityName, hasKeys } from './object.util.js';
5
5
  import { UqlUsageError } from './uqlError.js';
@@ -7,7 +7,9 @@ export function raw(value, ...rest) {
7
7
  if (!isTemplateStrings(value)) {
8
8
  return new QueryRaw(value);
9
9
  }
10
- return new QueryRaw((opts) => {
10
+ // Writes joined by whitespace alone are still only writes, so a set-based trigger narrows each one.
11
+ const writes = rest.length > 0 && rest.every((v) => v instanceof TriggerWriteRaw) && value.every((part) => !part.trim());
12
+ return new (writes ? TriggerWriteRaw : QueryRaw)((opts) => {
11
13
  const { ctx } = opts;
12
14
  ctx.append(value[0]);
13
15
  rest.forEach((interpolated, i) => {
@@ -1,4 +1,4 @@
1
- import type { EntityPredicate, QueryRaw, Type, UpdatePayload, WritableKey, WriteRow } from '../type/index.js';
1
+ import { type EntityPredicate, type QueryRaw, type TriggerWrite, type Type, type UpdatePayload, type WritableKey, type WriteRow } from '../type/index.js';
2
2
  /**
3
3
  * A row inserted by a trigger's body, into any table: `insertInto(PostAudit, { postId: newRow.id })`.
4
4
  * Each value is a literal or SQL, a row's ref most often, and every engine renders it, SQL Server's
@@ -13,3 +13,5 @@ export declare function updateTable<E extends object>(entity: Type<E>, q: {
13
13
  export declare function deleteFrom<E extends object>(entity: Type<E>, q: {
14
14
  readonly $where: EntityPredicate<E>;
15
15
  }): QueryRaw;
16
+ /** A write in a trigger's body, as every helper here and a stamp render one. */
17
+ export declare function written(write: TriggerWrite): QueryRaw;
@@ -1,4 +1,4 @@
1
- import { raw } from './raw.js';
1
+ import { TriggerWriteRaw, } from '../type/index.js';
2
2
  /**
3
3
  * A row inserted by a trigger's body, into any table: `insertInto(PostAudit, { postId: newRow.id })`.
4
4
  * Each value is a literal or SQL, a row's ref most often, and every engine renders it, SQL Server's
@@ -15,6 +15,7 @@ export function updateTable(entity, q, set) {
15
15
  export function deleteFrom(entity, q) {
16
16
  return written({ kind: 'delete', entity, where: q.$where });
17
17
  }
18
- function written(write) {
19
- return raw(({ ctx, dialect, rows }) => dialect.triggerWrite(ctx, write, rows));
18
+ /** A write in a trigger's body, as every helper here and a stamp render one. */
19
+ export function written(write) {
20
+ return new TriggerWriteRaw(({ ctx, dialect, rows }) => dialect.triggerWrite(ctx, write, rows));
20
21
  }
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "homepage": "https://uql-orm.dev",
4
4
  "description": "JSON-native TypeScript ORM for Bun, Browsers, Edge, Deno, Node, Workers. Supports PostgreSQL, PGlite, MySQL, MariaDB, SQLite, CockroachDB, SQL Server, Turso, Neon, Cloudflare D1 and MongoDB. Queries are plain JSON, typed to the leaf.",
5
5
  "license": "MIT",
6
- "version": "0.87.0",
6
+ "version": "0.89.0",
7
7
  "type": "module",
8
8
  "engines": {
9
9
  "node": ">=24"
@@ -84,7 +84,7 @@ export class Post {
84
84
  an update must carry the version it read (a compile error otherwise), and one against a row someone else moved on throws `UqlOptimisticLockError` (kind `optimisticLock`, HTTP 409). Its updates name one row by its id; save and upsert are refused.
85
85
  - `@Field({ computed })` is a value the database produces, on a `readonly` property: SQL over the row, ``(u) => raw`${u.first} || ' ' || ${u.last}` ``, or a relation aggregate, `(order) => order.items.count()`.
86
86
  `stored: true` makes the SQL a generated column; `stored: ['insert', 'update']` makes it a stamp, a trigger writing it on those events whoever writes the row (``computed: raw`CURRENT_TIMESTAMP` ``), where `onUpdate` covers only uql's own writes.
87
- - `@Trigger({ on: 'afterUpdate', of: (post) => [post.status], where: { $old: { status: 'draft' } }, run })` is a trigger the database fires (`defineEntity`'s `triggers` or `defineTrigger` without decorators); `where` holds a `$where` predicate per row it names, or SQL off the rows. `run` takes `(newRow, oldRow)`, each only where the event has it (no `oldRow` on insert, no `newRow` on delete), and returns the body: `insertInto(Audit, { postId: newRow.id })`, `updateTable(Audit, { $where: { postId: newRow.id } }, { status: newRow.status })` or `deleteFrom(Audit, { $where: { postId: oldRow.id } })`, typed by the entity written and rendered on every engine (no `onInsert`/`onUpdate` fills, so an insert names each field uql fills on insert unless its column has a `defaultValue`; `$where` reads the entity's own fields; no entity filters, security ones included, so a soft-delete entity is hard-deleted; an update or delete naming no rows is refused; no `$inc`/`$mul`/`$push` on SQL Server), several joined in one `raw`. Anything else is `raw` SQL over the refs, one body for every engine or `{ postgres, mssql, ... }` where they differ (SQL Server fires per statement, reading `inserted`/`deleted` as tables, with no `before*` and no `where`). MongoDB has none, and refuses a write to an entity declaring one.
87
+ - `@Trigger({ on: 'afterUpdate', of: (post) => [post.status], where: { $old: { status: 'draft' } }, run })` is a trigger the database fires (`defineEntity`'s `triggers` or `defineTrigger` without decorators); `where` holds a `$where` predicate per row it names, or SQL off the rows. `run` takes `(newRow, oldRow)`, each only where the event has it (no `oldRow` on insert, no `newRow` on delete), and returns the body: `insertInto(Audit, { postId: newRow.id })`, `updateTable(Audit, { $where: { postId: newRow.id } }, { status: newRow.status })` or `deleteFrom(Audit, { $where: { postId: oldRow.id } })`, typed by the entity written and rendered on every engine (no `onInsert`/`onUpdate` fills, so an insert names each field uql fills on insert unless its column has a `defaultValue`; `$where` reads the entity's own fields; no entity filters, security ones included, so a soft-delete entity is hard-deleted; an update or delete naming no rows is refused; no `$inc`/`$mul`/`$push` on SQL Server), several joined in one `raw`. Anything else is `raw` SQL over the refs, one body for every engine or `{ postgres, mssql, ... }` where they differ (SQL Server fires per statement, reading `inserted`/`deleted` as tables, with no `before*`; `of` and `where` narrow what the write helpers read there, so SQL of its own is refused beside them). MongoDB has none, and refuses a write to an entity declaring one.
88
88
  - `defineEntity` defines the same entity without decorators: https://uql-orm.dev/entities/imperative.md
89
89
 
90
90
  ## Queries
@@ -154,7 +154,7 @@ transaction. A querier from `pool.getQuerier()` is yours to release: bind it wit
154
154
  ## Migrations
155
155
 
156
156
  `npx uql-migrate` reads `uql.config.ts`. `sync` creates what the entities imply (development only);
157
- `generate:entities` writes the diff as a migration file to review, renaming a column its field was renamed from and printing `renameTable` for a table that may have been; `up` applies migrations; `generate:from-db`
157
+ `generate:entities` writes the diff as a migration file to review, renaming a column its field was renamed from, printing `renameTable` for a table that may have been, rebuilding a SQLite table for what it cannot alter, and refusing a required column with no default on a table holding rows; `up` applies migrations; `generate:from-db`
158
158
  writes entity classes from an existing database; `drift:check` fails when the database no longer matches.
159
159
  Triggers are part of the diff: uql installs its own under `_uql_`-prefixed names and never touches another.
160
160