uql-orm 0.78.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 (81) hide show
  1. package/dist/browser/uql-browser.min.js +2 -2
  2. package/dist/browser/uql-browser.min.js.map +4 -4
  3. package/dist/cockroachdb/cockroachDialect.js +5 -1
  4. package/dist/dialect/abstractDialect.d.ts +1 -31
  5. package/dist/dialect/abstractDialect.js +3 -27
  6. package/dist/dialect/abstractSqlDialect.d.ts +25 -56
  7. package/dist/dialect/abstractSqlDialect.js +78 -145
  8. package/dist/dialect/aliases.d.ts +5 -0
  9. package/dist/dialect/aliases.js +5 -0
  10. package/dist/dialect/mysqlLikeSqlDialect.d.ts +4 -2
  11. package/dist/dialect/mysqlLikeSqlDialect.js +14 -1
  12. package/dist/dialect/operators.d.ts +66 -0
  13. package/dist/dialect/operators.js +129 -0
  14. package/dist/dialect/pgLikeSqlDialect.d.ts +4 -1
  15. package/dist/dialect/pgLikeSqlDialect.js +16 -3
  16. package/dist/entity/decorator/entity.d.ts +6 -1
  17. package/dist/entity/decorator/entity.js +12 -1
  18. package/dist/entity/index.d.ts +1 -1
  19. package/dist/entity/index.js +1 -1
  20. package/dist/entity/metadata/definition.d.ts +6 -1
  21. package/dist/entity/metadata/definition.js +19 -0
  22. package/dist/migrate/codegen/entityTypes.js +1 -2
  23. package/dist/migrate/ddl/mssqlIndexDdl.d.ts +5 -0
  24. package/dist/migrate/ddl/mssqlIndexDdl.js +10 -0
  25. package/dist/migrate/ddl/mssqlTableDdl.d.ts +2 -0
  26. package/dist/migrate/ddl/mssqlTableDdl.js +5 -0
  27. package/dist/migrate/ddl/tableDdl.d.ts +2 -0
  28. package/dist/migrate/ddl/tableDdl.js +4 -0
  29. package/dist/migrate/generator/mongoSchemaGenerator.d.ts +4 -0
  30. package/dist/migrate/generator/mongoSchemaGenerator.js +10 -0
  31. package/dist/migrate/introspection/abstractSqlSchemaIntrospector.d.ts +13 -1
  32. package/dist/migrate/introspection/abstractSqlSchemaIntrospector.js +21 -0
  33. package/dist/migrate/introspection/mongoIntrospector.d.ts +3 -1
  34. package/dist/migrate/introspection/mongoIntrospector.js +4 -0
  35. package/dist/migrate/introspection/mssqlIntrospector.d.ts +1 -0
  36. package/dist/migrate/introspection/mssqlIntrospector.js +8 -0
  37. package/dist/migrate/introspection/mysqlIntrospector.d.ts +1 -0
  38. package/dist/migrate/introspection/mysqlIntrospector.js +9 -0
  39. package/dist/migrate/introspection/postgresIntrospector.d.ts +1 -0
  40. package/dist/migrate/introspection/postgresIntrospector.js +10 -0
  41. package/dist/migrate/introspection/sqliteIntrospector.d.ts +1 -0
  42. package/dist/migrate/introspection/sqliteIntrospector.js +3 -0
  43. package/dist/migrate/migrator.d.ts +28 -1
  44. package/dist/migrate/migrator.js +88 -9
  45. package/dist/migrate/schemaGenerator.d.ts +12 -1
  46. package/dist/migrate/schemaGenerator.js +47 -5
  47. package/dist/migrate/storage/databaseStorage.d.ts +4 -0
  48. package/dist/migrate/storage/databaseStorage.js +14 -8
  49. package/dist/migrate/triggerSql.d.ts +24 -0
  50. package/dist/migrate/triggerSql.js +229 -0
  51. package/dist/mongo/mongoDialect.d.ts +0 -21
  52. package/dist/mongo/mongoDialect.js +105 -100
  53. package/dist/mongo/mongodbQuerier.js +17 -1
  54. package/dist/mssql/mssqlDialect.d.ts +18 -7
  55. package/dist/mssql/mssqlDialect.js +77 -33
  56. package/dist/mssql/mssqlQuerier.js +2 -2
  57. package/dist/schema/canonicalType.d.ts +7 -1
  58. package/dist/schema/canonicalType.js +35 -15
  59. package/dist/schema/schemaASTBuilder.d.ts +2 -8
  60. package/dist/schema/schemaASTBuilder.js +6 -20
  61. package/dist/sqlite/sqliteDialect.d.ts +1 -1
  62. package/dist/sqlite/sqliteDialect.js +12 -3
  63. package/dist/type/dialect.d.ts +69 -9
  64. package/dist/type/entity.d.ts +102 -5
  65. package/dist/type/migration.d.ts +17 -0
  66. package/dist/type/query.d.ts +13 -4
  67. package/dist/type/queryRaw.d.ts +8 -1
  68. package/dist/type/queryRaw.js +10 -2
  69. package/dist/type/queryWhere.d.ts +4 -2
  70. package/dist/util/field.util.d.ts +9 -1
  71. package/dist/util/field.util.js +14 -2
  72. package/dist/util/fieldOption.util.d.ts +10 -2
  73. package/dist/util/fieldOption.util.js +17 -2
  74. package/dist/util/raw.d.ts +14 -1
  75. package/dist/util/raw.js +45 -13
  76. package/dist/util/sql.util.d.ts +12 -0
  77. package/dist/util/sql.util.js +24 -3
  78. package/dist/util/uqlError.d.ts +2 -0
  79. package/dist/util/uqlError.js +4 -0
  80. package/package.json +4 -4
  81. package/skills/uql-orm/SKILL.md +11 -5
@@ -1,8 +1,9 @@
1
1
  import type { EnumValues, ForeignKeyAction, IndexType } from '../schema/types.js';
2
+ import type { SqlDialectName } from './dialect.js';
2
3
  import type { FilterOptions, RelationQuery } from './query.js';
3
4
  import type { ColumnRef, QueryRaw, RelationAggregate } from './queryRaw.js';
4
5
  import type { QueryWhere } from './queryWhere.js';
5
- import type { Except, ExactlyOne, IsEqual, IsMany, Json, Scalar, Type, Unpacked, Writable } from './utility.js';
6
+ import type { AtLeastOne, Except, ExactlyOne, IsEqual, IsMany, Json, Scalar, Type, Unpacked, Writable } from './utility.js';
6
7
  import type { VectorDistance, VectorIndexOptions, VectorIndexType } from './vector.js';
7
8
  /** Brands the property an entity is identified by, where it is not `id`, `_id` or `uuid`. */
8
9
  export declare const idKey: unique symbol;
@@ -254,8 +255,12 @@ export type FieldOptions<V = TsTypeOf<FieldType>, E = unknown> = {
254
255
  * subquery a `$count` reads. Both resolve to SQL at registration, so everything downstream sees one.
255
256
  */
256
257
  readonly computed?: ComputedSql<E>;
257
- /** Whether {@link FieldOptions.computed} is a generated column rather than spliced into each read; no query changes either way. */
258
- readonly stored?: boolean;
258
+ /**
259
+ * Where {@link FieldOptions.computed} lives instead of being spliced into each read. `true` makes it a
260
+ * generated column, which takes only an immutable expression; a list of events makes it a stamp a trigger
261
+ * writes on each, whoever writes the row - how `CURRENT_TIMESTAMP` is kept, where `onUpdate` sees only uql's writes.
262
+ */
263
+ readonly stored?: boolean | readonly StampEvent[];
259
264
  readonly updatable?: boolean;
260
265
  readonly eager?: boolean;
261
266
  readonly onInsert?: OnFieldCallback<V>;
@@ -271,8 +276,12 @@ export type FieldOptions<V = TsTypeOf<FieldType>, E = unknown> = {
271
276
  * column is `NOT NULL DEFAULT 0`, and the entity brands the property with {@link versionKey}.
272
277
  */
273
278
  readonly version?: true;
274
- /** The SQL type, where it differs from the one `type` implies: `type: String, columnType: 'decimal'`. */
275
- readonly columnType?: ColumnType;
279
+ /**
280
+ * The SQL type, where it differs from the one `type` implies: `type: String, columnType: 'decimal'`.
281
+ * An engine's own type is a `raw` constant, `columnType: raw`tsvector``, rendered verbatim and
282
+ * never translated, so a column declaring one is yours to keep portable.
283
+ */
284
+ readonly columnType?: ColumnType | QueryRaw;
276
285
  /** A string column's length. */
277
286
  readonly length?: number;
278
287
  /** A decimal column's precision. */
@@ -687,11 +696,97 @@ export type EntityMeta<E> = {
687
696
  checks?: EntityCheckMeta<E>[];
688
697
  /** Lifecycle hooks registered via @BeforeInsert, @AfterUpdate, etc. */
689
698
  hooks?: Partial<Record<HookEvent, HookRegistration[]>>;
699
+ /** Triggers the database runs, compiled when the schema is built. */
700
+ triggers?: EntityTriggerMeta<E>[];
690
701
  /** Bumped by every `define*` call, so what is derived from the metadata can tell it changed. */
691
702
  revision: number;
692
703
  /** The revision `getMeta` last finalized, which is what makes finalizing idempotent and re-entrant. */
693
704
  processedAt?: number;
694
705
  };
706
+ /** When the database writes a stamp: as the row is inserted, as it is updated, or both. */
707
+ export type StampEvent = 'insert' | 'update';
708
+ /**
709
+ * The events a trigger fires on: the lifecycle names, minus the upsert pair, which names no event of its
710
+ * own because `ON CONFLICT` fires the insert or the update triggers, and minus `afterLoad`, which is a
711
+ * read. Derived from {@link HookEvent} so the two vocabularies cannot drift.
712
+ */
713
+ export type TriggerEvent = Exclude<HookEvent, 'beforeUpsert' | 'afterUpsert' | 'afterLoad'>;
714
+ /** Both events for one operation, since a trigger's timing never changes which rows it has. */
715
+ type TriggerEventOn<Op extends string> = Extract<TriggerEvent, `before${Op}` | `after${Op}`>;
716
+ /** The events with a row on both sides, the only ones that can say which columns moved. */
717
+ type TriggerUpdateEvent = TriggerEventOn<'Update'>;
718
+ /**
719
+ * The rows an event has, as `where` keys them: the incoming one on an insert, the outgoing one on a
720
+ * delete, both on an update.
721
+ */
722
+ type TriggerRow<Ev extends TriggerEvent> = Ev extends TriggerEventOn<'Insert'> ? '$new' : Ev extends TriggerEventOn<'Delete'> ? '$old' : '$new' | '$old';
723
+ /** One row's refs, rendering `NEW."col"` or `OLD."col"`, or `never` where the event has no such row. */
724
+ type TriggerRowRefs<E, Ev extends TriggerEvent, R extends '$new' | '$old'> = R extends TriggerRow<Ev> ? RefMap<E> : never;
725
+ /**
726
+ * What a trigger runs, over its rows: the incoming row first and the outgoing one second, on every event.
727
+ * The one an event lacks is `never`, so reading it does not compile, and a body reading only the outgoing
728
+ * row - `(_newRow, oldRow)` - serves an update and a delete alike.
729
+ */
730
+ type TriggerBody<E, Ev extends TriggerEvent> = (newRow: TriggerRowRefs<E, Ev, '$new'>, oldRow: TriggerRowRefs<E, Ev, '$old'>) => QueryRaw;
731
+ /**
732
+ * A condition as data: a predicate on each row it names, of the rows the event has, all of which hold.
733
+ * The row is `$`-marked, as the operators inside it are, so it never reads as a field of the entity.
734
+ */
735
+ type TriggerPredicate<E, Ev extends TriggerEvent> = {
736
+ readonly [R in TriggerRow<Ev>]?: EntityPredicate<E>;
737
+ };
738
+ /**
739
+ * The body, the engine's own SQL: one for every engine it reads alike, or a map naming one per engine
740
+ * where they differ, as SQL Server's set-based `inserted`/`deleted` does. A missing entry for the engine
741
+ * in use is refused at `sync`, since an entity is declared without knowing which pool will render it.
742
+ */
743
+ type TriggerRun<E, Ev extends TriggerEvent> = TriggerBody<E, Ev> | Readonly<AtLeastOne<Record<SqlDialectName, TriggerBody<E, Ev>>>>;
744
+ /**
745
+ * A trigger, `{ on: 'beforeUpdate', of: (post) => [post.body], run: (newRow) => raw`...` }`.
746
+ *
747
+ * A list rather than a map keyed by the event, as `checks` and `indexes` are lists: several triggers may
748
+ * share an event, they fire in the order written, and each is named, diffed and dropped by that name.
749
+ */
750
+ export type TriggerOptions<E = unknown> = {
751
+ [Ev in TriggerEvent]: {
752
+ readonly on: Ev;
753
+ /**
754
+ * What to call this trigger within the entity, for a clearer identifier than its event and position.
755
+ * A label, not the identifier: uql prefixes and qualifies what it installs, so it can tell its own
756
+ * objects from hand-written ones and two entities may share a label.
757
+ */
758
+ readonly name?: string;
759
+ /**
760
+ * The columns whose change the trigger waits for, reading as the `UPDATE OF` it renders. Beside it
761
+ * goes a `WHEN` comparing each with `IS DISTINCT FROM`, which is the point of the pair: `UPDATE OF`
762
+ * fires on a column that was merely assigned, and the comparison narrows that to one that moved.
763
+ */
764
+ readonly of?: Ev extends TriggerUpdateEvent ? (refs: RefMap<E>) => readonly ColumnRef<string>[] : never;
765
+ /**
766
+ * A further condition, as the `WHEN` the engine evaluates before entering the body, over the rows the
767
+ * body reads: a predicate on each, `{ $old: { status: 'draft' }, $new: { status: 'published' } }`,
768
+ * rendered on every engine from the one declaration, or SQL off them for what no predicate states.
769
+ */
770
+ readonly where?: TriggerPredicate<E, Ev> | TriggerBody<E, Ev>;
771
+ readonly run: TriggerRun<E, Ev>;
772
+ };
773
+ }[TriggerEvent];
774
+ /**
775
+ * A trigger as entity metadata keeps it: the column callback resolved to keys, and a body widened to
776
+ * take both rows, which the renderer passes whatever the event, each body reading only its own.
777
+ */
778
+ export type EntityTriggerMeta<E = object> = Except<TriggerOptions<E>, 'of' | 'where' | 'run'> & {
779
+ readonly of?: readonly string[];
780
+ readonly where?: TriggerPredicate<E, TriggerUpdateEvent> | TriggerMetaBody<E>;
781
+ readonly run: TriggerMetaBody<E> | Readonly<Partial<Record<SqlDialectName, TriggerMetaBody<E>>>>;
782
+ };
783
+ /**
784
+ * A body as the renderer calls it, with both rows whatever the event. Bivariant, as {@link EntitySql} is,
785
+ * so a body typing the row its event lacks as `never` is still held here.
786
+ */
787
+ export type TriggerMetaBody<E> = {
788
+ run(newRow: RefMap<E>, oldRow: RefMap<E>): QueryRaw;
789
+ }['run'];
695
790
  /**
696
791
  * A table's `CHECK`, `{ where: { balance: { $gte: 0 } } }`, or SQL off the refs,
697
792
  * `{ where: (wallet) => raw`${wallet.spent} <= ${wallet.balance}` }`.
@@ -739,6 +834,8 @@ export type EntityOptions<E = unknown> = {
739
834
  readonly checks?: readonly CheckOptions<E>[];
740
835
  /** Each lifecycle event and the methods it runs, read off the key map: `{ beforeInsert: (post) => [post.stamp] }`. */
741
836
  readonly hooks?: Partial<Record<HookEvent, (keys: KeyMap<E>) => readonly MethodKey<E>[]>>;
837
+ /** Triggers the database runs, in the order written. See {@link TriggerOptions}. */
838
+ readonly triggers?: readonly TriggerOptions<E>[];
742
839
  };
743
840
  /**
744
841
  * Everything an index carries beyond its columns, as the migration builder's `table.index(...)` takes it,
@@ -248,6 +248,8 @@ export interface DropSchemaOptions {
248
248
  readonly ifExists?: boolean;
249
249
  readonly cascade?: boolean;
250
250
  }
251
+ /** The triggers uql installed on one table, by name, each with the statements that recreate it as it stands. */
252
+ export type InstalledTriggers = ReadonlyMap<string, readonly string[]>;
251
253
  /**
252
254
  * Interface for generating DDL statements from entity metadata
253
255
  */
@@ -262,6 +264,15 @@ export interface SchemaGenerator {
262
264
  generateDropSchema(entities: readonly Type<object>[], options?: DropSchemaOptions): string[];
263
265
  /** Generate DROP TABLE statement. */
264
266
  generateDropTable(tableName: string, options?: DropSchemaOptions): string;
267
+ /**
268
+ * What takes the triggers on `entity`'s table from `installed` - each by name, with the statements that
269
+ * recreate it - to what it declares: nothing where the two agree, which is always on MongoDB.
270
+ */
271
+ generateTriggers(entity: Type<object>, installed?: InstalledTriggers): string[];
272
+ /** The inverse of {@link generateTriggers} from the same `installed`: its triggers dropped, and the ones it dropped restored. */
273
+ generateTriggersDown(entity: Type<object>, installed?: InstalledTriggers): string[];
274
+ /** A `DROP` for each trigger uql owns among `names` on `entity`'s table, whatever the entity declares. */
275
+ generateTriggerDrops(entity: Type<object>, names: readonly string[]): string[];
265
276
  /**
266
277
  * Generate ALTER TABLE statements based on schema diff
267
278
  */
@@ -321,6 +332,12 @@ export interface SchemaGenerator {
321
332
  * Interface for introspecting the current database schema
322
333
  */
323
334
  export interface SchemaIntrospector {
335
+ /**
336
+ * Every trigger uql installed in this schema, by table and then by name, each with the statements that
337
+ * recreate it as it stands. The names say which to drop once an entity no longer declares them; the
338
+ * statements are what a rollback puts back, read off the engine rather than recorded anywhere by uql.
339
+ */
340
+ ownedTriggers(): Promise<Map<string, InstalledTriggers>>;
324
341
  /**
325
342
  * What this introspector can read back about an index, and so all that diffing may compare.
326
343
  * Comparing a feature it cannot read reports the same drift forever: the entity side declares it,
@@ -22,13 +22,22 @@ export type QueryOptions = {
22
22
  * table look alike. The entity's own filters never count as naming one.
23
23
  */
24
24
  unfiltered?: boolean;
25
- /**
26
- * prefix the query with this.
27
- */
25
+ };
26
+ /**
27
+ * What a statement is rendered with, on top of the options its caller passed. Kept apart from
28
+ * {@link QueryOptions} because that one is public - it is the third argument of every querier method -
29
+ * and none of this is a caller's to set: an alias is the dialect's to choose and to spell.
30
+ */
31
+ export type QueryRenderOptions = QueryOptions & {
32
+ /** The alias columns are read off, escaped by the dialect unless {@link escapedPrefix} spells it. */
28
33
  prefix?: string;
29
34
  /**
30
- * automatically infer the prefix for the query.
35
+ * The prefix already written out, for the one caller whose row is not an identifier: a trigger reads
36
+ * `NEW."col"`, where `NEW` is a record the engine declares, and quoting it names a table that is not
37
+ * in scope. Defaults to {@link prefix} escaped.
31
38
  */
39
+ escapedPrefix?: string;
40
+ /** Whether to infer the alias where none is given. */
32
41
  autoPrefix?: boolean;
33
42
  };
34
43
  /**
@@ -27,10 +27,17 @@ export type QueryRawFnOptions = Partial<QueryRawRenderOptions>;
27
27
  export type QueryRawFn = (opts: QueryRawRenderOptions) => unknown;
28
28
  export declare const RAW_VALUE: unique symbol;
29
29
  export declare const RAW_ALIAS: unique symbol;
30
+ export declare const RAW_TEXT: unique symbol;
30
31
  export declare class QueryRaw {
31
32
  readonly [RAW_VALUE]: QueryRawFn;
32
33
  readonly [RAW_ALIAS]?: string;
33
- constructor(value: QueryRawFn, alias?: string);
34
+ /**
35
+ * The SQL verbatim, set only where it is a constant: a template that interpolates nothing binds no
36
+ * value and reads no column, so it needs no dialect to render. What a DDL clause with nowhere to
37
+ * bind reads - see {@link constantSql}.
38
+ */
39
+ readonly [RAW_TEXT]?: string;
40
+ constructor(value: QueryRawFn, alias?: string, text?: string);
34
41
  /** The same expression under an alias, for a `$select` projection. */
35
42
  as(alias: string): QueryRaw;
36
43
  /** Writes the expression into `opts.ctx`. The alias is the projection's to write, after the term. */
@@ -1,15 +1,23 @@
1
1
  export const RAW_VALUE = Symbol('rawValue');
2
2
  export const RAW_ALIAS = Symbol('rawAlias');
3
+ export const RAW_TEXT = Symbol('rawText');
3
4
  export class QueryRaw {
4
5
  [RAW_VALUE];
5
6
  [RAW_ALIAS];
6
- constructor(value, alias) {
7
+ /**
8
+ * The SQL verbatim, set only where it is a constant: a template that interpolates nothing binds no
9
+ * value and reads no column, so it needs no dialect to render. What a DDL clause with nowhere to
10
+ * bind reads - see {@link constantSql}.
11
+ */
12
+ [RAW_TEXT];
13
+ constructor(value, alias, text) {
7
14
  this[RAW_VALUE] = value;
8
15
  this[RAW_ALIAS] = alias;
16
+ this[RAW_TEXT] = text;
9
17
  }
10
18
  /** The same expression under an alias, for a `$select` projection. */
11
19
  as(alias) {
12
- return new QueryRaw(this[RAW_VALUE], alias);
20
+ return new QueryRaw(this[RAW_VALUE], alias, this[RAW_TEXT]);
13
21
  }
14
22
  /** Writes the expression into `opts.ctx`. The alias is the projection's to write, after the term. */
15
23
  render(opts) {
@@ -223,6 +223,8 @@ export type QueryWhereElemMatch<U, Raw = QueryRaw> = unknown extends U ? {
223
223
  } : NonNullable<U> extends Scalar ? QueryWhereFieldOperators<NonNullable<U>, Raw> : {
224
224
  [K in keyof NonNullable<U>]?: QueryWhereFieldValue<NonNullable<U>[K], Raw>;
225
225
  };
226
+ /** Every operator a field condition takes, which is what a key of one is once checked. */
227
+ export type QueryWhereFieldOp = keyof QueryWhereFieldOperatorMap<unknown>;
226
228
  /**
227
229
  * Simple relational comparison operators. `Pick`'s constraint ties this back to
228
230
  * {@link QueryWhereFieldOperatorMap} so a rename there breaks this union at compile time.
@@ -251,7 +253,7 @@ type QueryArrayOp = keyof Pick<QueryWhereFieldOperatorMap<unknown>, '$all' | '$s
251
253
  /**
252
254
  * Ordering operators: {@link QueryCompareOp} plus `$between`.
253
255
  */
254
- type QueryOrderedOp = QueryCompareOp | keyof Pick<QueryWhereFieldOperatorMap<unknown>, '$between'>;
256
+ export type QueryOrderedOp = QueryCompareOp | keyof Pick<QueryWhereFieldOperatorMap<unknown>, '$between'>;
255
257
  /**
256
258
  * Vector-only operators. `Pick`'s constraint ties this back to {@link QueryWhereFieldOperatorMap}
257
259
  * so a rename there breaks this union at compile time.
@@ -261,7 +263,7 @@ type QueryVectorOp = keyof Pick<QueryWhereFieldOperatorMap<unknown>, '$near'>;
261
263
  * The operators every field takes. A subtraction, so an operator added to the map without being
262
264
  * classified above is offered on every field: classify it first.
263
265
  */
264
- type QueryCommonOp = Exclude<keyof QueryWhereFieldOperatorMap<unknown>, QueryStringOp | QueryArrayOp | QueryOrderedOp | QueryVectorOp>;
266
+ type QueryCommonOp = Exclude<QueryWhereFieldOp, QueryStringOp | QueryArrayOp | QueryOrderedOp | QueryVectorOp>;
265
267
  /**
266
268
  * Operator keys applicable to a field of type `T`. Brackets prevent union distribution so an
267
269
  * optional field (`string | undefined`) or a literal union (`'a' | 'b'`) gates as one type.
@@ -1,4 +1,4 @@
1
- import { type ColumnFamily, type EntityMeta, type FieldKey, type FieldOptions, type RelationAggregateSpec } from '../type/index.js';
1
+ import { type ColumnFamily, type EntityMeta, type FieldKey, type FieldOptions, type StampEvent, type RelationAggregateSpec } from '../type/index.js';
2
2
  /** The family of a logical field type, or `undefined` where it names none. */
3
3
  export declare function columnFamily(type: unknown): ColumnFamily | undefined;
4
4
  /**
@@ -12,6 +12,14 @@ export declare function isIntegerColumn(field: Pick<FieldOptions, 'type' | 'colu
12
12
  * because an inlined field has no column to name, while a stored one is read like any other.
13
13
  */
14
14
  export declare function isInlinedExpression<F extends FieldOptions>(field: F): field is F & Required<Pick<F, 'computed'>>;
15
+ /** Whether the entity puts anything on its table the database runs: an authored trigger, or a stamp. */
16
+ export declare function hasTriggers<E>(meta: EntityMeta<E>): boolean;
17
+ /**
18
+ * The events the database writes this field on, or `undefined` where it is not a stamp. A stamp is a
19
+ * real column the engine fills on each event, which is how an expression too volatile for a generated
20
+ * column - `now()` - is still kept by the database rather than by whoever happens to write the row.
21
+ */
22
+ export declare function stampEvents(field: FieldOptions): readonly StampEvent[] | undefined;
15
23
  /**
16
24
  * The relation aggregate a field computes, where it computes one rather than writing SQL: what it
17
25
  * reads, off which relation, narrowed and capped how. Every engine renders it from this - a correlated
@@ -1,5 +1,5 @@
1
1
  import { COLUMN_TYPES, RelationAggregate, } from '../type/index.js';
2
- import { getKeys } from './object.util.js';
2
+ import { definedEntries, getKeys } from './object.util.js';
3
3
  // Constructors and type strings in one map: a logical type is either, and every caller asks the same
4
4
  // question of both.
5
5
  const FAMILY_OF = new Map([
@@ -39,7 +39,19 @@ export function isIntegerColumn(field) {
39
39
  * because an inlined field has no column to name, while a stored one is read like any other.
40
40
  */
41
41
  export function isInlinedExpression(field) {
42
- return field.computed !== undefined && field.stored !== true;
42
+ return field.computed !== undefined && !field.stored;
43
+ }
44
+ /** Whether the entity puts anything on its table the database runs: an authored trigger, or a stamp. */
45
+ export function hasTriggers(meta) {
46
+ return Boolean(meta.triggers?.length) || definedEntries(meta.fields).some(([, field]) => stampEvents(field));
47
+ }
48
+ /**
49
+ * The events the database writes this field on, or `undefined` where it is not a stamp. A stamp is a
50
+ * real column the engine fills on each event, which is how an expression too volatile for a generated
51
+ * column - `now()` - is still kept by the database rather than by whoever happens to write the row.
52
+ */
53
+ export function stampEvents(field) {
54
+ return Array.isArray(field.stored) ? field.stored : undefined;
43
55
  }
44
56
  /**
45
57
  * The relation aggregate a field computes, where it computes one rather than writing SQL: what it
@@ -1,4 +1,4 @@
1
- import type { ColumnFamily, FamilyOf, FieldOptions, QueryRaw } from '../type/index.js';
1
+ import { type ColumnFamily, type FamilyOf, type FieldOptions, QueryRaw, type StampEvent } from '../type/index.js';
2
2
  /**
3
3
  * The column family each field option means anything on, or `'*'` where it applies to every column.
4
4
  * Exhaustive over {@link FieldOptions}, so a new option cannot be added without placing it - the
@@ -54,6 +54,12 @@ type GeneratedWrite = (typeof GENERATED_WRITES)[number];
54
54
  */
55
55
  declare const VERSION_WRITES: readonly ["updatable", "onInsert", "onUpdate", "softDelete", "defaultValue", "autoIncrement", "computed", "stored", "isId"];
56
56
  type VersionWrite = (typeof VERSION_WRITES)[number];
57
+ /**
58
+ * What bounds a type: stated separately where uql spells the type, and part of the text where the
59
+ * engine's own is written out, which renders verbatim and leaves these unread.
60
+ */
61
+ declare const TYPE_BOUNDS: readonly ["length", "precision", "scale", "dimensions"];
62
+ type TypeBound = (typeof TYPE_BOUNDS)[number];
57
63
  /**
58
64
  * The first option `opts` cannot use, phrased as the tail of `'Entity.field' ...`, or `undefined`
59
65
  * where every option applies. The runtime half of the decorators' check, so the imperative API and
@@ -68,7 +74,7 @@ type OptionsFamily<O> = O extends {
68
74
  } ? FamilyOf<T> : ColumnFamily;
69
75
  /** What the field's own values leave unread, matching {@link deadOn} line for line. */
70
76
  type DeadOptions<O> = (O extends {
71
- readonly stored: true;
77
+ readonly stored: true | readonly StampEvent[];
72
78
  } ? GeneratedWrite : O extends {
73
79
  readonly computed: QueryRaw;
74
80
  } ? Exclude<keyof FieldOptions, InlineRead> : never) | (O extends {
@@ -77,6 +83,8 @@ type DeadOptions<O> = (O extends {
77
83
  } ? 'nullable' : never) | (O extends {
78
84
  readonly updatable: false;
79
85
  } ? 'onUpdate' : never) | (O extends {
86
+ readonly columnType: QueryRaw;
87
+ } ? TypeBound : never) | (O extends {
80
88
  readonly version: true;
81
89
  } ? VersionWrite : never) | (O extends {
82
90
  readonly version: true;
@@ -1,5 +1,7 @@
1
+ import { QueryRaw } from '../type/index.js';
1
2
  import { columnFamily, isInlinedExpression } from './field.util.js';
2
3
  import { getKeys } from './object.util.js';
4
+ import { constantSql } from './raw.js';
3
5
  /**
4
6
  * The column family each field option means anything on, or `'*'` where it applies to every column.
5
7
  * Exhaustive over {@link FieldOptions}, so a new option cannot be added without placing it - the
@@ -68,6 +70,11 @@ const GENERATED_WRITES = [...VALUE_DECIDERS, 'version'];
68
70
  * would make it another kind of column entirely. Its `nullable: false` and `DEFAULT 0` are implied.
69
71
  */
70
72
  const VERSION_WRITES = [...VALUE_DECIDERS, 'computed', 'stored', 'isId'];
73
+ /**
74
+ * What bounds a type: stated separately where uql spells the type, and part of the text where the
75
+ * engine's own is written out, which renders verbatim and leaves these unread.
76
+ */
77
+ const TYPE_BOUNDS = ['length', 'precision', 'scale', 'dimensions'];
71
78
  /**
72
79
  * Whether `key` is the `nullable: true` a NOT NULL column contradicts. `nullable: false` says what
73
80
  * such a column already is, and rejecting an accurate statement teaches an author to distrust the check.
@@ -79,12 +86,15 @@ function contradictsNotNull(opts, key) {
79
86
  function deadOn(opts, key) {
80
87
  if (isInlinedExpression(opts) && !INLINE_READS.some((read) => read === key))
81
88
  return 'an inlined computed field';
82
- if (opts.stored === true && GENERATED_WRITES.some((write) => write === key))
83
- return 'a stored computed column';
89
+ if (opts.stored && GENERATED_WRITES.some((write) => write === key))
90
+ return 'a column the database writes';
84
91
  if (opts.isId === true && contradictsNotNull(opts, key))
85
92
  return 'a primary key';
86
93
  if (opts.updatable === false && key === 'onUpdate')
87
94
  return "a field declared 'updatable: false'";
95
+ if (opts.columnType instanceof QueryRaw && TYPE_BOUNDS.some((bound) => bound === key)) {
96
+ return 'a column type written out as SQL, which carries its own bounds';
97
+ }
88
98
  if (opts.version === true && (VERSION_WRITES.some((write) => write === key) || contradictsNotNull(opts, key))) {
89
99
  return 'a version field';
90
100
  }
@@ -96,6 +106,11 @@ function deadOn(opts, key) {
96
106
  * plain JavaScript reach the same answer.
97
107
  */
98
108
  export function fieldOptionConflict(opts) {
109
+ // Caught here rather than where the type is resolved, which is a migration on most engines and a
110
+ // query on SQL Server, and which knows no field to name.
111
+ if (opts.columnType instanceof QueryRaw && constantSql(opts.columnType) === undefined) {
112
+ return "cannot use 'columnType': a `raw` one names a constant type, so it can bind no value and read no column";
113
+ }
99
114
  const family = columnFamily(opts.columnType ?? opts.type);
100
115
  // Walked in table order, not in the order the field happened to be written, so a field with two
101
116
  // conflicts always reports the same one. An option no rule knows is a typo, which `@Field`'s own check
@@ -1,4 +1,4 @@
1
- import { type EntitySql, type EntityWhere, type EntityWhereMeta, QueryRaw, type QueryRawFn, type ComputedRefs, type RefMap, type Type } from '../type/index.js';
1
+ import { ColumnRef, type EntitySql, type EntityWhere, type EntityWhereMeta, QueryRaw, type QueryRawFn, type ComputedRefs, type RefMap, type TriggerRowName, type Type } from '../type/index.js';
2
2
  /**
3
3
  * Raw SQL, where an interpolated value binds, a `refs` field renders its column, and a `raw` renders
4
4
  * in place: `raw`GREATEST(0, ${user.credits} - ${amount})``. A callback writes whatever it writes, so
@@ -6,6 +6,11 @@ import { type EntitySql, type EntityWhere, type EntityWhereMeta, QueryRaw, type
6
6
  */
7
7
  export declare function raw(strings: TemplateStringsArray, ...values: readonly unknown[]): QueryRaw;
8
8
  export declare function raw(value: QueryRawFn): QueryRaw;
9
+ /**
10
+ * The SQL of a `raw` that names a constant, for a DDL clause with no dialect to render against and
11
+ * nowhere to bind a value; `undefined` where it interpolates and so needs one.
12
+ */
13
+ export declare function constantSql(value: QueryRaw): string | undefined;
9
14
  /**
10
15
  * The fields of `entity` as {@link ColumnRef}s, each rendering inside `raw` as its column: named the way
11
16
  * the dialect names it, so the naming strategy and `@Field({ name })` apply, and qualified by the alias
@@ -21,3 +26,11 @@ export declare function memberRefs<E>(): ComputedRefs<E>;
21
26
  export declare function entitySql<E>(sql: EntitySql<E>): QueryRaw;
22
27
  /** A definition's predicate, its callback resolved the way {@link entitySql} resolves one. */
23
28
  export declare function entityWhere<E>(where: EntityWhere<E>): EntityWhereMeta<E>;
29
+ /**
30
+ * The fields of `E` as the row a trigger body reads them off, qualified by the side it names: `NEW."col"`
31
+ * against the incoming row, `OLD."col"` against the outgoing one. Columns only, never a relation's
32
+ * aggregate: that is a subquery, and a trigger fires on one row rather than over a table to correlate to.
33
+ */
34
+ export declare function rowRefs<E>(qualifier: TriggerRowName): RefMap<E>;
35
+ /** One field of a trigger's row, for code that names it by its key rather than off {@link rowRefs}. */
36
+ export declare function rowColumn(qualifier: TriggerRowName, key: string): ColumnRef;
package/dist/util/raw.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { getMeta } from '../entity/metadata/definition.js';
2
- import { ColumnRef, QueryRaw, RelationAggregate, } from '../type/index.js';
3
- import { isInlinedExpression } from './field.util.js';
4
- import { hasKeys } from './object.util.js';
2
+ import { ColumnRef, QueryRaw, RAW_TEXT, RelationAggregate, } from '../type/index.js';
3
+ import { aggregateOf, isInlinedExpression } from './field.util.js';
4
+ import { entityName, hasKeys } from './object.util.js';
5
5
  export function raw(value, ...rest) {
6
6
  if (!isTemplateStrings(value)) {
7
7
  return new QueryRaw(value);
@@ -18,7 +18,14 @@ export function raw(value, ...rest) {
18
18
  }
19
19
  ctx.append(value[i + 1]);
20
20
  });
21
- });
21
+ }, undefined, rest.length === 0 ? value[0] : undefined);
22
+ }
23
+ /**
24
+ * The SQL of a `raw` that names a constant, for a DDL clause with no dialect to render against and
25
+ * nowhere to bind a value; `undefined` where it interpolates and so needs one.
26
+ */
27
+ export function constantSql(value) {
28
+ return value[RAW_TEXT];
22
29
  }
23
30
  /**
24
31
  * The fields of `entity` as {@link ColumnRef}s, each rendering inside `raw` as its column: named the way
@@ -76,26 +83,51 @@ function relationAggregate(spec) {
76
83
  opts.dialect.appendRelationAggregate(opts.ctx, opts.entity, spec, opts.prefix);
77
84
  });
78
85
  }
79
- /** One field as SQL, against its own entity or, read off a definition, the entity rendering it. */
80
- function columnRef(entity, key) {
86
+ /**
87
+ * The fields of `E` as the row a trigger body reads them off, qualified by the side it names: `NEW."col"`
88
+ * against the incoming row, `OLD."col"` against the outgoing one. Columns only, never a relation's
89
+ * aggregate: that is a subquery, and a trigger fires on one row rather than over a table to correlate to.
90
+ */
91
+ export function rowRefs(qualifier) {
92
+ return new Proxy({}, { get: (_, key) => rowColumn(qualifier, String(key)) });
93
+ }
94
+ /** One field of a trigger's row, for code that names it by its key rather than off {@link rowRefs}. */
95
+ export function rowColumn(qualifier, key) {
96
+ return columnRef(undefined, key, qualifier);
97
+ }
98
+ /**
99
+ * One field as SQL, against its own entity or, read off a definition, the entity rendering it. A
100
+ * `qualifier` names the row it reads from, `NEW` or `OLD`, instead of the alias in scope.
101
+ */
102
+ function columnRef(entity, key, qualifier) {
81
103
  return new ColumnRef(key, (opts) => {
82
104
  const owner = entity ?? opts.entity;
83
105
  if (!owner) {
84
106
  throw new TypeError(`'${key}' was read off a definition's refs, so it renders only inside its entity's SQL`);
85
107
  }
86
- renderColumn(getMeta(owner), key, { ...opts, entity: owner });
108
+ renderColumn(getMeta(owner), key, { ...opts, entity: owner }, qualifier);
87
109
  });
88
110
  }
89
- /** A field's column, or the expression an inlined computed one stands for, as a `$where` on it reads it. */
90
- function renderColumn(meta, key, opts) {
111
+ /**
112
+ * A field's column, or the expression an inlined computed one stands for, as a `$where` on it reads it.
113
+ * Under a `qualifier` the column is read off that row rather than off the alias in scope, and the row is
114
+ * written verbatim: it is a record the engine declares, not an identifier to quote and case-fold.
115
+ */
116
+ function renderColumn(meta, key, opts, qualifier) {
117
+ const scope = qualifier === undefined ? opts : { ...opts, escapedPrefix: `${qualifier}.` };
91
118
  const field = meta.fields[key];
92
119
  if (field && isInlinedExpression(field)) {
93
- opts.ctx.append('(');
94
- field.computed.render(opts);
95
- opts.ctx.append(')');
120
+ // A relation aggregate is a subquery correlated to a table in scope, and a trigger's row is not one.
121
+ if (qualifier !== undefined && aggregateOf(field)) {
122
+ throw new TypeError(`'${entityName(meta)}.${key}' reads a relation, which a trigger's row cannot: it fires on one row, ` +
123
+ 'with no table in scope to correlate a subquery to. Name the columns it is derived from instead.');
124
+ }
125
+ scope.ctx.append('(');
126
+ field.computed.render(scope);
127
+ scope.ctx.append(')');
96
128
  return;
97
129
  }
98
- opts.ctx.append(opts.escapedPrefix + opts.dialect.escapeId(opts.dialect.columnOf(meta, key), true));
130
+ scope.ctx.append(scope.escapedPrefix + scope.dialect.escapeId(scope.dialect.columnOf(meta, key), true));
99
131
  }
100
132
  /** A tag call passes the frozen strings array, which carries its own `raw` counterpart. */
101
133
  function isTemplateStrings(value) {
@@ -23,6 +23,18 @@ export declare function qualifyName(name: string, schema?: string): string;
23
23
  export declare function derivedConstraintName(table: string, parts: readonly (string | number)[], kind: ConstraintKind): string;
24
24
  /** The kinds of derived name, which is also what `indexNameStem` strips to compare them. */
25
25
  export type ConstraintKind = 'pk' | 'fk' | 'idx' | 'ck' | 'uk';
26
+ /**
27
+ * Whether uql installed the object called `name`. Ownership is the prefix and nothing else, since no
28
+ * engine records who created one - so this is the only thing standing between a hand-written trigger and
29
+ * a `DROP`, and it is asked on both sides: when reading the catalogue, and again before emitting.
30
+ */
31
+ export declare function isOwnedName(name: string): boolean;
32
+ /**
33
+ * The identifier uql installs a schema object under: its own prefix, the table it hangs off, the label
34
+ * the author gave it, and last a hash of the object's `content`, which no clamping cuts. The table keeps
35
+ * two entities sharing a label apart where an engine scopes such names to the schema.
36
+ */
37
+ export declare function ownedName(table: string, label: string, content: string): string;
26
38
  /**
27
39
  * The name a derived index gets when nothing named it: `Order__total_idx`, or `Order__total_uk` for a
28
40
  * unique one - which the builder has always spelled apart, and which reads as what it enforces.
@@ -1,3 +1,4 @@
1
+ import { OWNED_PREFIX } from '../dialect/aliases.js';
1
2
  import { hasKeys } from './object.util.js';
2
3
  /** Pre-computed regex for each SQL identifier escape character to avoid per-call allocation. */
3
4
  const escapeIdRegexCache = { '`': /`/g, '"': /"/g };
@@ -75,13 +76,33 @@ export function derivedConstraintName(table, parts, kind) {
75
76
  }
76
77
  /** Between table and columns, doubled: index names share one namespace per database, where `a` + `b_c` and `a_b` + `c` would collide. */
77
78
  const TABLE_SEPARATOR = '__';
79
+ /** What every name uql installs begins with, so the two ends asking about one cannot spell it apart. */
80
+ const OWNED_START = `${OWNED_PREFIX}_`;
81
+ /**
82
+ * Whether uql installed the object called `name`. Ownership is the prefix and nothing else, since no
83
+ * engine records who created one - so this is the only thing standing between a hand-written trigger and
84
+ * a `DROP`, and it is asked on both sides: when reading the catalogue, and again before emitting.
85
+ */
86
+ export function isOwnedName(name) {
87
+ return name.startsWith(OWNED_START);
88
+ }
89
+ /**
90
+ * The identifier uql installs a schema object under: its own prefix, the table it hangs off, the label
91
+ * the author gave it, and last a hash of the object's `content`, which no clamping cuts. The table keeps
92
+ * two entities sharing a label apart where an engine scopes such names to the schema.
93
+ */
94
+ export function ownedName(table, label, content) {
95
+ const version = `_${hashIdentifier(content)}`;
96
+ return clampIdentifier(`${OWNED_START}${table}${TABLE_SEPARATOR}${label}`, version.length) + version;
97
+ }
78
98
  /** A name the engine stores whole, shortened around a hash of the full one, which stays stable across runs. */
79
- function clampIdentifier(name) {
80
- if (name.length <= MAX_IDENTIFIER_LENGTH) {
99
+ function clampIdentifier(name, reserved = 0) {
100
+ const max = MAX_IDENTIFIER_LENGTH - reserved;
101
+ if (name.length <= max) {
81
102
  return name;
82
103
  }
83
104
  const suffix = `_${hashIdentifier(name)}`;
84
- return name.slice(0, MAX_IDENTIFIER_LENGTH - suffix.length) + suffix;
105
+ return name.slice(0, max - suffix.length) + suffix;
85
106
  }
86
107
  /**
87
108
  * FNV-1a, by hand: the package ships zero runtime dependencies, and `node:crypto` is not reachable
@@ -18,6 +18,8 @@ export declare class UqlUsageError extends TypeError {
18
18
  /** What an HTTP transport answers with. */
19
19
  readonly status = 400;
20
20
  }
21
+ /** What a value is, for a refusal naming what `/http` handed over instead of what the types require. */
22
+ export declare function kindOf(value: unknown): string;
21
23
  /**
22
24
  * @deprecated since 0.77.1 - use {@link UqlUsageError}, which every misuse throws, lock or not. The
23
25
  * same class under both names, so an existing `instanceof` keeps working.
@@ -11,6 +11,10 @@ export class UqlUsageError extends TypeError {
11
11
  /** What an HTTP transport answers with. */
12
12
  status = 400;
13
13
  }
14
+ /** What a value is, for a refusal naming what `/http` handed over instead of what the types require. */
15
+ export function kindOf(value) {
16
+ return value === null ? 'null' : Array.isArray(value) ? 'array' : typeof value;
17
+ }
14
18
  /**
15
19
  * @deprecated since 0.77.1 - use {@link UqlUsageError}, which every misuse throws, lock or not. The
16
20
  * same class under both names, so an existing `instanceof` keeps working.