uql-orm 0.81.0 → 0.83.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 (113) hide show
  1. package/README.md +3 -3
  2. package/dist/browser/querier/httpQuerier.d.ts +2 -2
  3. package/dist/browser/querier/httpQuerier.js +2 -1
  4. package/dist/browser/type/clientQuerier.d.ts +2 -2
  5. package/dist/browser/uql-browser.min.js +2 -2
  6. package/dist/browser/uql-browser.min.js.map +9 -8
  7. package/dist/bunSql/bunSql.util.js +2 -1
  8. package/dist/cockroachdb/crdbQuerierPool.js +2 -2
  9. package/dist/dialect/abstractSqlDialect.d.ts +16 -6
  10. package/dist/dialect/abstractSqlDialect.js +110 -41
  11. package/dist/dialect/hydrateColumn.js +2 -12
  12. package/dist/dialect/mysqlLikeSqlDialect.d.ts +2 -0
  13. package/dist/dialect/mysqlLikeSqlDialect.js +5 -0
  14. package/dist/dialect/operators.d.ts +7 -1
  15. package/dist/dialect/operators.js +13 -1
  16. package/dist/dialect/pgLikeSqlDialect.d.ts +1 -0
  17. package/dist/dialect/pgLikeSqlDialect.js +13 -4
  18. package/dist/entity/metadata/definition.d.ts +1 -2
  19. package/dist/entity/metadata/definition.js +37 -39
  20. package/dist/http/handler.js +5 -4
  21. package/dist/http/query.d.ts +1 -1
  22. package/dist/http/query.js +2 -2
  23. package/dist/index.d.ts +1 -0
  24. package/dist/index.js +1 -0
  25. package/dist/maria/mariadbQuerierPool.js +4 -2
  26. package/dist/migrate/acquireQuerierForMigrations.js +2 -1
  27. package/dist/migrate/assertCliConfig.js +7 -6
  28. package/dist/migrate/bin.js +0 -0
  29. package/dist/migrate/builder/expressions.d.ts +2 -0
  30. package/dist/migrate/builder/expressions.js +20 -10
  31. package/dist/migrate/builder/tableBuilder.js +1 -1
  32. package/dist/migrate/cli-config.js +5 -4
  33. package/dist/migrate/ddl/indexDdl.js +4 -3
  34. package/dist/migrate/ddl/mysqlIndexDdl.js +5 -4
  35. package/dist/migrate/ddl/pgIndexDdl.js +2 -1
  36. package/dist/migrate/ddl/sqliteIndexDdl.js +2 -1
  37. package/dist/migrate/ddl/tableDdl.js +2 -1
  38. package/dist/migrate/generator/mongoCommand.js +2 -1
  39. package/dist/migrate/generator/mongoSchemaGenerator.d.ts +1 -1
  40. package/dist/migrate/generator/mongoSchemaGenerator.js +7 -6
  41. package/dist/migrate/indexPredicate.js +2 -1
  42. package/dist/migrate/introspection/abstractSqlSchemaIntrospector.js +2 -1
  43. package/dist/migrate/introspection/mongoIntrospector.d.ts +1 -1
  44. package/dist/migrate/introspection/mongoIntrospector.js +3 -2
  45. package/dist/migrate/introspection/mssqlIntrospector.js +13 -1
  46. package/dist/migrate/introspection/mysqlIntrospector.d.ts +2 -0
  47. package/dist/migrate/introspection/mysqlIntrospector.js +6 -2
  48. package/dist/migrate/introspection/postgresIntrospector.js +1 -1
  49. package/dist/migrate/migrationTarget.js +2 -1
  50. package/dist/migrate/migrator.js +2 -1
  51. package/dist/migrate/schemaGenerator.js +5 -4
  52. package/dist/migrate/storage/databaseStorage.js +1 -1
  53. package/dist/migrate/triggerSql.d.ts +1 -1
  54. package/dist/migrate/triggerSql.js +77 -61
  55. package/dist/mongo/mongoDialect.d.ts +1 -3
  56. package/dist/mongo/mongoDialect.js +9 -14
  57. package/dist/mongo/mongodbQuerier.js +7 -10
  58. package/dist/mssql/mssqlQuerier.d.ts +2 -0
  59. package/dist/mssql/mssqlQuerier.js +8 -5
  60. package/dist/mysql/mysql2QuerierPool.d.ts +1 -0
  61. package/dist/mysql/mysql2QuerierPool.js +20 -2
  62. package/dist/neon/neonQuerierPool.js +2 -2
  63. package/dist/pglite/pgliteQuerierPool.js +10 -4
  64. package/dist/postgres/pgQuerierPool.js +2 -2
  65. package/dist/postgres/{pgNumericTypes.d.ts → pgWireTypes.d.ts} +4 -3
  66. package/dist/postgres/{pgNumericTypes.js → pgWireTypes.js} +7 -3
  67. package/dist/querier/abstractQuerier.d.ts +9 -4
  68. package/dist/querier/abstractQuerier.js +26 -19
  69. package/dist/querier/abstractQuerierPool.d.ts +3 -3
  70. package/dist/querier/abstractSqlQuerier.d.ts +2 -2
  71. package/dist/querier/abstractSqlQuerier.js +1 -1
  72. package/dist/querier/abstractSqlQuerierPool.d.ts +2 -2
  73. package/dist/querier/queryError.d.ts +2 -2
  74. package/dist/schema/canonicalType.d.ts +3 -0
  75. package/dist/schema/canonicalType.js +31 -9
  76. package/dist/schema/schemaASTBuilder.js +2 -1
  77. package/dist/schema/schemaASTDiffer.js +4 -2
  78. package/dist/sqlite/sqliteDialect.d.ts +1 -3
  79. package/dist/sqlite/sqliteDialect.js +3 -6
  80. package/dist/type/dialect.d.ts +23 -1
  81. package/dist/type/entity.d.ts +16 -12
  82. package/dist/type/logger.d.ts +2 -2
  83. package/dist/type/querier.d.ts +3 -3
  84. package/dist/type/query.d.ts +3 -13
  85. package/dist/type/queryAggregate.d.ts +4 -10
  86. package/dist/type/queryRaw.d.ts +17 -3
  87. package/dist/type/queryRaw.js +2 -1
  88. package/dist/type/queryWhere.d.ts +7 -7
  89. package/dist/type/universalQuerier.d.ts +3 -3
  90. package/dist/type/vector.d.ts +2 -1
  91. package/dist/type/vector.js +2 -1
  92. package/dist/util/date.d.ts +11 -0
  93. package/dist/util/date.js +19 -0
  94. package/dist/util/dialect.util.d.ts +13 -5
  95. package/dist/util/dialect.util.js +28 -20
  96. package/dist/util/field.util.d.ts +4 -4
  97. package/dist/util/field.util.js +10 -2
  98. package/dist/util/fieldOption.util.d.ts +5 -3
  99. package/dist/util/fieldOption.util.js +6 -5
  100. package/dist/util/hook.util.d.ts +1 -1
  101. package/dist/util/hook.util.js +8 -1
  102. package/dist/util/index.d.ts +1 -0
  103. package/dist/util/index.js +1 -0
  104. package/dist/util/logger.d.ts +3 -3
  105. package/dist/util/object.util.js +3 -2
  106. package/dist/util/raw.d.ts +6 -7
  107. package/dist/util/raw.js +10 -12
  108. package/dist/util/sqlLiteral.d.ts +8 -1
  109. package/dist/util/sqlLiteral.js +14 -9
  110. package/dist/util/triggerWrite.d.ts +15 -0
  111. package/dist/util/triggerWrite.js +20 -0
  112. package/package.json +1 -1
  113. package/skills/uql-orm/SKILL.md +4 -4
@@ -0,0 +1,19 @@
1
+ /**
2
+ * `YYYY-MM-DD HH:mm:ss.SSS` in UTC, then `zone`: how a date is written, whichever machine writes it. Not
3
+ * `toISOString` as it is, whose `T` and `Z` MySQL rejects outright ("Invalid default value").
4
+ */
5
+ export function utcTimestamp(date, zone = '') {
6
+ return date.toISOString().replace('T', ' ').replace('Z', zone);
7
+ }
8
+ /** A time of day that names no zone, which `Date` would read in the process's own. */
9
+ const ZONELESS_TIME = /T[\d:.]+$/;
10
+ /**
11
+ * A timestamp's text as the `Date` it names, the one rule every driver and hydration read dates by: UTC
12
+ * where it names no zone, its own offset where it does, a bare day at UTC midnight, and the fraction cut
13
+ * to the milliseconds a `Date` holds. Text that is none of these, such as `infinity`, stays text.
14
+ */
15
+ export function decodeDate(text) {
16
+ const iso = text.replace(' ', 'T').replace(/(\.\d{3})\d+/, '$1');
17
+ const date = new Date(ZONELESS_TIME.test(iso) ? `${iso}Z` : iso);
18
+ return Number.isNaN(date.getTime()) ? text : date;
19
+ }
@@ -1,5 +1,5 @@
1
1
  import type { IndexType } from '../schema/types.js';
2
- import { type AggregateCall, type CascadeType, type EntityData, type EntityId, type EntityIndexMeta, type EntityMeta, type FieldKey, type FieldOptions, type FieldUpdateOp, type JsonUpdateOp, type OnFieldCallback, type Query, type QueryAggMap, type QueryExclude, type QueryGroupMap, type QueryOptions, QueryRaw, type QuerySearch, type QuerySelect, type QuerySelectValue, type QuerySortDirection, type QuerySizeComparisonOps, type QuerySortMap, type QueryTextSearchOptions, type QueryVectorQuery, type QueryVectorSearch, type QueryWhere, type RelationKey, type UpdatePayload, type VectorDistance } from '../type/index.js';
2
+ import { type AggregateCall, type CascadeType, type EntityData, type EntityId, type EntityIndexMeta, type EntityMeta, type FieldKey, type FieldOptions, type FieldUpdateOp, type JsonUpdateOp, type OnFieldCallback, type Query, type QueryAggMap, type QueryConflictPaths, type QueryExclude, type QueryGroupMap, type QueryOptions, QueryRaw, type QuerySearch, type QuerySelect, type QuerySelectValue, type QuerySortDirection, type QuerySizeComparisonOps, type QuerySortMap, type QueryTextSearchOptions, type QueryVectorQuery, type QueryVectorSearch, type QueryWhere, type QueryWhereArray, type RelationKey, type UpdatePayload, type VectorDistance } from '../type/index.js';
3
3
  export type CallbackKey = keyof Pick<FieldOptions, 'onInsert' | 'onUpdate'>;
4
4
  /** The keys of `payload` a write persists as columns. */
5
5
  export declare function filterFieldKeys<E>(meta: EntityMeta<E>, payload: EntityData<E> | UpdatePayload<E>, callbackKey: CallbackKey): FieldKey<E>[];
@@ -50,10 +50,18 @@ export declare function isCascadable(action: CascadeType, configuration?: boolea
50
50
  */
51
51
  export declare function isPagedQuery<E>(q: QuerySearch<E>): boolean;
52
52
  /**
53
- * `q` selecting nothing but the id: what a write hands its backend's own read builder to settle the
54
- * rows it will name. The cast is unavoidable - a computed key is not a `QuerySelect` key to the
55
- * compiler - so it is spelled once here rather than in each querier.
56
- */
53
+ * Each of `keys` switched on, as a `$select` or conflict paths name them. This and the `where*` builders
54
+ * below hold the casts a statement built in generic code needs: a key read at run time is no key of
55
+ * these maps to the compiler, whose values it works out per entity.
56
+ */
57
+ export declare function keySet<E>(keys: readonly FieldKey<E>[]): QueryConflictPaths<E>;
58
+ /** `where`, or no `$where`, with `key` held to `value` as well: spread, so the two `AND`. */
59
+ export declare function whereWith<E>(key: FieldKey<E>, value: unknown, where?: QueryWhere<E>): QueryWhere<E>;
60
+ /** The `$where` holding each of `keys` to what `valueOf` reads for it. */
61
+ export declare function whereEach<E>(keys: readonly FieldKey<E>[], valueOf: (key: FieldKey<E>) => unknown): QueryWhere<E>;
62
+ /** The `$where` any one of `clauses` satisfies. */
63
+ export declare function whereAnyOf<E>(clauses: QueryWhereArray<E>): QueryWhere<E>;
64
+ /** `q` selecting nothing but the id: what a write hands its backend's own read builder to settle the rows it will name. */
57
65
  export declare function idOnlyQuery<E>(meta: EntityMeta<E>, q: QuerySearch<E>): Query<E>;
58
66
  /**
59
67
  * The map form of a `$select` value, or `undefined` for the raw-array form. Centralizes the one
@@ -2,7 +2,7 @@ import { getContext, UqlSecurityError } from '../context/context.js';
2
2
  import { soleIdOf } from '../entity/metadata/definition.js';
3
3
  import { QueryRaw, resolveAggregateOp, SOFT_DELETE_FILTER, } from '../type/index.js';
4
4
  import { DEFAULT_VECTOR_DISTANCE, VECTOR_INDEX_TYPES } from '../type/vector.js';
5
- import { getFieldKeys, isDatabaseWritten } from './field.util.js';
5
+ import { defaultReadKeys, fieldKeys, isDatabaseWritten } from './field.util.js';
6
6
  import { entityName, getKeys, hasKeys, isOperatorObject, isScalarId, isRecord, isWhereMap, someKey, } from './object.util.js';
7
7
  import { UqlUsageError } from './uqlError.js';
8
8
  /** The keys of `payload` a write persists as columns. */
@@ -50,12 +50,7 @@ export function getInsertFieldKeys(meta, payloads) {
50
50
  for (const record of payloads) {
51
51
  addInsertFieldKeys(meta, record, seen, keys);
52
52
  }
53
- for (const key of getKeys(meta.fields)) {
54
- if (meta.fields[key].onInsert !== undefined && !seen.has(key)) {
55
- keys.push(key);
56
- }
57
- }
58
- return keys;
53
+ return [...keys, ...fieldKeys(meta, (field) => field.onInsert !== undefined).filter((key) => !seen.has(key))];
59
54
  }
60
55
  export function getFieldCallbackValue(val) {
61
56
  return typeof val === 'function' ? val() : val;
@@ -72,7 +67,7 @@ export function fillOnFields(meta, payload, callbackKey) {
72
67
  const payloads = Array.isArray(payload) ? payload : [payload];
73
68
  // By presence, not truthiness, as `addInsertFieldKeys` above reads it: `onInsert: 0` and `onInsert: ''`
74
69
  // are values a caller meant, and a falsy one was silently never filled.
75
- const keys = getKeys(meta.fields).filter((key) => meta.fields[key][callbackKey] !== undefined);
70
+ const keys = fieldKeys(meta, (field) => field[callbackKey] !== undefined);
76
71
  if (keys.length === 0) {
77
72
  return payloads;
78
73
  }
@@ -120,12 +115,28 @@ export function isPagedQuery(q) {
120
115
  return q.$sort !== undefined || q.$limit !== undefined || q.$skip !== undefined;
121
116
  }
122
117
  /**
123
- * `q` selecting nothing but the id: what a write hands its backend's own read builder to settle the
124
- * rows it will name. The cast is unavoidable - a computed key is not a `QuerySelect` key to the
125
- * compiler - so it is spelled once here rather than in each querier.
118
+ * Each of `keys` switched on, as a `$select` or conflict paths name them. This and the `where*` builders
119
+ * below hold the casts a statement built in generic code needs: a key read at run time is no key of
120
+ * these maps to the compiler, whose values it works out per entity.
126
121
  */
122
+ export function keySet(keys) {
123
+ return Object.fromEntries(keys.map((key) => [key, true]));
124
+ }
125
+ /** `where`, or no `$where`, with `key` held to `value` as well: spread, so the two `AND`. */
126
+ export function whereWith(key, value, where) {
127
+ return { ...where, [key]: value };
128
+ }
129
+ /** The `$where` holding each of `keys` to what `valueOf` reads for it. */
130
+ export function whereEach(keys, valueOf) {
131
+ return Object.fromEntries(keys.map((key) => [key, valueOf(key)]));
132
+ }
133
+ /** The `$where` any one of `clauses` satisfies. */
134
+ export function whereAnyOf(clauses) {
135
+ return { $or: clauses };
136
+ }
137
+ /** `q` selecting nothing but the id: what a write hands its backend's own read builder to settle the rows it will name. */
127
138
  export function idOnlyQuery(meta, q) {
128
- return { ...q, $select: Object.fromEntries(meta.ids.map((key) => [key, true])) };
139
+ return { ...q, $select: keySet(meta.ids) };
129
140
  }
130
141
  /**
131
142
  * The map form of a `$select` value, or `undefined` for the raw-array form. Centralizes the one
@@ -165,7 +176,7 @@ export function normalizeScalarFieldSelection(meta, select, exclude) {
165
176
  }
166
177
  }
167
178
  }
168
- const allFields = getFieldKeys(meta.fields);
179
+ const allFields = defaultReadKeys(meta);
169
180
  if (!excludedFields) {
170
181
  return allFields;
171
182
  }
@@ -217,10 +228,7 @@ export function findVectorIndex(meta, key) {
217
228
  * serves no other, else cosine. The one fallback every engine resolves, so none can rank by another.
218
229
  */
219
230
  export function vectorDistanceOf(meta, key, search) {
220
- return (search.$distance ??
221
- meta.fields[key]?.distance ??
222
- findVectorIndex(meta, key)?.distance ??
223
- DEFAULT_VECTOR_DISTANCE);
231
+ return (search.$distance ?? meta.fields[key]?.distance ?? findVectorIndex(meta, key)?.distance ?? DEFAULT_VECTOR_DISTANCE);
224
232
  }
225
233
  /**
226
234
  * Whether a `$where` filters by vector distance anywhere in its tree, `$and`/`$or`/`$not` included.
@@ -271,7 +279,7 @@ export function fieldUpdateOf(key, value) {
271
279
  */
272
280
  export function whereIds(meta, ids) {
273
281
  if (Array.isArray(ids) ? ids.every(isScalarId) : isScalarId(ids)) {
274
- return { [soleIdOf(meta, 'addressing by a bare id value')]: ids };
282
+ return whereWith(soleIdOf(meta, 'addressing by a bare id value'), ids);
275
283
  }
276
284
  return (Array.isArray(ids) ? { $or: ids } : ids);
277
285
  }
@@ -492,11 +500,11 @@ export function fulltextWeights(index) {
492
500
  return undefined;
493
501
  }
494
502
  if (index.type !== 'fulltext') {
495
- throw new TypeError(`a column weight ranks a fulltext index, and this one is ${index.type ?? 'btree'}`);
503
+ throw new UqlUsageError(`a column weight ranks a fulltext index, and this one is ${index.type ?? 'btree'}`);
496
504
  }
497
505
  const weights = index.entries.map(({ weight = 1 }) => {
498
506
  if (!Number.isInteger(weight) || weight < 1 || weight > MAX_TEXT_WEIGHT) {
499
- throw new TypeError(`a column weight is a whole number from 1 to ${MAX_TEXT_WEIGHT}, not ${weight}`);
507
+ throw new UqlUsageError(`a column weight is a whole number from 1 to ${MAX_TEXT_WEIGHT}, not ${weight}`);
500
508
  }
501
509
  return weight;
502
510
  });
@@ -1,4 +1,4 @@
1
- import { type ColumnFamily, type EntityMeta, type FieldKey, type FieldOptions, type StampEvent, type RelationAggregateSpec } from '../type/index.js';
1
+ import { type ColumnFamily, type EntityMeta, type FieldKey, type FieldMeta, 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
  /**
@@ -39,11 +39,11 @@ export declare function isSoleIdField<E>(meta: EntityMeta<E>, field: FieldOption
39
39
  * states its width. The one answer the create statement and the diff both read.
40
40
  */
41
41
  export declare function isAutoIncrement(field: FieldOptions, isPrimaryKey: boolean): boolean;
42
+ /** The fields `meta` declares whose options `pick` accepts, in declaration order, each as the entity's own key. */
43
+ export declare function fieldKeys<E>(meta: EntityMeta<E>, pick: (field: FieldMeta) => unknown): FieldKey<E>[];
42
44
  /**
43
45
  * The fields a read answers with where it names none. A relation aggregate is left out unless it asks
44
46
  * for `eager: true`: it reads the related rows, which is what a relation does, and a relation is loaded
45
47
  * only when a query asks for it. Naming one in `$select` reads it, whatever the default.
46
48
  */
47
- export declare function getFieldKeys<E>(fields: {
48
- [K in FieldKey<E>]?: FieldOptions;
49
- }): FieldKey<E>[];
49
+ export declare function defaultReadKeys<E>(meta: EntityMeta<E>): FieldKey<E>[];
@@ -83,11 +83,19 @@ export function isAutoIncrement(field, isPrimaryKey) {
83
83
  return field.autoIncrement;
84
84
  return isPrimaryKey && columnFamily(field.type) === 'numeric' && !field.onInsert && !field.references;
85
85
  }
86
+ /** The fields `meta` declares whose options `pick` accepts, in declaration order, each as the entity's own key. */
87
+ export function fieldKeys(meta, pick) {
88
+ const fields = meta.fields;
89
+ return getKeys(fields).filter((key) => {
90
+ const field = fields[key];
91
+ return field !== undefined && Boolean(pick(field));
92
+ });
93
+ }
86
94
  /**
87
95
  * The fields a read answers with where it names none. A relation aggregate is left out unless it asks
88
96
  * for `eager: true`: it reads the related rows, which is what a relation does, and a relation is loaded
89
97
  * only when a query asks for it. Naming one in `$select` reads it, whatever the default.
90
98
  */
91
- export function getFieldKeys(fields) {
92
- return getKeys(fields).filter((field) => fields[field].eager ?? !aggregateOf(fields[field]));
99
+ export function defaultReadKeys(meta) {
100
+ return fieldKeys(meta, (field) => field.eager ?? !aggregateOf(field));
93
101
  }
@@ -1,6 +1,6 @@
1
1
  import { type ColumnFamily, type FamilyOf, type FieldOptions, QueryRaw, type StampEvent } from '../type/index.js';
2
2
  /**
3
- * The column family each field option means anything on, or `'*'` where it applies to every column.
3
+ * The column families 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
5
5
  * discipline `INDEX_FEATURE_LABELS` uses for index features.
6
6
  */
@@ -23,7 +23,7 @@ declare const FIELD_OPTION_FAMILY: {
23
23
  readonly version: 'numeric';
24
24
  readonly columnType: '*';
25
25
  readonly length: 'string';
26
- readonly precision: 'numeric';
26
+ readonly precision: readonly ["numeric", "date"];
27
27
  readonly scale: 'numeric';
28
28
  readonly nullable: '*';
29
29
  readonly unique: '*';
@@ -91,8 +91,10 @@ type DeadOptions<O> = (O extends {
91
91
  readonly nullable: true;
92
92
  } ? 'nullable' : never);
93
93
  type Given<O> = Extract<keyof O, keyof FieldOptions>;
94
+ /** The families option `K` applies to, `'*'` for every one. */
95
+ type OptionFamilies<K extends keyof FieldOptions> = (typeof FIELD_OPTION_FAMILY)[K] extends readonly (infer F)[] ? F : (typeof FIELD_OPTION_FAMILY)[K];
94
96
  type Offending<O> = {
95
- [K in Given<O>]: (typeof FIELD_OPTION_FAMILY)[K] extends OptionsFamily<O> | '*' ? K extends DeadOptions<O> ? K : never : K;
97
+ [K in Given<O>]: [Extract<OptionFamilies<K>, OptionsFamily<O> | '*'>] extends [never] ? K : K extends DeadOptions<O> ? K : never;
96
98
  }[Given<O>];
97
99
  /**
98
100
  * Every option `O` states but cannot use, mapped to `never`, so one that would be ignored does not compile.
@@ -3,7 +3,7 @@ import { columnFamily, isInlinedExpression } from './field.util.js';
3
3
  import { getKeys } from './object.util.js';
4
4
  import { constantSql } from './raw.js';
5
5
  /**
6
- * The column family each field option means anything on, or `'*'` where it applies to every column.
6
+ * The column families each field option means anything on, or `'*'` where it applies to every column.
7
7
  * Exhaustive over {@link FieldOptions}, so a new option cannot be added without placing it - the
8
8
  * discipline `INDEX_FEATURE_LABELS` uses for index features.
9
9
  */
@@ -26,7 +26,8 @@ const FIELD_OPTION_FAMILY = {
26
26
  version: 'numeric',
27
27
  columnType: '*',
28
28
  length: 'string',
29
- precision: 'numeric',
29
+ // A decimal's digits, or a timestamp's fractional-second digits.
30
+ precision: ['numeric', 'date'],
30
31
  scale: 'numeric',
31
32
  nullable: '*',
32
33
  unique: '*',
@@ -116,11 +117,11 @@ export function fieldOptionConflict(opts) {
116
117
  // conflicts always reports the same one. An option no rule knows is a typo, which `@Field`'s own check
117
118
  // reports where it can still be spelled right.
118
119
  for (const key of getKeys(FIELD_OPTION_FAMILY)) {
119
- const applies = FIELD_OPTION_FAMILY[key];
120
+ const applies = [FIELD_OPTION_FAMILY[key]].flat();
120
121
  if (opts[key] === undefined)
121
122
  continue;
122
- if (family && applies !== '*' && applies !== family) {
123
- return `cannot use '${key}': it applies to a ${applies} column, not to a ${family} one`;
123
+ if (family && !applies.includes('*') && !applies.includes(family)) {
124
+ return `cannot use '${key}': it applies to a ${applies.join(' or ')} column, not to a ${family} one`;
124
125
  }
125
126
  const dead = deadOn(opts, key);
126
127
  if (dead) {
@@ -11,4 +11,4 @@ export type HookContext = {
11
11
  * Hooks are invoked with `this` bound to the payload via `call`,
12
12
  * so mutations go directly to the original object.
13
13
  */
14
- export declare function runHooks<E extends object>(entity: Type<E>, event: HookEvent, payloads: E[], ctx: HookContext): Promise<void>;
14
+ export declare function runHooks<E extends object>(entity: Type<E>, event: HookEvent, payloads: readonly E[], ctx: HookContext): Promise<void>;
@@ -1,4 +1,5 @@
1
1
  import { getMeta } from '../entity/index.js';
2
+ import { UqlUsageError } from './uqlError.js';
2
3
  /**
3
4
  * Run all registered hooks for the given event on each payload.
4
5
  * Hooks are invoked with `this` bound to the payload via `call`,
@@ -9,9 +10,15 @@ export async function runHooks(entity, event, payloads, ctx) {
9
10
  const registrations = meta.hooks?.[event];
10
11
  if (!registrations?.length)
11
12
  return;
13
+ // A prototype is typed `any`, so what is read off it is typed here, where it enters.
14
+ const prototype = entity.prototype;
12
15
  for (const payload of payloads) {
13
16
  for (const { methodName } of registrations) {
14
- const result = entity.prototype[methodName].call(payload, ctx);
17
+ const method = prototype[methodName];
18
+ if (typeof method !== 'function') {
19
+ throw new UqlUsageError(`'${entity.name}' runs '${methodName}' on ${event}, but has no such method`);
20
+ }
21
+ const result = method.call(payload, ctx);
15
22
  if (result instanceof Promise)
16
23
  await result;
17
24
  }
@@ -7,6 +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
11
  export * from './rowKey.util.js';
11
12
  export * from './relationQuery.util.js';
12
13
  export * from './sql.util.js';
@@ -7,6 +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
11
  export * from './rowKey.util.js';
11
12
  export * from './relationQuery.util.js';
12
13
  export * from './sql.util.js';
@@ -3,8 +3,8 @@ import type { Logger, LoggingOptions } from '../type/logger.js';
3
3
  * Default implementation of the Logger interface using console methods.
4
4
  */
5
5
  export declare class DefaultLogger implements Logger {
6
- logQuery(query: string, values?: unknown[], duration?: number): void;
7
- logSlowQuery(query: string, values?: unknown[], duration?: number): void;
6
+ logQuery(query: string, values?: readonly unknown[], duration?: number): void;
7
+ logSlowQuery(query: string, values?: readonly unknown[], duration?: number): void;
8
8
  logWarn(message: string): void;
9
9
  logError(message: string, error?: unknown): void;
10
10
  logInfo(message: string): void;
@@ -37,7 +37,7 @@ export declare class LoggerWrapper implements Logger {
37
37
  constructor(options?: LoggingOptions, config?: LoggerWrapperConfig);
38
38
  /** Whether `logQuery` would ever actually surface bound values, given the configured levels/slowQuery/logValues. */
39
39
  willLogValues(): boolean;
40
- logQuery(query: string, values?: unknown[], duration?: number): void;
40
+ logQuery(query: string, values?: readonly unknown[], duration?: number): void;
41
41
  logWarn(message: string): void;
42
42
  logError(message: string, error?: unknown): void;
43
43
  logInfo(message: string): void;
@@ -1,8 +1,9 @@
1
+ import { UqlUsageError } from './uqlError.js';
1
2
  export function throwPendingTransaction() {
2
- throw TypeError('pending transaction');
3
+ throw new UqlUsageError('pending transaction');
3
4
  }
4
5
  export function throwNoPendingTransaction() {
5
- throw TypeError('not a pending transaction');
6
+ throw new UqlUsageError('not a pending transaction');
6
7
  }
7
8
  export function clone(value) {
8
9
  if (typeof value !== 'object' || value === null) {
@@ -1,4 +1,4 @@
1
- import { ColumnRef, type EntitySql, type EntityWhere, type EntityWhereMeta, QueryRaw, type QueryRawFn, type ComputedRefs, type RefMap, type TriggerRowName, type Type } from '../type/index.js';
1
+ import { 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
@@ -27,10 +27,9 @@ export declare function entitySql<E>(sql: EntitySql<E>): QueryRaw;
27
27
  /** A definition's predicate, its callback resolved the way {@link entitySql} resolves one. */
28
28
  export declare function entityWhere<E>(where: EntityWhere<E>): EntityWhereMeta<E>;
29
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.
30
+ * The fields of `entity` as the row a trigger body reads them off, qualified by the side it names:
31
+ * `NEW."col"` against the incoming row, `OLD."col"` against the outgoing one. Bound to the entity, as
32
+ * {@link refs} are, so each names its own column wherever it renders, a write to another table included.
33
+ * Columns only: a relation's aggregate is a subquery, and a trigger fires on one row, not over a table.
33
34
  */
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;
35
+ export declare function rowRefs<E>(entity: Type<E>, qualifier: TriggerRowName): RefMap<E>;
package/dist/util/raw.js CHANGED
@@ -2,6 +2,7 @@ import { getMeta } from '../entity/metadata/definition.js';
2
2
  import { ColumnRef, QueryRaw, RAW_TEXT, RelationAggregate, } from '../type/index.js';
3
3
  import { aggregateOf, isInlinedExpression } from './field.util.js';
4
4
  import { entityName, hasKeys } from './object.util.js';
5
+ import { UqlUsageError } from './uqlError.js';
5
6
  export function raw(value, ...rest) {
6
7
  if (!isTemplateStrings(value)) {
7
8
  return new QueryRaw(value);
@@ -78,22 +79,19 @@ function rowsOf(q) {
78
79
  function relationAggregate(spec) {
79
80
  return new RelationAggregate(spec, (opts) => {
80
81
  if (!opts.entity) {
81
- throw new TypeError(`'${spec.relation}' was read off a definition's refs, so it renders only inside its entity's SQL`);
82
+ throw new UqlUsageError(`'${spec.relation}' was read off a definition's refs, so it renders only inside its entity's SQL`);
82
83
  }
83
84
  opts.dialect.appendRelationAggregate(opts.ctx, opts.entity, spec, opts.prefix);
84
85
  });
85
86
  }
86
87
  /**
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.
88
+ * The fields of `entity` as the row a trigger body reads them off, qualified by the side it names:
89
+ * `NEW."col"` against the incoming row, `OLD."col"` against the outgoing one. Bound to the entity, as
90
+ * {@link refs} are, so each names its own column wherever it renders, a write to another table included.
91
+ * Columns only: a relation's aggregate is a subquery, and a trigger fires on one row, not over a table.
90
92
  */
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);
93
+ export function rowRefs(entity, qualifier) {
94
+ return new Proxy({}, { get: (_, key) => columnRef(entity, String(key), qualifier) });
97
95
  }
98
96
  /**
99
97
  * One field as SQL, against its own entity or, read off a definition, the entity rendering it. A
@@ -103,7 +101,7 @@ function columnRef(entity, key, qualifier) {
103
101
  return new ColumnRef(key, (opts) => {
104
102
  const owner = entity ?? opts.entity;
105
103
  if (!owner) {
106
- throw new TypeError(`'${key}' was read off a definition's refs, so it renders only inside its entity's SQL`);
104
+ throw new UqlUsageError(`'${key}' was read off a definition's refs, so it renders only inside its entity's SQL`);
107
105
  }
108
106
  renderColumn(getMeta(owner), key, { ...opts, entity: owner }, qualifier);
109
107
  });
@@ -119,7 +117,7 @@ function renderColumn(meta, key, opts, qualifier) {
119
117
  if (field && isInlinedExpression(field)) {
120
118
  // A relation aggregate is a subquery correlated to a table in scope, and a trigger's row is not one.
121
119
  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, ` +
120
+ throw new UqlUsageError(`'${entityName(meta)}.${key}' reads a relation, which a trigger's row cannot: it fires on one row, ` +
123
121
  'with no table in scope to correlate a subquery to. Name the columns it is derived from instead.');
124
122
  }
125
123
  scope.ctx.append('(');
@@ -2,7 +2,14 @@
2
2
  export declare function escapeSingleQuotes(val: string): string;
3
3
  /** The text a MySQL string literal's body stands for: its backslash escapes and doubled quotes undone. */
4
4
  export declare function unescapeMysqlString(body: string): string;
5
- /** Escape `value` for Postgres, SQLite and related dialects (single-quote doubling). */
5
+ /** How a Postgres timestamp says it is UTC. */
6
+ export declare const PG_UTC = "+00";
7
+ /** Escape `value` for SQLite and SQL Server (single-quote doubling). */
6
8
  export declare const escapeAnsiSqlLiteral: (value: unknown) => string;
9
+ /**
10
+ * Escape `value` for the Postgres family: ANSI, a date marked UTC, which a `TIMESTAMPTZ` otherwise reads
11
+ * in the session's zone.
12
+ */
13
+ export declare const escapePgSqlLiteral: (value: unknown) => string;
7
14
  /** Escape `value` for MySQL and MariaDB (backslash escaping). */
8
15
  export declare const escapeMysqlSqlLiteral: (value: unknown) => string;
@@ -1,6 +1,8 @@
1
1
  // SQL literal escaping for `Dialect.escape`: ANSI quote doubling, or MySQL's backslashes. UQL binds
2
2
  // values instead, so this is the hand-written-SQL hatch, and inline MySQL literals break under
3
3
  // `NO_BACKSLASH_ESCAPES` or a GBK-like charset: prefer bound parameters. Postgres arrays are separate.
4
+ import { utcTimestamp } from './date.js';
5
+ import { UqlUsageError } from './uqlError.js';
4
6
  const SINGLE_QUOTE = /'/g;
5
7
  /** Doubles every single quote in `val`, the ANSI escaping shared by string literals and JSON path keys. */
6
8
  export function escapeSingleQuotes(val) {
@@ -42,12 +44,8 @@ function bytesToHexLiteral(bytes) {
42
44
  * A factory, not a function taking `escapeString` as an argument: threading it through every call
43
45
  * measured 1.1-1.5x slower. Rejects unsupported types rather than stringifying them into SQL.
44
46
  */
45
- function createEscaper(escapeString) {
46
- /**
47
- * `YYYY-MM-DD HH:mm:ss.SSS` in UTC, so the SQL is the same whichever machine wrote it. Not `toISOString`
48
- * as it is, whose `T` and `Z` MySQL rejects outright ("Invalid default value").
49
- */
50
- const dateLiteral = (date) => escapeString(date.toISOString().replace('T', ' ').replace('Z', ''));
47
+ function createEscaper(escapeString, zone = '') {
48
+ const dateLiteral = (date) => escapeString(utcTimestamp(date, zone));
51
49
  const sqlList = (arr) => {
52
50
  let sql = '';
53
51
  for (let i = 0; i < arr.length; i++) {
@@ -73,7 +71,7 @@ function createEscaper(escapeString) {
73
71
  if ('toSqlString' in value && typeof value.toSqlString === 'function') {
74
72
  return String(value.toSqlString());
75
73
  }
76
- throw new TypeError('escapeSqlLiteral: plain objects are not supported; use bound parameters or JSON.stringify + a string column.');
74
+ throw new UqlUsageError('escapeSqlLiteral: plain objects are not supported; use bound parameters or JSON.stringify + a string column.');
77
75
  };
78
76
  const escapeValue = (value) => {
79
77
  if (value === undefined || value === null) {
@@ -92,12 +90,19 @@ function createEscaper(escapeString) {
92
90
  return escapeObject(value);
93
91
  default:
94
92
  // A symbol or a function, or a future JS type: none of them may silently become SQL.
95
- throw new TypeError(`escapeSqlLiteral: unsupported value type '${typeof value}'; use bound parameters.`);
93
+ throw new UqlUsageError(`escapeSqlLiteral: unsupported value type '${typeof value}'; use bound parameters.`);
96
94
  }
97
95
  };
98
96
  return escapeValue;
99
97
  }
100
- /** Escape `value` for Postgres, SQLite and related dialects (single-quote doubling). */
98
+ /** How a Postgres timestamp says it is UTC. */
99
+ export const PG_UTC = '+00';
100
+ /** Escape `value` for SQLite and SQL Server (single-quote doubling). */
101
101
  export const escapeAnsiSqlLiteral = createEscaper(ansiStringLiteral);
102
+ /**
103
+ * Escape `value` for the Postgres family: ANSI, a date marked UTC, which a `TIMESTAMPTZ` otherwise reads
104
+ * in the session's zone.
105
+ */
106
+ export const escapePgSqlLiteral = createEscaper(ansiStringLiteral, PG_UTC);
102
107
  /** Escape `value` for MySQL and MariaDB (backslash escaping). */
103
108
  export const escapeMysqlSqlLiteral = createEscaper(mysqlStringLiteral);
@@ -0,0 +1,15 @@
1
+ import type { EntityPredicate, QueryRaw, Type, UpdatePayload, WritableKey, WriteRow } from '../type/index.js';
2
+ /**
3
+ * A row inserted by a trigger's body, into any table: `insertInto(PostAudit, { postId: newRow.id })`.
4
+ * Each value is a literal or SQL, a row's ref most often, and every engine renders it, SQL Server's
5
+ * set-based trigger included, where it inserts one row for each the statement touched.
6
+ */
7
+ export declare function insertInto<E extends object>(entity: Type<E>, row: WriteRow<E>): QueryRaw;
8
+ /** The rows `q.$where` names updated by a trigger's body: `updateTable(Post, { $where: { id: newRow.postId } }, set)`. */
9
+ export declare function updateTable<E extends object>(entity: Type<E>, q: {
10
+ readonly $where: EntityPredicate<E>;
11
+ }, set: UpdatePayload<E, QueryRaw, WritableKey<E>, never>): QueryRaw;
12
+ /** The rows `q.$where` names deleted by a trigger's body, outright: a soft delete is an `updateTable`. */
13
+ export declare function deleteFrom<E extends object>(entity: Type<E>, q: {
14
+ readonly $where: EntityPredicate<E>;
15
+ }): QueryRaw;
@@ -0,0 +1,20 @@
1
+ import { raw } from './raw.js';
2
+ /**
3
+ * A row inserted by a trigger's body, into any table: `insertInto(PostAudit, { postId: newRow.id })`.
4
+ * Each value is a literal or SQL, a row's ref most often, and every engine renders it, SQL Server's
5
+ * set-based trigger included, where it inserts one row for each the statement touched.
6
+ */
7
+ export function insertInto(entity, row) {
8
+ return written({ kind: 'insert', entity, row });
9
+ }
10
+ /** The rows `q.$where` names updated by a trigger's body: `updateTable(Post, { $where: { id: newRow.postId } }, set)`. */
11
+ export function updateTable(entity, q, set) {
12
+ return written({ kind: 'update', entity, set, where: q.$where });
13
+ }
14
+ /** The rows `q.$where` names deleted by a trigger's body, outright: a soft delete is an `updateTable`. */
15
+ export function deleteFrom(entity, q) {
16
+ return written({ kind: 'delete', entity, where: q.$where });
17
+ }
18
+ function written(write) {
19
+ return raw(({ ctx, dialect, rows }) => dialect.triggerWrite(ctx, write, rows));
20
+ }
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "homepage": "https://uql-orm.dev",
4
4
  "description": "The 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.81.0",
6
+ "version": "0.83.0",
7
7
  "type": "module",
8
8
  "engines": {
9
9
  "node": ">=24"
@@ -74,7 +74,7 @@ export class Post {
74
74
  }
75
75
  ```
76
76
 
77
- - Every `@Field` states its `type` (`String`, `Number`, `Boolean`, `Date`, `BigInt`, or a column type such as `'uuid'`, `'text'`, `'jsonb'`), except a foreign key, which takes `references` and inherits the target key's type.
77
+ - Every `@Field` states its `type` (`String`, `Number`, `Boolean`, `Date` (an instant, bound and read as UTC on every engine; `TIMESTAMPTZ` on Postgres and CockroachDB, `DATETIME(3)` on MySQL and MariaDB; `precision` sets its fractional-second digits), `BigInt`, or a column type such as `'uuid'`, `'text'`, `'jsonb'`), except a foreign key, which takes `references` and inherits the target key's type.
78
78
  - A column is nullable unless it says `nullable: false`, and its property must admit `null` to match: `title?: string | null`. A property typed without `| null` on a nullable column is a compile error.
79
79
  - An engine's own column type is a `raw` constant, ``columnType: raw`tsvector` ``, rendered verbatim and carrying its own `length`/`precision`: never a bare string.
80
80
  - Members are named by callbacks, never by strings: `mappedBy: (post) => post.author`, `references: (post) => post.authorId`.
@@ -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; `where` holds a `$where` predicate per row it names, or SQL off the rows. `run` reads `{ newRow }` on insert, `{ newRow, oldRow }` on update, `{ oldRow }` on delete, and returns the engine's own SQL, 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*` and no `where`). 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
@@ -123,7 +123,7 @@ const users = await pool.findMany(User, {
123
123
  - Methods: `findMany`, `findOne`, `findOneById`, `findManyAndCount`, `findManyStream`, `count`, `exists`,
124
124
  `aggregate`, `insertOne`, `insertMany`, `updateOneById`, `updateMany`, `saveOne`, `saveMany`, `upsertOne`,
125
125
  `upsertMany`, `deleteOneById`, `deleteMany`. Each takes the entity class first.
126
- - `updateMany` and `deleteMany` naming no rows - no `$where`, no `$limit` - throw; `{ unfiltered: true }` means the whole table.
126
+ - `updateMany` and `deleteMany` naming no rows - no `$where` holding a value (an `undefined` or an empty group holds none), no `$limit` - throw; `{ unfiltered: true }` means the whole table.
127
127
  - An update takes `{ stock: { $inc: -1 } }` to add, or `$mul` to multiply, in the statement, a NULL counting as 0,
128
128
  so a guard in `$where` (`stock: { $gte: 1 }`) makes a decrement race-safe. JSON fields take `$set`, `$unset`,
129
129
  `$push`, `$pull`.
@@ -133,7 +133,7 @@ const users = await pool.findMany(User, {
133
133
  - `queryErrorKind(err)` names any failure the same on every engine - `uniqueViolation`, `foreignKeyViolation`,
134
134
  `notNullViolation`, `checkViolation`, `optimisticLock`, `retryable`, `usage` - so catch by kind rather than by
135
135
  a driver's code or an `instanceof`.
136
- - `raw()` embeds SQL anywhere a value or field goes; `pool.all(sql, values)` runs a raw `SELECT`.
136
+ - `raw()` embeds SQL anywhere a value or field goes; `pool.all(sql, values)` runs a raw `SELECT`. A field read off `refs(Entity)` carries its type: on its own as a value it fits only a field of that type.
137
137
 
138
138
  ## Connections and transactions
139
139