spacetimedb 2.5.0 → 2.6.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 (180) hide show
  1. package/LICENSE.txt +759 -759
  2. package/README.md +211 -211
  3. package/dist/angular/index.cjs.map +1 -1
  4. package/dist/angular/index.mjs.map +1 -1
  5. package/dist/browser/angular/index.mjs.map +1 -1
  6. package/dist/browser/react/index.mjs +129 -57
  7. package/dist/browser/react/index.mjs.map +1 -1
  8. package/dist/browser/solid/index.mjs +120 -50
  9. package/dist/browser/solid/index.mjs.map +1 -1
  10. package/dist/browser/svelte/index.mjs.map +1 -1
  11. package/dist/browser/vue/index.mjs.map +1 -1
  12. package/dist/index.browser.mjs +10 -2
  13. package/dist/index.browser.mjs.map +1 -1
  14. package/dist/index.cjs +10 -2
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.mjs +10 -2
  17. package/dist/index.mjs.map +1 -1
  18. package/dist/min/index.browser.mjs +1 -1
  19. package/dist/min/index.browser.mjs.map +1 -1
  20. package/dist/min/react/index.mjs +1 -1
  21. package/dist/min/react/index.mjs.map +1 -1
  22. package/dist/min/sdk/index.browser.mjs +1 -1
  23. package/dist/min/sdk/index.browser.mjs.map +1 -1
  24. package/dist/react/index.cjs +129 -57
  25. package/dist/react/index.cjs.map +1 -1
  26. package/dist/react/index.mjs +129 -57
  27. package/dist/react/index.mjs.map +1 -1
  28. package/dist/react/useTable.d.ts.map +1 -1
  29. package/dist/sdk/connection_manager.d.ts +8 -0
  30. package/dist/sdk/connection_manager.d.ts.map +1 -1
  31. package/dist/sdk/db_connection_impl.d.ts +7 -0
  32. package/dist/sdk/db_connection_impl.d.ts.map +1 -1
  33. package/dist/sdk/index.browser.mjs +10 -2
  34. package/dist/sdk/index.browser.mjs.map +1 -1
  35. package/dist/sdk/index.cjs +10 -2
  36. package/dist/sdk/index.cjs.map +1 -1
  37. package/dist/sdk/index.mjs +10 -2
  38. package/dist/sdk/index.mjs.map +1 -1
  39. package/dist/sdk/websocket_test_adapter.d.ts +2 -1
  40. package/dist/sdk/websocket_test_adapter.d.ts.map +1 -1
  41. package/dist/server/index.mjs.map +1 -1
  42. package/dist/server/runtime.d.ts.map +1 -1
  43. package/dist/solid/index.cjs +120 -50
  44. package/dist/solid/index.cjs.map +1 -1
  45. package/dist/solid/index.mjs +120 -50
  46. package/dist/solid/index.mjs.map +1 -1
  47. package/dist/svelte/index.cjs.map +1 -1
  48. package/dist/svelte/index.mjs.map +1 -1
  49. package/dist/tanstack/index.cjs +120 -50
  50. package/dist/tanstack/index.cjs.map +1 -1
  51. package/dist/tanstack/index.mjs +120 -50
  52. package/dist/tanstack/index.mjs.map +1 -1
  53. package/dist/vue/index.cjs.map +1 -1
  54. package/dist/vue/index.mjs.map +1 -1
  55. package/package.json +1 -1
  56. package/src/angular/connection_state.ts +19 -19
  57. package/src/angular/index.ts +3 -3
  58. package/src/angular/injectors/index.ts +4 -4
  59. package/src/angular/injectors/inject-reducer.ts +62 -62
  60. package/src/angular/injectors/inject-spacetimedb-connected.ts +13 -13
  61. package/src/angular/injectors/inject-spacetimedb.ts +10 -10
  62. package/src/angular/injectors/inject-table.ts +234 -234
  63. package/src/angular/providers/index.ts +1 -1
  64. package/src/angular/providers/provide-spacetimedb.ts +96 -96
  65. package/src/index.ts +16 -16
  66. package/src/lib/algebraic_type.ts +819 -819
  67. package/src/lib/algebraic_type_variants.ts +26 -26
  68. package/src/lib/algebraic_value.ts +10 -10
  69. package/src/lib/autogen/types.ts +746 -746
  70. package/src/lib/binary_reader.ts +188 -188
  71. package/src/lib/binary_writer.ts +213 -213
  72. package/src/lib/connection_id.ts +102 -102
  73. package/src/lib/constraints.ts +48 -48
  74. package/src/lib/errors.ts +26 -26
  75. package/src/lib/filter.ts +195 -195
  76. package/src/lib/identity.ts +83 -83
  77. package/src/lib/indexes.ts +251 -251
  78. package/src/lib/option.ts +34 -34
  79. package/src/lib/query.ts +1019 -1019
  80. package/src/lib/reducer_schema.ts +38 -38
  81. package/src/lib/reducers.ts +116 -116
  82. package/src/lib/result.ts +36 -36
  83. package/src/lib/schedule_at.ts +86 -86
  84. package/src/lib/schema.ts +420 -420
  85. package/src/lib/table.ts +548 -548
  86. package/src/lib/table_schema.ts +64 -64
  87. package/src/lib/time_duration.ts +77 -77
  88. package/src/lib/timestamp.ts +148 -148
  89. package/src/lib/type_builders.test-d.ts +128 -128
  90. package/src/lib/type_builders.ts +4014 -4014
  91. package/src/lib/type_util.ts +124 -124
  92. package/src/lib/util.ts +196 -196
  93. package/src/lib/uuid.ts +337 -337
  94. package/src/react/SpacetimeDBProvider.ts +84 -84
  95. package/src/react/connection_state.ts +6 -6
  96. package/src/react/index.ts +5 -5
  97. package/src/react/useProcedure.ts +60 -60
  98. package/src/react/useReducer.ts +53 -53
  99. package/src/react/useSpacetimeDB.ts +18 -18
  100. package/src/react/useTable.ts +256 -251
  101. package/src/sdk/client_api/index.ts +114 -114
  102. package/src/sdk/client_api/types/procedures.ts +8 -8
  103. package/src/sdk/client_api/types/reducers.ts +8 -8
  104. package/src/sdk/client_api/types.ts +288 -288
  105. package/src/sdk/client_cache.ts +129 -129
  106. package/src/sdk/client_table.ts +179 -179
  107. package/src/sdk/connection_manager.ts +352 -237
  108. package/src/sdk/db_connection_builder.ts +290 -290
  109. package/src/sdk/db_connection_impl.ts +1356 -1347
  110. package/src/sdk/db_context.ts +28 -28
  111. package/src/sdk/db_view.ts +12 -12
  112. package/src/sdk/decompress.ts +51 -51
  113. package/src/sdk/event.ts +18 -18
  114. package/src/sdk/event_context.ts +51 -51
  115. package/src/sdk/event_emitter.ts +32 -32
  116. package/src/sdk/index.ts +14 -14
  117. package/src/sdk/internal.ts +2 -2
  118. package/src/sdk/json_api.ts +46 -46
  119. package/src/sdk/logger.ts +134 -134
  120. package/src/sdk/message_types.ts +46 -46
  121. package/src/sdk/procedures.ts +83 -83
  122. package/src/sdk/reducer_event.ts +20 -20
  123. package/src/sdk/reducer_handle.ts +12 -12
  124. package/src/sdk/reducers.ts +159 -159
  125. package/src/sdk/schema.ts +45 -45
  126. package/src/sdk/spacetime_module.ts +28 -28
  127. package/src/sdk/subscription_builder_impl.ts +275 -275
  128. package/src/sdk/table_cache.ts +581 -581
  129. package/src/sdk/type_utils.ts +19 -19
  130. package/src/sdk/version.ts +133 -133
  131. package/src/sdk/websocket_decompress_adapter.ts +63 -63
  132. package/src/sdk/websocket_protocols.ts +25 -25
  133. package/src/sdk/websocket_test_adapter.ts +107 -100
  134. package/src/sdk/websocket_v3_frames.ts +126 -126
  135. package/src/sdk/ws.ts +105 -105
  136. package/src/server/console.ts +81 -81
  137. package/src/server/db_view.ts +21 -21
  138. package/src/server/errors.ts +138 -138
  139. package/src/server/http.test-d.ts +80 -80
  140. package/src/server/http.ts +14 -14
  141. package/src/server/http_handlers.ts +413 -413
  142. package/src/server/http_internal.ts +79 -79
  143. package/src/server/http_shared.ts +186 -186
  144. package/src/server/index.ts +37 -37
  145. package/src/server/polyfills.ts +4 -4
  146. package/src/server/procedures.ts +239 -239
  147. package/src/server/query.ts +1 -1
  148. package/src/server/range.ts +53 -53
  149. package/src/server/reducers.ts +113 -113
  150. package/src/server/rng.ts +113 -113
  151. package/src/server/runtime.ts +1102 -1102
  152. package/src/server/schema.test-d.ts +99 -99
  153. package/src/server/schema.ts +663 -663
  154. package/src/server/sys.d.ts +125 -125
  155. package/src/server/view.test-d.ts +194 -194
  156. package/src/server/views.ts +340 -340
  157. package/src/solid/SpacetimeDBProvider.ts +97 -97
  158. package/src/solid/connection_state.ts +6 -6
  159. package/src/solid/index.ts +5 -5
  160. package/src/solid/useProcedure.ts +57 -57
  161. package/src/solid/useReducer.ts +50 -50
  162. package/src/solid/useSpacetimeDB.ts +18 -18
  163. package/src/solid/useTable.ts +203 -203
  164. package/src/svelte/SpacetimeDBProvider.ts +101 -101
  165. package/src/svelte/connection_state.ts +16 -16
  166. package/src/svelte/index.ts +4 -4
  167. package/src/svelte/useReducer.ts +61 -61
  168. package/src/svelte/useSpacetimeDB.ts +22 -22
  169. package/src/svelte/useTable.ts +218 -218
  170. package/src/tanstack/SpacetimeDBQueryClient.ts +330 -330
  171. package/src/tanstack/hooks.ts +83 -83
  172. package/src/tanstack/index.ts +16 -16
  173. package/src/util-stub.ts +1 -1
  174. package/src/vue/SpacetimeDBProvider.ts +157 -157
  175. package/src/vue/connection_state.ts +19 -19
  176. package/src/vue/index.ts +5 -5
  177. package/src/vue/useProcedure.ts +62 -62
  178. package/src/vue/useReducer.ts +55 -55
  179. package/src/vue/useSpacetimeDB.ts +18 -18
  180. package/src/vue/useTable.ts +229 -229
package/src/lib/query.ts CHANGED
@@ -1,1019 +1,1019 @@
1
- import { ConnectionId } from './connection_id';
2
- import { Identity } from './identity';
3
- import type { ColumnIndex, IndexColumns, IndexOpts } from './indexes';
4
- import type { UntypedSchemaDef } from './schema';
5
- import type { UntypedTableSchema } from './table_schema';
6
- import { Timestamp } from './timestamp';
7
- import type {
8
- ColumnBuilder,
9
- ColumnMetadata,
10
- RowBuilder,
11
- TypeBuilder,
12
- } from './type_builders';
13
- import type { Values } from './type_util';
14
- import type { Bool as SatsBool } from './algebraic_type_variants';
15
-
16
- /**
17
- * Helper to get the set of table names.
18
- */
19
- export type TableNames<SchemaDef extends UntypedSchemaDef> = Values<
20
- SchemaDef['tables']
21
- >['accessorName'] &
22
- string;
23
-
24
- /** helper: pick the table def object from the schema by its name */
25
- export type TableDefByName<
26
- SchemaDef extends UntypedSchemaDef,
27
- Name extends TableNames<SchemaDef>,
28
- > = Extract<Values<SchemaDef['tables']>, { accessorName: Name }>;
29
-
30
- // internal only — NOT exported.
31
- // This is how we make sure queries are only created with our helpers.
32
- const QueryBrand = Symbol('QueryBrand');
33
-
34
- export interface TableTypedQuery<TableDef extends TypedTableDef> {
35
- readonly [QueryBrand]: true;
36
- readonly __table?: TableDef;
37
- }
38
-
39
- export interface RowTypedQuery<Row, ST> {
40
- readonly [QueryBrand]: true;
41
- // Phantom type to track the row type.
42
- readonly __row?: Row;
43
- readonly __algebraicType?: ST;
44
- }
45
-
46
- export type Query<TableDef extends TypedTableDef> = RowTypedQuery<
47
- RowType<TableDef>,
48
- TableDef['rowType']
49
- >;
50
-
51
- export const isRowTypedQuery = (val: unknown): val is RowTypedQuery<any, any> =>
52
- !!val && typeof val === 'object' && QueryBrand in (val as object);
53
-
54
- export const isTypedQuery = (val: unknown): val is TableTypedQuery<any> =>
55
- !!val && typeof val === 'object' && QueryBrand in (val as object);
56
-
57
- export function toSql(q: Query<any>): string {
58
- return (q as unknown as { toSql(): string }).toSql();
59
- }
60
-
61
- // A query builder with a single table.
62
- type From<TableDef extends TypedTableDef> = RowTypedQuery<
63
- RowType<TableDef>,
64
- TableDef['rowType']
65
- > &
66
- Readonly<{
67
- toSql(): string;
68
- where(
69
- predicate: (row: RowExpr<TableDef>) => PredicateExpr<TableDef>
70
- ): From<TableDef>;
71
- rightSemijoin<RightTable extends TypedTableDef>(
72
- other: TableRef<RightTable>,
73
- on: (
74
- left: IndexedRowExpr<TableDef>,
75
- right: IndexedRowExpr<RightTable>
76
- ) => BooleanExpr<TableDef | RightTable>
77
- ): SemijoinBuilder<RightTable>;
78
- leftSemijoin<RightTable extends TypedTableDef>(
79
- other: TableRef<RightTable>,
80
- on: (
81
- left: IndexedRowExpr<TableDef>,
82
- right: IndexedRowExpr<RightTable>
83
- ) => BooleanExpr<TableDef | RightTable>
84
- ): SemijoinBuilder<TableDef>;
85
- /** @deprecated No longer needed — builder is already a valid query. */
86
- build(): Query<TableDef>;
87
- }>;
88
-
89
- // A query builder with a semijoin.
90
- type SemijoinBuilder<TableDef extends TypedTableDef> = RowTypedQuery<
91
- RowType<TableDef>,
92
- TableDef['rowType']
93
- > &
94
- Readonly<{
95
- toSql(): string;
96
- where(
97
- predicate: (row: RowExpr<TableDef>) => PredicateExpr<TableDef>
98
- ): SemijoinBuilder<TableDef>;
99
- /** @deprecated No longer needed — builder is already a valid query. */
100
- build(): Query<TableDef>;
101
- }>;
102
-
103
- class SemijoinImpl<TableDef extends TypedTableDef>
104
- implements SemijoinBuilder<TableDef>, TableTypedQuery<TableDef>
105
- {
106
- readonly [QueryBrand] = true;
107
- readonly type = 'semijoin' as const;
108
- constructor(
109
- readonly sourceQuery: FromBuilder<TableDef>,
110
- readonly filterQuery: FromBuilder<any>,
111
- readonly joinCondition: BooleanExpr<any>
112
- ) {
113
- if (sourceQuery.table.sourceName === filterQuery.table.sourceName) {
114
- // TODO: Handle aliasing properly instead of just forbidding it.
115
- throw new Error('Cannot semijoin a table to itself');
116
- }
117
- }
118
-
119
- build(): Query<TableDef> {
120
- return this as Query<TableDef>;
121
- }
122
-
123
- where(
124
- predicate: (row: RowExpr<TableDef>) => PredicateExpr<TableDef>
125
- ): SemijoinImpl<TableDef> {
126
- const nextSourceQuery = this.sourceQuery.where(predicate);
127
- return new SemijoinImpl<TableDef>(
128
- nextSourceQuery,
129
- this.filterQuery,
130
- this.joinCondition
131
- );
132
- }
133
-
134
- toSql(): string {
135
- const left = this.filterQuery;
136
- const right = this.sourceQuery;
137
- const leftTable = quoteIdentifier(left.table.sourceName);
138
- const rightTable = quoteIdentifier(right.table.sourceName);
139
- let sql = `SELECT ${rightTable}.* FROM ${leftTable} JOIN ${rightTable} ON ${booleanExprToSql(this.joinCondition)}`;
140
-
141
- const clauses: string[] = [];
142
- if (left.whereClause) {
143
- clauses.push(booleanExprToSql(left.whereClause));
144
- }
145
- if (right.whereClause) {
146
- clauses.push(booleanExprToSql(right.whereClause));
147
- }
148
-
149
- if (clauses.length > 0) {
150
- const whereSql =
151
- clauses.length === 1
152
- ? clauses[0]
153
- : clauses.map(wrapInParens).join(' AND ');
154
- sql += ` WHERE ${whereSql}`;
155
- }
156
-
157
- return sql;
158
- }
159
- }
160
-
161
- class FromBuilder<TableDef extends TypedTableDef>
162
- implements From<TableDef>, TableTypedQuery<TableDef>
163
- {
164
- readonly [QueryBrand] = true;
165
- constructor(
166
- readonly table: TableRef<TableDef>,
167
- readonly whereClause?: BooleanExpr<TableDef>
168
- ) {}
169
-
170
- where(
171
- predicate: (row: RowExpr<TableDef>) => PredicateExpr<TableDef>
172
- ): FromBuilder<TableDef> {
173
- const newCondition = normalizePredicateExpr(predicate(this.table.cols));
174
- const nextWhere = this.whereClause
175
- ? this.whereClause.and(newCondition)
176
- : newCondition;
177
- return new FromBuilder<TableDef>(this.table, nextWhere);
178
- }
179
-
180
- rightSemijoin<OtherTable extends TypedTableDef>(
181
- right: TableRef<OtherTable>,
182
- on: (
183
- left: IndexedRowExpr<TableDef>,
184
- right: IndexedRowExpr<OtherTable>
185
- ) => BooleanExpr<TableDef | OtherTable>
186
- ): SemijoinBuilder<OtherTable> {
187
- const sourceQuery = new FromBuilder(right);
188
- const joinCondition = on(
189
- this.table.indexedCols,
190
- right.indexedCols
191
- ) as BooleanExpr<any>;
192
- return new SemijoinImpl<OtherTable>(sourceQuery, this, joinCondition);
193
- }
194
-
195
- leftSemijoin<OtherTable extends TypedTableDef>(
196
- right: TableRef<OtherTable>,
197
- on: (
198
- left: IndexedRowExpr<TableDef>,
199
- right: IndexedRowExpr<OtherTable>
200
- ) => BooleanExpr<TableDef | OtherTable>
201
- ): SemijoinBuilder<TableDef> {
202
- const filterQuery = new FromBuilder(right);
203
- const joinCondition = on(
204
- this.table.indexedCols,
205
- right.indexedCols
206
- ) as BooleanExpr<any>;
207
- return new SemijoinImpl<TableDef>(this, filterQuery, joinCondition);
208
- }
209
-
210
- toSql(): string {
211
- return renderSelectSqlWithJoins(this.table, this.whereClause);
212
- }
213
-
214
- build(): Query<TableDef> {
215
- return this as Query<TableDef>;
216
- }
217
- }
218
-
219
- export type QueryBuilder<SchemaDef extends UntypedSchemaDef> = {
220
- readonly [Tbl in Values<
221
- SchemaDef['tables']
222
- > as Tbl['accessorName']]: TableRef<Tbl> & From<Tbl>;
223
- } & {};
224
-
225
- /**
226
- * A runtime reference to a table. This materializes the RowExpr for us.
227
- * TODO: Maybe add the full SchemaDef to the type signature depending on how joins will work.
228
- */
229
- export type TableRef<TableDef extends TypedTableDef> = Readonly<{
230
- type: 'table';
231
- sourceName: TableDef['sourceName'];
232
- accessorName: string;
233
- cols: RowExpr<TableDef>;
234
- indexedCols: IndexedRowExpr<TableDef>;
235
- tableDef: TableDef;
236
- // Delegated UntypedTableDef properties for compatibility.
237
- columns: TableDef['columns'];
238
- indexes: TableDef['indexes'];
239
- rowType: TableDef['rowType'];
240
- constraints: any;
241
- }>;
242
-
243
- class TableRefImpl<TableDef extends TypedTableDef>
244
- implements TableRef<TableDef>, From<TableDef>
245
- {
246
- readonly [QueryBrand] = true;
247
- readonly type = 'table' as const;
248
- sourceName: string;
249
- accessorName: string;
250
- cols: RowExpr<TableDef>;
251
- indexedCols: IndexedRowExpr<TableDef>;
252
- tableDef: TableDef;
253
- // Delegate UntypedTableDef properties from tableDef so this can be used as a table def.
254
- get columns() {
255
- return this.tableDef.columns;
256
- }
257
- get indexes() {
258
- return this.tableDef.indexes;
259
- }
260
- get rowType() {
261
- return this.tableDef.rowType;
262
- }
263
- get constraints() {
264
- return (this.tableDef as any).constraints;
265
- }
266
- constructor(tableDef: TableDef) {
267
- this.sourceName = tableDef.sourceName;
268
- this.accessorName = tableDef.accessorName;
269
- this.cols = createRowExpr(tableDef);
270
- // this.indexedCols = createIndexedRowExpr(tableDef, this.cols);
271
- // TODO: we could create an indexedRowExpr to avoid having the extra columns.
272
- // Right now, the objects we pass will actually have all the columns, but the
273
- // type system will consider it an error.
274
- this.indexedCols = this.cols;
275
- this.tableDef = tableDef;
276
- Object.freeze(this);
277
- }
278
-
279
- asFrom(): FromBuilder<TableDef> {
280
- return new FromBuilder<TableDef>(this);
281
- }
282
-
283
- rightSemijoin<RightTable extends TypedTableDef>(
284
- other: TableRef<RightTable>,
285
- on: (
286
- left: IndexedRowExpr<TableDef>,
287
- right: IndexedRowExpr<RightTable>
288
- ) => EqExpr<TableDef | RightTable>
289
- ): SemijoinBuilder<RightTable> {
290
- return this.asFrom().rightSemijoin(other, on);
291
- }
292
-
293
- leftSemijoin<RightTable extends TypedTableDef>(
294
- other: TableRef<RightTable>,
295
- on: (
296
- left: IndexedRowExpr<TableDef>,
297
- right: IndexedRowExpr<RightTable>
298
- ) => EqExpr<TableDef | RightTable>
299
- ): SemijoinBuilder<TableDef> {
300
- return this.asFrom().leftSemijoin(other, on);
301
- }
302
-
303
- build(): Query<TableDef> {
304
- return this.asFrom().build();
305
- }
306
-
307
- toSql(): string {
308
- return this.asFrom().toSql();
309
- }
310
-
311
- where(
312
- predicate: (row: RowExpr<TableDef>) => PredicateExpr<TableDef>
313
- ): FromBuilder<TableDef> {
314
- return this.asFrom().where(predicate);
315
- }
316
- }
317
-
318
- export function createTableRefFromDef<TableDef extends TypedTableDef>(
319
- tableDef: TableDef
320
- ): TableRef<TableDef> {
321
- return new TableRefImpl<TableDef>(tableDef);
322
- }
323
-
324
- export function makeQueryBuilder<SchemaDef extends UntypedSchemaDef>(
325
- schema: SchemaDef
326
- ): QueryBuilder<SchemaDef> {
327
- const qb = Object.create(null) as QueryBuilder<SchemaDef>;
328
- for (const table of Object.values(schema.tables)) {
329
- const ref = createTableRefFromDef(
330
- table as TableDefByName<SchemaDef, TableNames<SchemaDef>>
331
- );
332
- (qb as Record<string, TableRef<any>>)[table.accessorName] = ref;
333
- }
334
- return Object.freeze(qb) as QueryBuilder<SchemaDef>;
335
- }
336
-
337
- function createRowExpr<TableDef extends TypedTableDef>(
338
- tableDef: TableDef
339
- ): RowExpr<TableDef> {
340
- const row: Record<string, ColumnExpr<TableDef, any>> = {};
341
- for (const columnName of Object.keys(tableDef.columns) as Array<
342
- keyof TableDef['columns'] & string
343
- >) {
344
- const columnBuilder = tableDef.columns[columnName];
345
- const column = new ColumnExpression<TableDef, typeof columnName>(
346
- tableDef.sourceName,
347
- columnName,
348
- columnBuilder.typeBuilder.algebraicType as InferSpacetimeTypeOfColumn<
349
- TableDef,
350
- typeof columnName
351
- >,
352
- columnBuilder.columnMetadata.name
353
- );
354
- row[columnName] = Object.freeze(column);
355
- }
356
- return Object.freeze(row) as RowExpr<TableDef>;
357
- }
358
-
359
- function renderSelectSqlWithJoins<Table extends TypedTableDef>(
360
- table: TableRef<Table>,
361
- where?: BooleanExpr<Table>,
362
- extraClauses: readonly string[] = []
363
- ): string {
364
- const quotedTable = quoteIdentifier(table.sourceName);
365
- const sql = `SELECT * FROM ${quotedTable}`;
366
- const clauses: string[] = [];
367
- if (where) clauses.push(booleanExprToSql(where));
368
- clauses.push(...extraClauses);
369
- if (clauses.length === 0) return sql;
370
- const whereSql =
371
- clauses.length === 1 ? clauses[0] : clauses.map(wrapInParens).join(' AND ');
372
- return `${sql} WHERE ${whereSql}`;
373
- }
374
-
375
- // TODO: Just use UntypedTableDef if they end up being the same.
376
- export type TypedTableDef<
377
- Columns extends Record<
378
- string,
379
- ColumnBuilder<any, any, ColumnMetadata<any>>
380
- > = Record<string, ColumnBuilder<any, any, ColumnMetadata<any>>>,
381
- > = {
382
- sourceName: string;
383
- accessorName: string;
384
- columns: Columns;
385
- indexes: readonly IndexOpts<any>[];
386
- rowType: RowBuilder<Columns>['algebraicType']['value'];
387
- };
388
-
389
- export type TableSchemaAsTableDef<TSchema extends UntypedTableSchema> = {
390
- name: TSchema['tableName'];
391
- columns: TSchema['rowType']['row'];
392
- indexes: TSchema['idxs'];
393
- };
394
-
395
- type RowType<TableDef extends TypedTableDef> = {
396
- [K in keyof TableDef['columns']]: TableDef['columns'][K] extends ColumnBuilder<
397
- infer T,
398
- any,
399
- any
400
- >
401
- ? T
402
- : never;
403
- };
404
-
405
- // TODO: Consider making a smaller version of these types that doesn't expose the internals.
406
- // Restricting it later should not break anyone in practice.
407
- export type ColumnExpr<
408
- TableDef extends TypedTableDef,
409
- ColumnName extends ColumnNames<TableDef>,
410
- > = ColumnExpression<TableDef, ColumnName>;
411
-
412
- type ColumnSpacetimeType<Col extends ColumnExpr<any, any>> =
413
- Col extends ColumnExpr<infer T, infer N>
414
- ? InferSpacetimeTypeOfColumn<T, N>
415
- : never;
416
-
417
- // TODO: This checks that they match, but we also need to make sure that they are comparable types,
418
- // since you can use product types at all.
419
- type ColumnSameSpacetime<
420
- ThisTable extends TypedTableDef,
421
- ThisCol extends ColumnNames<ThisTable>,
422
- OtherCol extends ColumnExpr<any, any>,
423
- > = [InferSpacetimeTypeOfColumn<ThisTable, ThisCol>] extends [
424
- ColumnSpacetimeType<OtherCol>,
425
- ]
426
- ? [ColumnSpacetimeType<OtherCol>] extends [
427
- InferSpacetimeTypeOfColumn<ThisTable, ThisCol>,
428
- ]
429
- ? OtherCol
430
- : never
431
- : never;
432
-
433
- // Helper to get the table back from a column.
434
- type ExtractTable<Col extends ColumnExpr<any, any>> =
435
- Col extends ColumnExpr<infer T, any> ? T : never;
436
-
437
- export class ColumnExpression<
438
- TableDef extends TypedTableDef,
439
- ColumnName extends ColumnNames<TableDef>,
440
- > {
441
- readonly type = 'column' as const;
442
- // This is the column accessor
443
- readonly column: ColumnName;
444
- // The name of the column in the database.
445
- readonly columnName: string;
446
- readonly table: TableDef['sourceName'];
447
- // phantom: actual runtime value is undefined
448
- readonly tsValueType?: RowType<TableDef>[ColumnName];
449
- readonly spacetimeType: InferSpacetimeTypeOfColumn<TableDef, ColumnName>;
450
-
451
- constructor(
452
- table: TableDef['sourceName'],
453
- column: ColumnName,
454
- spacetimeType: InferSpacetimeTypeOfColumn<TableDef, ColumnName>,
455
- columnName?: string
456
- ) {
457
- this.table = table;
458
- this.column = column;
459
- this.columnName = columnName || column;
460
- this.spacetimeType = spacetimeType;
461
- }
462
-
463
- eq(
464
- literal: LiteralValue & RowType<TableDef>[ColumnName]
465
- ): BooleanExpr<TableDef>;
466
- eq<OtherCol extends ColumnExpr<any, any>>(
467
- value: ColumnSameSpacetime<TableDef, ColumnName, OtherCol>
468
- ): BooleanExpr<TableDef | ExtractTable<OtherCol>>;
469
-
470
- eq(x: any): any {
471
- return new BooleanExpr({
472
- type: 'eq',
473
- left: this as unknown as ValueExpr<TableDef, any>,
474
- right: normalizeValue(x) as ValueExpr<TableDef, any>,
475
- });
476
- }
477
-
478
- ne(
479
- literal: LiteralValue & RowType<TableDef>[ColumnName]
480
- ): BooleanExpr<TableDef>;
481
- ne<OtherCol extends ColumnExpr<any, any>>(
482
- value: ColumnSameSpacetime<TableDef, ColumnName, OtherCol>
483
- ): BooleanExpr<TableDef | ExtractTable<OtherCol>>;
484
-
485
- ne(x: any): any {
486
- return new BooleanExpr({
487
- type: 'ne',
488
- left: this as unknown as ValueExpr<TableDef, any>,
489
- right: normalizeValue(x) as ValueExpr<TableDef, any>,
490
- });
491
- }
492
-
493
- lt(
494
- literal: LiteralValue & RowType<TableDef>[ColumnName]
495
- ): BooleanExpr<TableDef>;
496
- lt<OtherCol extends ColumnExpr<any, any>>(
497
- value: ColumnSameSpacetime<TableDef, ColumnName, OtherCol>
498
- ): BooleanExpr<TableDef | ExtractTable<OtherCol>>;
499
-
500
- lt(x: any): any {
501
- return new BooleanExpr({
502
- type: 'lt',
503
- left: this as unknown as ValueExpr<TableDef, any>,
504
- right: normalizeValue(x) as ValueExpr<TableDef, any>,
505
- });
506
- }
507
-
508
- lte(
509
- literal: LiteralValue & RowType<TableDef>[ColumnName]
510
- ): BooleanExpr<TableDef>;
511
- lte<OtherCol extends ColumnExpr<any, any>>(
512
- value: ColumnSameSpacetime<TableDef, ColumnName, OtherCol>
513
- ): BooleanExpr<TableDef | ExtractTable<OtherCol>>;
514
-
515
- lte(x: any): any {
516
- return new BooleanExpr({
517
- type: 'lte',
518
- left: this as unknown as ValueExpr<TableDef, any>,
519
- right: normalizeValue(x) as ValueExpr<TableDef, any>,
520
- });
521
- }
522
-
523
- gt(
524
- literal: LiteralValue & RowType<TableDef>[ColumnName]
525
- ): BooleanExpr<TableDef>;
526
- gt<OtherCol extends ColumnExpr<any, any>>(
527
- value: ColumnSameSpacetime<TableDef, ColumnName, OtherCol>
528
- ): BooleanExpr<TableDef | ExtractTable<OtherCol>>;
529
-
530
- gt(x: any): any {
531
- return new BooleanExpr({
532
- type: 'gt',
533
- left: this as unknown as ValueExpr<TableDef, any>,
534
- right: normalizeValue(x) as ValueExpr<TableDef, any>,
535
- });
536
- }
537
-
538
- gte(
539
- literal: LiteralValue & RowType<TableDef>[ColumnName]
540
- ): BooleanExpr<TableDef>;
541
- gte<OtherCol extends ColumnExpr<any, any>>(
542
- value: ColumnSameSpacetime<TableDef, ColumnName, OtherCol>
543
- ): BooleanExpr<TableDef | ExtractTable<OtherCol>>;
544
-
545
- gte(x: any): any {
546
- return new BooleanExpr({
547
- type: 'gte',
548
- left: this as unknown as ValueExpr<TableDef, any>,
549
- right: normalizeValue(x) as ValueExpr<TableDef, any>,
550
- });
551
- }
552
- }
553
-
554
- /**
555
- * Helper to get the spacetime type of a column.
556
- */
557
- type InferSpacetimeTypeOfColumn<
558
- TableDef extends TypedTableDef,
559
- ColumnName extends ColumnNames<TableDef>,
560
- > =
561
- TableDef['columns'][ColumnName]['typeBuilder'] extends TypeBuilder<
562
- any,
563
- infer U
564
- >
565
- ? U
566
- : never;
567
-
568
- type ColumnNames<TableDef extends TypedTableDef> = keyof RowType<TableDef> &
569
- string;
570
-
571
- // For composite indexes, we only consider it as an index over the first column in the index.
572
- type FirstIndexColumn<I extends IndexOpts<any>> =
573
- IndexColumns<I> extends readonly [infer Head extends string, ...infer _Rest]
574
- ? Head
575
- : never;
576
-
577
- // Columns that are indexed by something in the indexes: [...] part.
578
- type ExplicitIndexedColumns<TableDef extends TypedTableDef> =
579
- TableDef['indexes'][number] extends infer I
580
- ? I extends IndexOpts<ColumnNames<TableDef>>
581
- ? FirstIndexColumn<I> & ColumnNames<TableDef>
582
- : never
583
- : never;
584
-
585
- // Columns with an index defined on the column definition.
586
- type MetadataIndexedColumns<TableDef extends TypedTableDef> = {
587
- [K in ColumnNames<TableDef>]: ColumnIndex<
588
- K,
589
- TableDef['columns'][K]['columnMetadata']
590
- > extends never
591
- ? never
592
- : K;
593
- }[ColumnNames<TableDef>];
594
-
595
- export type IndexedColumnNames<TableDef extends TypedTableDef> =
596
- | ExplicitIndexedColumns<TableDef>
597
- | MetadataIndexedColumns<TableDef>;
598
-
599
- export type IndexedRowExpr<TableDef extends TypedTableDef> = Readonly<{
600
- readonly [C in IndexedColumnNames<TableDef>]: ColumnExpr<TableDef, C>;
601
- }>;
602
-
603
- /**
604
- * Acts as a row when writing filters for queries. It is a way to get column references.
605
- */
606
- export type RowExpr<TableDef extends TypedTableDef> = Readonly<{
607
- readonly [C in ColumnNames<TableDef>]: ColumnExpr<TableDef, C>;
608
- }>;
609
-
610
- /**
611
- * Union of ColumnExprs from Table whose spacetimeType is compatible with Value
612
- * (produces a union of ColumnExpr<Table, C> for matching columns).
613
- */
614
- export type ColumnExprForValue<Table extends TypedTableDef, Value> = {
615
- [C in ColumnNames<Table>]: InferSpacetimeTypeOfColumn<Table, C> extends Value
616
- ? ColumnExpr<Table, C>
617
- : never;
618
- }[ColumnNames<Table>];
619
-
620
- type LiteralValue =
621
- | string
622
- | number
623
- | bigint
624
- | boolean
625
- | Identity
626
- | Timestamp
627
- | ConnectionId;
628
-
629
- type ValueLike = LiteralValue | ColumnExpr<any, any> | LiteralExpr<any>;
630
- type ValueInput<TableDef extends TypedTableDef> =
631
- | ValueLike
632
- | ValueExpr<TableDef, any>;
633
-
634
- export type ValueExpr<TableDef extends TypedTableDef, Value> =
635
- | LiteralExpr<Value & LiteralValue>
636
- | ColumnExprForValue<TableDef, Value>;
637
-
638
- type PredicateExpr<TableDef extends TypedTableDef> =
639
- | BooleanExpr<TableDef>
640
- | ColumnExprForValue<TableDef, SatsBool>
641
- | boolean;
642
-
643
- type LiteralExpr<Value> = {
644
- type: 'literal';
645
- value: Value;
646
- };
647
-
648
- export function literal<Value extends LiteralValue>(
649
- value: Value
650
- ): ValueExpr<never, Value> {
651
- return { type: 'literal', value };
652
- }
653
-
654
- // This is here to take literal values and wrap them in an AST node.
655
- function normalizeValue(val: ValueInput<any>): ValueExpr<any, any> {
656
- if ((val as LiteralExpr<any>).type === 'literal')
657
- return val as LiteralExpr<any>;
658
- if (
659
- typeof val === 'object' &&
660
- val != null &&
661
- 'type' in (val as any) &&
662
- (val as any).type === 'column'
663
- ) {
664
- return val as ColumnExpr<any, any>;
665
- }
666
- return literal(val as LiteralValue);
667
- }
668
-
669
- function normalizePredicateExpr<TableDef extends TypedTableDef>(
670
- value: PredicateExpr<TableDef>
671
- ): BooleanExpr<TableDef> {
672
- if (value instanceof BooleanExpr) return value;
673
- if (typeof value === 'boolean') {
674
- return new BooleanExpr({
675
- type: 'eq',
676
- left: literal(value),
677
- right: literal(true),
678
- });
679
- }
680
- return new BooleanExpr({
681
- type: 'eq',
682
- left: value as ValueExpr<TableDef, any>,
683
- right: literal(true),
684
- });
685
- }
686
-
687
- type EqExpr<Table extends TypedTableDef = any> = BooleanExpr<Table>;
688
-
689
- type BooleanExprData<Table extends TypedTableDef> = (
690
- | {
691
- type: 'eq' | 'ne' | 'gt' | 'lt' | 'gte' | 'lte';
692
- left: ValueExpr<Table, any>;
693
- right: ValueExpr<Table, any>;
694
- }
695
- | {
696
- type: 'and';
697
- clauses: readonly [
698
- BooleanExprData<Table>,
699
- BooleanExprData<Table>,
700
- ...BooleanExprData<Table>[],
701
- ];
702
- }
703
- | {
704
- type: 'or';
705
- clauses: readonly [
706
- BooleanExprData<Table>,
707
- BooleanExprData<Table>,
708
- ...BooleanExprData<Table>[],
709
- ];
710
- }
711
- | {
712
- type: 'not';
713
- clause: BooleanExprData<Table>;
714
- }
715
- ) & {
716
- _tableType?: Table;
717
- };
718
-
719
- type AndOrMixedTableScopeError = {
720
- readonly 'Cannot combine predicates from different table scopes with and/or. In semijoin on(...), keep only the join equality and move extra predicates to .where(...).': never;
721
- };
722
-
723
- type RequireSameAndOrTable<
724
- Expected extends TypedTableDef,
725
- Actual extends TypedTableDef,
726
- > = [Expected] extends [Actual]
727
- ? [Actual] extends [Expected]
728
- ? unknown
729
- : AndOrMixedTableScopeError
730
- : AndOrMixedTableScopeError;
731
-
732
- export class BooleanExpr<Table extends TypedTableDef> {
733
- constructor(readonly data: BooleanExprData<Table>) {}
734
-
735
- and<OtherTable extends TypedTableDef>(
736
- other: BooleanExpr<OtherTable> & RequireSameAndOrTable<Table, OtherTable>
737
- ): BooleanExpr<Table> {
738
- return new BooleanExpr({
739
- type: 'and',
740
- clauses: [this.data, other.data as BooleanExprData<Table>],
741
- });
742
- }
743
-
744
- or<OtherTable extends TypedTableDef>(
745
- other: BooleanExpr<OtherTable> & RequireSameAndOrTable<Table, OtherTable>
746
- ): BooleanExpr<Table> {
747
- return new BooleanExpr({
748
- type: 'or',
749
- clauses: [this.data, other.data as BooleanExprData<Table>],
750
- });
751
- }
752
-
753
- not(): BooleanExpr<Table> {
754
- return new BooleanExpr({ type: 'not', clause: this.data });
755
- }
756
- }
757
-
758
- export function not<T extends TypedTableDef>(
759
- clause: BooleanExpr<T>
760
- ): BooleanExpr<T> {
761
- return new BooleanExpr({ type: 'not', clause: clause.data });
762
- }
763
-
764
- export function and<
765
- Table extends TypedTableDef,
766
- OtherTable extends TypedTableDef,
767
- >(
768
- first: BooleanExpr<Table>,
769
- second: BooleanExpr<OtherTable> & RequireSameAndOrTable<Table, OtherTable>,
770
- ...rest: readonly BooleanExpr<Table>[]
771
- ): BooleanExpr<Table> {
772
- const clauses = [first, second, ...rest];
773
- return new BooleanExpr({
774
- type: 'and',
775
- clauses: clauses.map(c => c.data) as [
776
- BooleanExprData<Table>,
777
- BooleanExprData<Table>,
778
- ...BooleanExprData<Table>[],
779
- ],
780
- });
781
- }
782
-
783
- export function or<
784
- Table extends TypedTableDef,
785
- OtherTable extends TypedTableDef,
786
- >(
787
- first: BooleanExpr<Table>,
788
- second: BooleanExpr<OtherTable> & RequireSameAndOrTable<Table, OtherTable>,
789
- ...rest: readonly BooleanExpr<Table>[]
790
- ): BooleanExpr<Table> {
791
- const clauses = [first, second, ...rest];
792
- return new BooleanExpr({
793
- type: 'or',
794
- clauses: clauses.map(c => c.data) as [
795
- BooleanExprData<Table>,
796
- BooleanExprData<Table>,
797
- ...BooleanExprData<Table>[],
798
- ],
799
- });
800
- }
801
-
802
- function booleanExprToSql<Table extends TypedTableDef>(
803
- expr: BooleanExpr<Table> | BooleanExprData<Table>,
804
- tableAlias?: string
805
- ): string {
806
- const data = expr instanceof BooleanExpr ? expr.data : expr;
807
- switch (data.type) {
808
- case 'eq':
809
- return `${valueExprToSql(data.left, tableAlias)} = ${valueExprToSql(data.right, tableAlias)}`;
810
- case 'ne':
811
- return `${valueExprToSql(data.left, tableAlias)} <> ${valueExprToSql(data.right, tableAlias)}`;
812
- case 'gt':
813
- return `${valueExprToSql(data.left, tableAlias)} > ${valueExprToSql(data.right, tableAlias)}`;
814
- case 'gte':
815
- return `${valueExprToSql(data.left, tableAlias)} >= ${valueExprToSql(data.right, tableAlias)}`;
816
- case 'lt':
817
- return `${valueExprToSql(data.left, tableAlias)} < ${valueExprToSql(data.right, tableAlias)}`;
818
- case 'lte':
819
- return `${valueExprToSql(data.left, tableAlias)} <= ${valueExprToSql(data.right, tableAlias)}`;
820
- case 'and':
821
- return data.clauses
822
- .map(c => booleanExprToSql(c, tableAlias))
823
- .map(wrapInParens)
824
- .join(' AND ');
825
- case 'or':
826
- return data.clauses
827
- .map(c => booleanExprToSql(c, tableAlias))
828
- .map(wrapInParens)
829
- .join(' OR ');
830
- case 'not':
831
- return `NOT ${wrapInParens(booleanExprToSql(data.clause, tableAlias))}`;
832
- }
833
- }
834
-
835
- function wrapInParens(sql: string): string {
836
- return `(${sql})`;
837
- }
838
-
839
- function valueExprToSql<Table extends TypedTableDef>(
840
- expr: ValueExpr<Table, any>,
841
- tableAlias?: string
842
- ): string {
843
- if (isLiteralExpr(expr)) {
844
- return literalValueToSql(expr.value);
845
- }
846
- const table = tableAlias ?? expr.table;
847
- return `${quoteIdentifier(table)}.${quoteIdentifier(expr.columnName)}`;
848
- }
849
-
850
- function literalValueToSql(value: unknown): string {
851
- if (value === null || value === undefined) {
852
- return 'NULL';
853
- }
854
- if (value instanceof Identity || value instanceof ConnectionId) {
855
- // We use this hex string syntax.
856
- return `0x${value.toHexString()}`;
857
- }
858
- if (value instanceof Timestamp) {
859
- return `'${value.toISOString()}'`;
860
- }
861
- switch (typeof value) {
862
- case 'number':
863
- case 'bigint':
864
- return String(value);
865
- case 'boolean':
866
- return value ? 'TRUE' : 'FALSE';
867
- case 'string':
868
- return `'${value.replace(/'/g, "''")}'`;
869
- default:
870
- // It might be safer to error here?
871
- return `'${JSON.stringify(value).replace(/'/g, "''")}'`;
872
- }
873
- }
874
-
875
- function quoteIdentifier(name: string): string {
876
- return `"${name.replace(/"/g, '""')}"`;
877
- }
878
-
879
- function isLiteralExpr<Value>(
880
- expr: ValueExpr<any, Value>
881
- ): expr is LiteralExpr<Value & LiteralValue> {
882
- return (expr as LiteralExpr<Value>).type === 'literal';
883
- }
884
-
885
- /**
886
- * Evaluate a BooleanExpr against a row at runtime for client-side filtering.
887
- */
888
- export function evaluateBooleanExpr(
889
- expr: BooleanExpr<any>,
890
- row: Record<string, any>
891
- ): boolean {
892
- return evaluateData(expr.data, row);
893
- }
894
-
895
- function evaluateData(
896
- data: BooleanExprData<any>,
897
- row: Record<string, any>
898
- ): boolean {
899
- switch (data.type) {
900
- case 'eq':
901
- return resolveValue(data.left, row) === resolveValue(data.right, row);
902
- case 'ne':
903
- return resolveValue(data.left, row) !== resolveValue(data.right, row);
904
- case 'gt':
905
- return resolveValue(data.left, row) > resolveValue(data.right, row);
906
- case 'gte':
907
- return resolveValue(data.left, row) >= resolveValue(data.right, row);
908
- case 'lt':
909
- return resolveValue(data.left, row) < resolveValue(data.right, row);
910
- case 'lte':
911
- return resolveValue(data.left, row) <= resolveValue(data.right, row);
912
- case 'and':
913
- return data.clauses.every(c => evaluateData(c, row));
914
- case 'or':
915
- return data.clauses.some(c => evaluateData(c, row));
916
- case 'not':
917
- return !evaluateData(data.clause, row);
918
- }
919
- }
920
-
921
- function resolveValue(
922
- expr: ValueExpr<any, any>,
923
- row: Record<string, any>
924
- ): any {
925
- if (isLiteralExpr(expr)) {
926
- return toComparableValue(expr.value);
927
- }
928
- return toComparableValue(row[expr.column]);
929
- }
930
-
931
- type TimestampLike = {
932
- __timestamp_micros_since_unix_epoch__: bigint;
933
- };
934
-
935
- type HexSerializableLike = {
936
- toHexString: () => string;
937
- };
938
-
939
- function isHexSerializableLike(value: unknown): value is HexSerializableLike {
940
- return (
941
- !!value &&
942
- typeof value === 'object' &&
943
- typeof (value as { toHexString?: unknown }).toHexString === 'function'
944
- );
945
- }
946
-
947
- // Check if this value is a Timestamp-like object. This is here because
948
- // running locally can end up with different versions of the Timestamp class,
949
- // which breaks the simple instanceof version.
950
- function isTimestampLike(value: unknown): value is TimestampLike {
951
- if (!value || typeof value !== 'object') return false;
952
-
953
- if (value instanceof Timestamp) return true;
954
-
955
- const micros = (value as Record<string, unknown>)[
956
- '__timestamp_micros_since_unix_epoch__'
957
- ];
958
- return typeof micros === 'bigint';
959
- }
960
-
961
- // Exported for tests.
962
- export function toComparableValue(value: any): any {
963
- // Handle `ConnectionId` and `Identity`.
964
- if (isHexSerializableLike(value)) {
965
- return value.toHexString();
966
- }
967
- if (isTimestampLike(value)) {
968
- return value.__timestamp_micros_since_unix_epoch__;
969
- }
970
- return value;
971
- }
972
-
973
- /**
974
- * Extract the table name from a query builder expression.
975
- */
976
- export function getQueryTableName(query: any): string {
977
- if (query.table) return query.table.name; // FromBuilder
978
- if (query.name) return query.name; // TableRefImpl
979
- if (query.sourceQuery) return query.sourceQuery.table.name; // SemijoinImpl (source table)
980
- throw new Error('Cannot extract table name from query');
981
- }
982
-
983
- /**
984
- * Extract the accessor name from a query builder expression.
985
- */
986
- export function getQueryAccessorName(query: any): string {
987
- if (query.table) return query.table.accessorName; // FromBuilder
988
- if (query.accessorName) return query.accessorName; // TableRefImpl
989
- if (query.sourceQuery) return query.sourceQuery.table.accessorName; // SemijoinImpl
990
- throw new Error('Cannot extract accessor name from query');
991
- }
992
-
993
- /**
994
- * Extract the BooleanExpr from a query builder, if any.
995
- */
996
- export function getQueryWhereClause(query: any): BooleanExpr<any> | undefined {
997
- if (query.whereClause) return query.whereClause; // FromBuilder
998
- return undefined; // TableRefImpl has no where clause
999
- }
1000
-
1001
- // TODO: Fix this.
1002
- function _createIndexedRowExpr<TableDef extends TypedTableDef>(
1003
- tableDef: TableDef,
1004
- cols: RowExpr<TableDef>
1005
- ): IndexedRowExpr<TableDef> {
1006
- const indexed = new Set<ColumnNames<TableDef>>();
1007
- for (const idx of tableDef.indexes) {
1008
- if ('columns' in idx) {
1009
- const [first] = idx.columns;
1010
- if (first) indexed.add(first);
1011
- } else if ('column' in idx) {
1012
- indexed.add(idx.column);
1013
- }
1014
- }
1015
- const pickedEntries = [...indexed].map(name => [name, cols[name]]);
1016
- return Object.freeze(
1017
- Object.fromEntries(pickedEntries)
1018
- ) as IndexedRowExpr<TableDef>;
1019
- }
1
+ import { ConnectionId } from './connection_id';
2
+ import { Identity } from './identity';
3
+ import type { ColumnIndex, IndexColumns, IndexOpts } from './indexes';
4
+ import type { UntypedSchemaDef } from './schema';
5
+ import type { UntypedTableSchema } from './table_schema';
6
+ import { Timestamp } from './timestamp';
7
+ import type {
8
+ ColumnBuilder,
9
+ ColumnMetadata,
10
+ RowBuilder,
11
+ TypeBuilder,
12
+ } from './type_builders';
13
+ import type { Values } from './type_util';
14
+ import type { Bool as SatsBool } from './algebraic_type_variants';
15
+
16
+ /**
17
+ * Helper to get the set of table names.
18
+ */
19
+ export type TableNames<SchemaDef extends UntypedSchemaDef> = Values<
20
+ SchemaDef['tables']
21
+ >['accessorName'] &
22
+ string;
23
+
24
+ /** helper: pick the table def object from the schema by its name */
25
+ export type TableDefByName<
26
+ SchemaDef extends UntypedSchemaDef,
27
+ Name extends TableNames<SchemaDef>,
28
+ > = Extract<Values<SchemaDef['tables']>, { accessorName: Name }>;
29
+
30
+ // internal only — NOT exported.
31
+ // This is how we make sure queries are only created with our helpers.
32
+ const QueryBrand = Symbol('QueryBrand');
33
+
34
+ export interface TableTypedQuery<TableDef extends TypedTableDef> {
35
+ readonly [QueryBrand]: true;
36
+ readonly __table?: TableDef;
37
+ }
38
+
39
+ export interface RowTypedQuery<Row, ST> {
40
+ readonly [QueryBrand]: true;
41
+ // Phantom type to track the row type.
42
+ readonly __row?: Row;
43
+ readonly __algebraicType?: ST;
44
+ }
45
+
46
+ export type Query<TableDef extends TypedTableDef> = RowTypedQuery<
47
+ RowType<TableDef>,
48
+ TableDef['rowType']
49
+ >;
50
+
51
+ export const isRowTypedQuery = (val: unknown): val is RowTypedQuery<any, any> =>
52
+ !!val && typeof val === 'object' && QueryBrand in (val as object);
53
+
54
+ export const isTypedQuery = (val: unknown): val is TableTypedQuery<any> =>
55
+ !!val && typeof val === 'object' && QueryBrand in (val as object);
56
+
57
+ export function toSql(q: Query<any>): string {
58
+ return (q as unknown as { toSql(): string }).toSql();
59
+ }
60
+
61
+ // A query builder with a single table.
62
+ type From<TableDef extends TypedTableDef> = RowTypedQuery<
63
+ RowType<TableDef>,
64
+ TableDef['rowType']
65
+ > &
66
+ Readonly<{
67
+ toSql(): string;
68
+ where(
69
+ predicate: (row: RowExpr<TableDef>) => PredicateExpr<TableDef>
70
+ ): From<TableDef>;
71
+ rightSemijoin<RightTable extends TypedTableDef>(
72
+ other: TableRef<RightTable>,
73
+ on: (
74
+ left: IndexedRowExpr<TableDef>,
75
+ right: IndexedRowExpr<RightTable>
76
+ ) => BooleanExpr<TableDef | RightTable>
77
+ ): SemijoinBuilder<RightTable>;
78
+ leftSemijoin<RightTable extends TypedTableDef>(
79
+ other: TableRef<RightTable>,
80
+ on: (
81
+ left: IndexedRowExpr<TableDef>,
82
+ right: IndexedRowExpr<RightTable>
83
+ ) => BooleanExpr<TableDef | RightTable>
84
+ ): SemijoinBuilder<TableDef>;
85
+ /** @deprecated No longer needed — builder is already a valid query. */
86
+ build(): Query<TableDef>;
87
+ }>;
88
+
89
+ // A query builder with a semijoin.
90
+ type SemijoinBuilder<TableDef extends TypedTableDef> = RowTypedQuery<
91
+ RowType<TableDef>,
92
+ TableDef['rowType']
93
+ > &
94
+ Readonly<{
95
+ toSql(): string;
96
+ where(
97
+ predicate: (row: RowExpr<TableDef>) => PredicateExpr<TableDef>
98
+ ): SemijoinBuilder<TableDef>;
99
+ /** @deprecated No longer needed — builder is already a valid query. */
100
+ build(): Query<TableDef>;
101
+ }>;
102
+
103
+ class SemijoinImpl<TableDef extends TypedTableDef>
104
+ implements SemijoinBuilder<TableDef>, TableTypedQuery<TableDef>
105
+ {
106
+ readonly [QueryBrand] = true;
107
+ readonly type = 'semijoin' as const;
108
+ constructor(
109
+ readonly sourceQuery: FromBuilder<TableDef>,
110
+ readonly filterQuery: FromBuilder<any>,
111
+ readonly joinCondition: BooleanExpr<any>
112
+ ) {
113
+ if (sourceQuery.table.sourceName === filterQuery.table.sourceName) {
114
+ // TODO: Handle aliasing properly instead of just forbidding it.
115
+ throw new Error('Cannot semijoin a table to itself');
116
+ }
117
+ }
118
+
119
+ build(): Query<TableDef> {
120
+ return this as Query<TableDef>;
121
+ }
122
+
123
+ where(
124
+ predicate: (row: RowExpr<TableDef>) => PredicateExpr<TableDef>
125
+ ): SemijoinImpl<TableDef> {
126
+ const nextSourceQuery = this.sourceQuery.where(predicate);
127
+ return new SemijoinImpl<TableDef>(
128
+ nextSourceQuery,
129
+ this.filterQuery,
130
+ this.joinCondition
131
+ );
132
+ }
133
+
134
+ toSql(): string {
135
+ const left = this.filterQuery;
136
+ const right = this.sourceQuery;
137
+ const leftTable = quoteIdentifier(left.table.sourceName);
138
+ const rightTable = quoteIdentifier(right.table.sourceName);
139
+ let sql = `SELECT ${rightTable}.* FROM ${leftTable} JOIN ${rightTable} ON ${booleanExprToSql(this.joinCondition)}`;
140
+
141
+ const clauses: string[] = [];
142
+ if (left.whereClause) {
143
+ clauses.push(booleanExprToSql(left.whereClause));
144
+ }
145
+ if (right.whereClause) {
146
+ clauses.push(booleanExprToSql(right.whereClause));
147
+ }
148
+
149
+ if (clauses.length > 0) {
150
+ const whereSql =
151
+ clauses.length === 1
152
+ ? clauses[0]
153
+ : clauses.map(wrapInParens).join(' AND ');
154
+ sql += ` WHERE ${whereSql}`;
155
+ }
156
+
157
+ return sql;
158
+ }
159
+ }
160
+
161
+ class FromBuilder<TableDef extends TypedTableDef>
162
+ implements From<TableDef>, TableTypedQuery<TableDef>
163
+ {
164
+ readonly [QueryBrand] = true;
165
+ constructor(
166
+ readonly table: TableRef<TableDef>,
167
+ readonly whereClause?: BooleanExpr<TableDef>
168
+ ) {}
169
+
170
+ where(
171
+ predicate: (row: RowExpr<TableDef>) => PredicateExpr<TableDef>
172
+ ): FromBuilder<TableDef> {
173
+ const newCondition = normalizePredicateExpr(predicate(this.table.cols));
174
+ const nextWhere = this.whereClause
175
+ ? this.whereClause.and(newCondition)
176
+ : newCondition;
177
+ return new FromBuilder<TableDef>(this.table, nextWhere);
178
+ }
179
+
180
+ rightSemijoin<OtherTable extends TypedTableDef>(
181
+ right: TableRef<OtherTable>,
182
+ on: (
183
+ left: IndexedRowExpr<TableDef>,
184
+ right: IndexedRowExpr<OtherTable>
185
+ ) => BooleanExpr<TableDef | OtherTable>
186
+ ): SemijoinBuilder<OtherTable> {
187
+ const sourceQuery = new FromBuilder(right);
188
+ const joinCondition = on(
189
+ this.table.indexedCols,
190
+ right.indexedCols
191
+ ) as BooleanExpr<any>;
192
+ return new SemijoinImpl<OtherTable>(sourceQuery, this, joinCondition);
193
+ }
194
+
195
+ leftSemijoin<OtherTable extends TypedTableDef>(
196
+ right: TableRef<OtherTable>,
197
+ on: (
198
+ left: IndexedRowExpr<TableDef>,
199
+ right: IndexedRowExpr<OtherTable>
200
+ ) => BooleanExpr<TableDef | OtherTable>
201
+ ): SemijoinBuilder<TableDef> {
202
+ const filterQuery = new FromBuilder(right);
203
+ const joinCondition = on(
204
+ this.table.indexedCols,
205
+ right.indexedCols
206
+ ) as BooleanExpr<any>;
207
+ return new SemijoinImpl<TableDef>(this, filterQuery, joinCondition);
208
+ }
209
+
210
+ toSql(): string {
211
+ return renderSelectSqlWithJoins(this.table, this.whereClause);
212
+ }
213
+
214
+ build(): Query<TableDef> {
215
+ return this as Query<TableDef>;
216
+ }
217
+ }
218
+
219
+ export type QueryBuilder<SchemaDef extends UntypedSchemaDef> = {
220
+ readonly [Tbl in Values<
221
+ SchemaDef['tables']
222
+ > as Tbl['accessorName']]: TableRef<Tbl> & From<Tbl>;
223
+ } & {};
224
+
225
+ /**
226
+ * A runtime reference to a table. This materializes the RowExpr for us.
227
+ * TODO: Maybe add the full SchemaDef to the type signature depending on how joins will work.
228
+ */
229
+ export type TableRef<TableDef extends TypedTableDef> = Readonly<{
230
+ type: 'table';
231
+ sourceName: TableDef['sourceName'];
232
+ accessorName: string;
233
+ cols: RowExpr<TableDef>;
234
+ indexedCols: IndexedRowExpr<TableDef>;
235
+ tableDef: TableDef;
236
+ // Delegated UntypedTableDef properties for compatibility.
237
+ columns: TableDef['columns'];
238
+ indexes: TableDef['indexes'];
239
+ rowType: TableDef['rowType'];
240
+ constraints: any;
241
+ }>;
242
+
243
+ class TableRefImpl<TableDef extends TypedTableDef>
244
+ implements TableRef<TableDef>, From<TableDef>
245
+ {
246
+ readonly [QueryBrand] = true;
247
+ readonly type = 'table' as const;
248
+ sourceName: string;
249
+ accessorName: string;
250
+ cols: RowExpr<TableDef>;
251
+ indexedCols: IndexedRowExpr<TableDef>;
252
+ tableDef: TableDef;
253
+ // Delegate UntypedTableDef properties from tableDef so this can be used as a table def.
254
+ get columns() {
255
+ return this.tableDef.columns;
256
+ }
257
+ get indexes() {
258
+ return this.tableDef.indexes;
259
+ }
260
+ get rowType() {
261
+ return this.tableDef.rowType;
262
+ }
263
+ get constraints() {
264
+ return (this.tableDef as any).constraints;
265
+ }
266
+ constructor(tableDef: TableDef) {
267
+ this.sourceName = tableDef.sourceName;
268
+ this.accessorName = tableDef.accessorName;
269
+ this.cols = createRowExpr(tableDef);
270
+ // this.indexedCols = createIndexedRowExpr(tableDef, this.cols);
271
+ // TODO: we could create an indexedRowExpr to avoid having the extra columns.
272
+ // Right now, the objects we pass will actually have all the columns, but the
273
+ // type system will consider it an error.
274
+ this.indexedCols = this.cols;
275
+ this.tableDef = tableDef;
276
+ Object.freeze(this);
277
+ }
278
+
279
+ asFrom(): FromBuilder<TableDef> {
280
+ return new FromBuilder<TableDef>(this);
281
+ }
282
+
283
+ rightSemijoin<RightTable extends TypedTableDef>(
284
+ other: TableRef<RightTable>,
285
+ on: (
286
+ left: IndexedRowExpr<TableDef>,
287
+ right: IndexedRowExpr<RightTable>
288
+ ) => EqExpr<TableDef | RightTable>
289
+ ): SemijoinBuilder<RightTable> {
290
+ return this.asFrom().rightSemijoin(other, on);
291
+ }
292
+
293
+ leftSemijoin<RightTable extends TypedTableDef>(
294
+ other: TableRef<RightTable>,
295
+ on: (
296
+ left: IndexedRowExpr<TableDef>,
297
+ right: IndexedRowExpr<RightTable>
298
+ ) => EqExpr<TableDef | RightTable>
299
+ ): SemijoinBuilder<TableDef> {
300
+ return this.asFrom().leftSemijoin(other, on);
301
+ }
302
+
303
+ build(): Query<TableDef> {
304
+ return this.asFrom().build();
305
+ }
306
+
307
+ toSql(): string {
308
+ return this.asFrom().toSql();
309
+ }
310
+
311
+ where(
312
+ predicate: (row: RowExpr<TableDef>) => PredicateExpr<TableDef>
313
+ ): FromBuilder<TableDef> {
314
+ return this.asFrom().where(predicate);
315
+ }
316
+ }
317
+
318
+ export function createTableRefFromDef<TableDef extends TypedTableDef>(
319
+ tableDef: TableDef
320
+ ): TableRef<TableDef> {
321
+ return new TableRefImpl<TableDef>(tableDef);
322
+ }
323
+
324
+ export function makeQueryBuilder<SchemaDef extends UntypedSchemaDef>(
325
+ schema: SchemaDef
326
+ ): QueryBuilder<SchemaDef> {
327
+ const qb = Object.create(null) as QueryBuilder<SchemaDef>;
328
+ for (const table of Object.values(schema.tables)) {
329
+ const ref = createTableRefFromDef(
330
+ table as TableDefByName<SchemaDef, TableNames<SchemaDef>>
331
+ );
332
+ (qb as Record<string, TableRef<any>>)[table.accessorName] = ref;
333
+ }
334
+ return Object.freeze(qb) as QueryBuilder<SchemaDef>;
335
+ }
336
+
337
+ function createRowExpr<TableDef extends TypedTableDef>(
338
+ tableDef: TableDef
339
+ ): RowExpr<TableDef> {
340
+ const row: Record<string, ColumnExpr<TableDef, any>> = {};
341
+ for (const columnName of Object.keys(tableDef.columns) as Array<
342
+ keyof TableDef['columns'] & string
343
+ >) {
344
+ const columnBuilder = tableDef.columns[columnName];
345
+ const column = new ColumnExpression<TableDef, typeof columnName>(
346
+ tableDef.sourceName,
347
+ columnName,
348
+ columnBuilder.typeBuilder.algebraicType as InferSpacetimeTypeOfColumn<
349
+ TableDef,
350
+ typeof columnName
351
+ >,
352
+ columnBuilder.columnMetadata.name
353
+ );
354
+ row[columnName] = Object.freeze(column);
355
+ }
356
+ return Object.freeze(row) as RowExpr<TableDef>;
357
+ }
358
+
359
+ function renderSelectSqlWithJoins<Table extends TypedTableDef>(
360
+ table: TableRef<Table>,
361
+ where?: BooleanExpr<Table>,
362
+ extraClauses: readonly string[] = []
363
+ ): string {
364
+ const quotedTable = quoteIdentifier(table.sourceName);
365
+ const sql = `SELECT * FROM ${quotedTable}`;
366
+ const clauses: string[] = [];
367
+ if (where) clauses.push(booleanExprToSql(where));
368
+ clauses.push(...extraClauses);
369
+ if (clauses.length === 0) return sql;
370
+ const whereSql =
371
+ clauses.length === 1 ? clauses[0] : clauses.map(wrapInParens).join(' AND ');
372
+ return `${sql} WHERE ${whereSql}`;
373
+ }
374
+
375
+ // TODO: Just use UntypedTableDef if they end up being the same.
376
+ export type TypedTableDef<
377
+ Columns extends Record<
378
+ string,
379
+ ColumnBuilder<any, any, ColumnMetadata<any>>
380
+ > = Record<string, ColumnBuilder<any, any, ColumnMetadata<any>>>,
381
+ > = {
382
+ sourceName: string;
383
+ accessorName: string;
384
+ columns: Columns;
385
+ indexes: readonly IndexOpts<any>[];
386
+ rowType: RowBuilder<Columns>['algebraicType']['value'];
387
+ };
388
+
389
+ export type TableSchemaAsTableDef<TSchema extends UntypedTableSchema> = {
390
+ name: TSchema['tableName'];
391
+ columns: TSchema['rowType']['row'];
392
+ indexes: TSchema['idxs'];
393
+ };
394
+
395
+ type RowType<TableDef extends TypedTableDef> = {
396
+ [K in keyof TableDef['columns']]: TableDef['columns'][K] extends ColumnBuilder<
397
+ infer T,
398
+ any,
399
+ any
400
+ >
401
+ ? T
402
+ : never;
403
+ };
404
+
405
+ // TODO: Consider making a smaller version of these types that doesn't expose the internals.
406
+ // Restricting it later should not break anyone in practice.
407
+ export type ColumnExpr<
408
+ TableDef extends TypedTableDef,
409
+ ColumnName extends ColumnNames<TableDef>,
410
+ > = ColumnExpression<TableDef, ColumnName>;
411
+
412
+ type ColumnSpacetimeType<Col extends ColumnExpr<any, any>> =
413
+ Col extends ColumnExpr<infer T, infer N>
414
+ ? InferSpacetimeTypeOfColumn<T, N>
415
+ : never;
416
+
417
+ // TODO: This checks that they match, but we also need to make sure that they are comparable types,
418
+ // since you can use product types at all.
419
+ type ColumnSameSpacetime<
420
+ ThisTable extends TypedTableDef,
421
+ ThisCol extends ColumnNames<ThisTable>,
422
+ OtherCol extends ColumnExpr<any, any>,
423
+ > = [InferSpacetimeTypeOfColumn<ThisTable, ThisCol>] extends [
424
+ ColumnSpacetimeType<OtherCol>,
425
+ ]
426
+ ? [ColumnSpacetimeType<OtherCol>] extends [
427
+ InferSpacetimeTypeOfColumn<ThisTable, ThisCol>,
428
+ ]
429
+ ? OtherCol
430
+ : never
431
+ : never;
432
+
433
+ // Helper to get the table back from a column.
434
+ type ExtractTable<Col extends ColumnExpr<any, any>> =
435
+ Col extends ColumnExpr<infer T, any> ? T : never;
436
+
437
+ export class ColumnExpression<
438
+ TableDef extends TypedTableDef,
439
+ ColumnName extends ColumnNames<TableDef>,
440
+ > {
441
+ readonly type = 'column' as const;
442
+ // This is the column accessor
443
+ readonly column: ColumnName;
444
+ // The name of the column in the database.
445
+ readonly columnName: string;
446
+ readonly table: TableDef['sourceName'];
447
+ // phantom: actual runtime value is undefined
448
+ readonly tsValueType?: RowType<TableDef>[ColumnName];
449
+ readonly spacetimeType: InferSpacetimeTypeOfColumn<TableDef, ColumnName>;
450
+
451
+ constructor(
452
+ table: TableDef['sourceName'],
453
+ column: ColumnName,
454
+ spacetimeType: InferSpacetimeTypeOfColumn<TableDef, ColumnName>,
455
+ columnName?: string
456
+ ) {
457
+ this.table = table;
458
+ this.column = column;
459
+ this.columnName = columnName || column;
460
+ this.spacetimeType = spacetimeType;
461
+ }
462
+
463
+ eq(
464
+ literal: LiteralValue & RowType<TableDef>[ColumnName]
465
+ ): BooleanExpr<TableDef>;
466
+ eq<OtherCol extends ColumnExpr<any, any>>(
467
+ value: ColumnSameSpacetime<TableDef, ColumnName, OtherCol>
468
+ ): BooleanExpr<TableDef | ExtractTable<OtherCol>>;
469
+
470
+ eq(x: any): any {
471
+ return new BooleanExpr({
472
+ type: 'eq',
473
+ left: this as unknown as ValueExpr<TableDef, any>,
474
+ right: normalizeValue(x) as ValueExpr<TableDef, any>,
475
+ });
476
+ }
477
+
478
+ ne(
479
+ literal: LiteralValue & RowType<TableDef>[ColumnName]
480
+ ): BooleanExpr<TableDef>;
481
+ ne<OtherCol extends ColumnExpr<any, any>>(
482
+ value: ColumnSameSpacetime<TableDef, ColumnName, OtherCol>
483
+ ): BooleanExpr<TableDef | ExtractTable<OtherCol>>;
484
+
485
+ ne(x: any): any {
486
+ return new BooleanExpr({
487
+ type: 'ne',
488
+ left: this as unknown as ValueExpr<TableDef, any>,
489
+ right: normalizeValue(x) as ValueExpr<TableDef, any>,
490
+ });
491
+ }
492
+
493
+ lt(
494
+ literal: LiteralValue & RowType<TableDef>[ColumnName]
495
+ ): BooleanExpr<TableDef>;
496
+ lt<OtherCol extends ColumnExpr<any, any>>(
497
+ value: ColumnSameSpacetime<TableDef, ColumnName, OtherCol>
498
+ ): BooleanExpr<TableDef | ExtractTable<OtherCol>>;
499
+
500
+ lt(x: any): any {
501
+ return new BooleanExpr({
502
+ type: 'lt',
503
+ left: this as unknown as ValueExpr<TableDef, any>,
504
+ right: normalizeValue(x) as ValueExpr<TableDef, any>,
505
+ });
506
+ }
507
+
508
+ lte(
509
+ literal: LiteralValue & RowType<TableDef>[ColumnName]
510
+ ): BooleanExpr<TableDef>;
511
+ lte<OtherCol extends ColumnExpr<any, any>>(
512
+ value: ColumnSameSpacetime<TableDef, ColumnName, OtherCol>
513
+ ): BooleanExpr<TableDef | ExtractTable<OtherCol>>;
514
+
515
+ lte(x: any): any {
516
+ return new BooleanExpr({
517
+ type: 'lte',
518
+ left: this as unknown as ValueExpr<TableDef, any>,
519
+ right: normalizeValue(x) as ValueExpr<TableDef, any>,
520
+ });
521
+ }
522
+
523
+ gt(
524
+ literal: LiteralValue & RowType<TableDef>[ColumnName]
525
+ ): BooleanExpr<TableDef>;
526
+ gt<OtherCol extends ColumnExpr<any, any>>(
527
+ value: ColumnSameSpacetime<TableDef, ColumnName, OtherCol>
528
+ ): BooleanExpr<TableDef | ExtractTable<OtherCol>>;
529
+
530
+ gt(x: any): any {
531
+ return new BooleanExpr({
532
+ type: 'gt',
533
+ left: this as unknown as ValueExpr<TableDef, any>,
534
+ right: normalizeValue(x) as ValueExpr<TableDef, any>,
535
+ });
536
+ }
537
+
538
+ gte(
539
+ literal: LiteralValue & RowType<TableDef>[ColumnName]
540
+ ): BooleanExpr<TableDef>;
541
+ gte<OtherCol extends ColumnExpr<any, any>>(
542
+ value: ColumnSameSpacetime<TableDef, ColumnName, OtherCol>
543
+ ): BooleanExpr<TableDef | ExtractTable<OtherCol>>;
544
+
545
+ gte(x: any): any {
546
+ return new BooleanExpr({
547
+ type: 'gte',
548
+ left: this as unknown as ValueExpr<TableDef, any>,
549
+ right: normalizeValue(x) as ValueExpr<TableDef, any>,
550
+ });
551
+ }
552
+ }
553
+
554
+ /**
555
+ * Helper to get the spacetime type of a column.
556
+ */
557
+ type InferSpacetimeTypeOfColumn<
558
+ TableDef extends TypedTableDef,
559
+ ColumnName extends ColumnNames<TableDef>,
560
+ > =
561
+ TableDef['columns'][ColumnName]['typeBuilder'] extends TypeBuilder<
562
+ any,
563
+ infer U
564
+ >
565
+ ? U
566
+ : never;
567
+
568
+ type ColumnNames<TableDef extends TypedTableDef> = keyof RowType<TableDef> &
569
+ string;
570
+
571
+ // For composite indexes, we only consider it as an index over the first column in the index.
572
+ type FirstIndexColumn<I extends IndexOpts<any>> =
573
+ IndexColumns<I> extends readonly [infer Head extends string, ...infer _Rest]
574
+ ? Head
575
+ : never;
576
+
577
+ // Columns that are indexed by something in the indexes: [...] part.
578
+ type ExplicitIndexedColumns<TableDef extends TypedTableDef> =
579
+ TableDef['indexes'][number] extends infer I
580
+ ? I extends IndexOpts<ColumnNames<TableDef>>
581
+ ? FirstIndexColumn<I> & ColumnNames<TableDef>
582
+ : never
583
+ : never;
584
+
585
+ // Columns with an index defined on the column definition.
586
+ type MetadataIndexedColumns<TableDef extends TypedTableDef> = {
587
+ [K in ColumnNames<TableDef>]: ColumnIndex<
588
+ K,
589
+ TableDef['columns'][K]['columnMetadata']
590
+ > extends never
591
+ ? never
592
+ : K;
593
+ }[ColumnNames<TableDef>];
594
+
595
+ export type IndexedColumnNames<TableDef extends TypedTableDef> =
596
+ | ExplicitIndexedColumns<TableDef>
597
+ | MetadataIndexedColumns<TableDef>;
598
+
599
+ export type IndexedRowExpr<TableDef extends TypedTableDef> = Readonly<{
600
+ readonly [C in IndexedColumnNames<TableDef>]: ColumnExpr<TableDef, C>;
601
+ }>;
602
+
603
+ /**
604
+ * Acts as a row when writing filters for queries. It is a way to get column references.
605
+ */
606
+ export type RowExpr<TableDef extends TypedTableDef> = Readonly<{
607
+ readonly [C in ColumnNames<TableDef>]: ColumnExpr<TableDef, C>;
608
+ }>;
609
+
610
+ /**
611
+ * Union of ColumnExprs from Table whose spacetimeType is compatible with Value
612
+ * (produces a union of ColumnExpr<Table, C> for matching columns).
613
+ */
614
+ export type ColumnExprForValue<Table extends TypedTableDef, Value> = {
615
+ [C in ColumnNames<Table>]: InferSpacetimeTypeOfColumn<Table, C> extends Value
616
+ ? ColumnExpr<Table, C>
617
+ : never;
618
+ }[ColumnNames<Table>];
619
+
620
+ type LiteralValue =
621
+ | string
622
+ | number
623
+ | bigint
624
+ | boolean
625
+ | Identity
626
+ | Timestamp
627
+ | ConnectionId;
628
+
629
+ type ValueLike = LiteralValue | ColumnExpr<any, any> | LiteralExpr<any>;
630
+ type ValueInput<TableDef extends TypedTableDef> =
631
+ | ValueLike
632
+ | ValueExpr<TableDef, any>;
633
+
634
+ export type ValueExpr<TableDef extends TypedTableDef, Value> =
635
+ | LiteralExpr<Value & LiteralValue>
636
+ | ColumnExprForValue<TableDef, Value>;
637
+
638
+ type PredicateExpr<TableDef extends TypedTableDef> =
639
+ | BooleanExpr<TableDef>
640
+ | ColumnExprForValue<TableDef, SatsBool>
641
+ | boolean;
642
+
643
+ type LiteralExpr<Value> = {
644
+ type: 'literal';
645
+ value: Value;
646
+ };
647
+
648
+ export function literal<Value extends LiteralValue>(
649
+ value: Value
650
+ ): ValueExpr<never, Value> {
651
+ return { type: 'literal', value };
652
+ }
653
+
654
+ // This is here to take literal values and wrap them in an AST node.
655
+ function normalizeValue(val: ValueInput<any>): ValueExpr<any, any> {
656
+ if ((val as LiteralExpr<any>).type === 'literal')
657
+ return val as LiteralExpr<any>;
658
+ if (
659
+ typeof val === 'object' &&
660
+ val != null &&
661
+ 'type' in (val as any) &&
662
+ (val as any).type === 'column'
663
+ ) {
664
+ return val as ColumnExpr<any, any>;
665
+ }
666
+ return literal(val as LiteralValue);
667
+ }
668
+
669
+ function normalizePredicateExpr<TableDef extends TypedTableDef>(
670
+ value: PredicateExpr<TableDef>
671
+ ): BooleanExpr<TableDef> {
672
+ if (value instanceof BooleanExpr) return value;
673
+ if (typeof value === 'boolean') {
674
+ return new BooleanExpr({
675
+ type: 'eq',
676
+ left: literal(value),
677
+ right: literal(true),
678
+ });
679
+ }
680
+ return new BooleanExpr({
681
+ type: 'eq',
682
+ left: value as ValueExpr<TableDef, any>,
683
+ right: literal(true),
684
+ });
685
+ }
686
+
687
+ type EqExpr<Table extends TypedTableDef = any> = BooleanExpr<Table>;
688
+
689
+ type BooleanExprData<Table extends TypedTableDef> = (
690
+ | {
691
+ type: 'eq' | 'ne' | 'gt' | 'lt' | 'gte' | 'lte';
692
+ left: ValueExpr<Table, any>;
693
+ right: ValueExpr<Table, any>;
694
+ }
695
+ | {
696
+ type: 'and';
697
+ clauses: readonly [
698
+ BooleanExprData<Table>,
699
+ BooleanExprData<Table>,
700
+ ...BooleanExprData<Table>[],
701
+ ];
702
+ }
703
+ | {
704
+ type: 'or';
705
+ clauses: readonly [
706
+ BooleanExprData<Table>,
707
+ BooleanExprData<Table>,
708
+ ...BooleanExprData<Table>[],
709
+ ];
710
+ }
711
+ | {
712
+ type: 'not';
713
+ clause: BooleanExprData<Table>;
714
+ }
715
+ ) & {
716
+ _tableType?: Table;
717
+ };
718
+
719
+ type AndOrMixedTableScopeError = {
720
+ readonly 'Cannot combine predicates from different table scopes with and/or. In semijoin on(...), keep only the join equality and move extra predicates to .where(...).': never;
721
+ };
722
+
723
+ type RequireSameAndOrTable<
724
+ Expected extends TypedTableDef,
725
+ Actual extends TypedTableDef,
726
+ > = [Expected] extends [Actual]
727
+ ? [Actual] extends [Expected]
728
+ ? unknown
729
+ : AndOrMixedTableScopeError
730
+ : AndOrMixedTableScopeError;
731
+
732
+ export class BooleanExpr<Table extends TypedTableDef> {
733
+ constructor(readonly data: BooleanExprData<Table>) {}
734
+
735
+ and<OtherTable extends TypedTableDef>(
736
+ other: BooleanExpr<OtherTable> & RequireSameAndOrTable<Table, OtherTable>
737
+ ): BooleanExpr<Table> {
738
+ return new BooleanExpr({
739
+ type: 'and',
740
+ clauses: [this.data, other.data as BooleanExprData<Table>],
741
+ });
742
+ }
743
+
744
+ or<OtherTable extends TypedTableDef>(
745
+ other: BooleanExpr<OtherTable> & RequireSameAndOrTable<Table, OtherTable>
746
+ ): BooleanExpr<Table> {
747
+ return new BooleanExpr({
748
+ type: 'or',
749
+ clauses: [this.data, other.data as BooleanExprData<Table>],
750
+ });
751
+ }
752
+
753
+ not(): BooleanExpr<Table> {
754
+ return new BooleanExpr({ type: 'not', clause: this.data });
755
+ }
756
+ }
757
+
758
+ export function not<T extends TypedTableDef>(
759
+ clause: BooleanExpr<T>
760
+ ): BooleanExpr<T> {
761
+ return new BooleanExpr({ type: 'not', clause: clause.data });
762
+ }
763
+
764
+ export function and<
765
+ Table extends TypedTableDef,
766
+ OtherTable extends TypedTableDef,
767
+ >(
768
+ first: BooleanExpr<Table>,
769
+ second: BooleanExpr<OtherTable> & RequireSameAndOrTable<Table, OtherTable>,
770
+ ...rest: readonly BooleanExpr<Table>[]
771
+ ): BooleanExpr<Table> {
772
+ const clauses = [first, second, ...rest];
773
+ return new BooleanExpr({
774
+ type: 'and',
775
+ clauses: clauses.map(c => c.data) as [
776
+ BooleanExprData<Table>,
777
+ BooleanExprData<Table>,
778
+ ...BooleanExprData<Table>[],
779
+ ],
780
+ });
781
+ }
782
+
783
+ export function or<
784
+ Table extends TypedTableDef,
785
+ OtherTable extends TypedTableDef,
786
+ >(
787
+ first: BooleanExpr<Table>,
788
+ second: BooleanExpr<OtherTable> & RequireSameAndOrTable<Table, OtherTable>,
789
+ ...rest: readonly BooleanExpr<Table>[]
790
+ ): BooleanExpr<Table> {
791
+ const clauses = [first, second, ...rest];
792
+ return new BooleanExpr({
793
+ type: 'or',
794
+ clauses: clauses.map(c => c.data) as [
795
+ BooleanExprData<Table>,
796
+ BooleanExprData<Table>,
797
+ ...BooleanExprData<Table>[],
798
+ ],
799
+ });
800
+ }
801
+
802
+ function booleanExprToSql<Table extends TypedTableDef>(
803
+ expr: BooleanExpr<Table> | BooleanExprData<Table>,
804
+ tableAlias?: string
805
+ ): string {
806
+ const data = expr instanceof BooleanExpr ? expr.data : expr;
807
+ switch (data.type) {
808
+ case 'eq':
809
+ return `${valueExprToSql(data.left, tableAlias)} = ${valueExprToSql(data.right, tableAlias)}`;
810
+ case 'ne':
811
+ return `${valueExprToSql(data.left, tableAlias)} <> ${valueExprToSql(data.right, tableAlias)}`;
812
+ case 'gt':
813
+ return `${valueExprToSql(data.left, tableAlias)} > ${valueExprToSql(data.right, tableAlias)}`;
814
+ case 'gte':
815
+ return `${valueExprToSql(data.left, tableAlias)} >= ${valueExprToSql(data.right, tableAlias)}`;
816
+ case 'lt':
817
+ return `${valueExprToSql(data.left, tableAlias)} < ${valueExprToSql(data.right, tableAlias)}`;
818
+ case 'lte':
819
+ return `${valueExprToSql(data.left, tableAlias)} <= ${valueExprToSql(data.right, tableAlias)}`;
820
+ case 'and':
821
+ return data.clauses
822
+ .map(c => booleanExprToSql(c, tableAlias))
823
+ .map(wrapInParens)
824
+ .join(' AND ');
825
+ case 'or':
826
+ return data.clauses
827
+ .map(c => booleanExprToSql(c, tableAlias))
828
+ .map(wrapInParens)
829
+ .join(' OR ');
830
+ case 'not':
831
+ return `NOT ${wrapInParens(booleanExprToSql(data.clause, tableAlias))}`;
832
+ }
833
+ }
834
+
835
+ function wrapInParens(sql: string): string {
836
+ return `(${sql})`;
837
+ }
838
+
839
+ function valueExprToSql<Table extends TypedTableDef>(
840
+ expr: ValueExpr<Table, any>,
841
+ tableAlias?: string
842
+ ): string {
843
+ if (isLiteralExpr(expr)) {
844
+ return literalValueToSql(expr.value);
845
+ }
846
+ const table = tableAlias ?? expr.table;
847
+ return `${quoteIdentifier(table)}.${quoteIdentifier(expr.columnName)}`;
848
+ }
849
+
850
+ function literalValueToSql(value: unknown): string {
851
+ if (value === null || value === undefined) {
852
+ return 'NULL';
853
+ }
854
+ if (value instanceof Identity || value instanceof ConnectionId) {
855
+ // We use this hex string syntax.
856
+ return `0x${value.toHexString()}`;
857
+ }
858
+ if (value instanceof Timestamp) {
859
+ return `'${value.toISOString()}'`;
860
+ }
861
+ switch (typeof value) {
862
+ case 'number':
863
+ case 'bigint':
864
+ return String(value);
865
+ case 'boolean':
866
+ return value ? 'TRUE' : 'FALSE';
867
+ case 'string':
868
+ return `'${value.replace(/'/g, "''")}'`;
869
+ default:
870
+ // It might be safer to error here?
871
+ return `'${JSON.stringify(value).replace(/'/g, "''")}'`;
872
+ }
873
+ }
874
+
875
+ function quoteIdentifier(name: string): string {
876
+ return `"${name.replace(/"/g, '""')}"`;
877
+ }
878
+
879
+ function isLiteralExpr<Value>(
880
+ expr: ValueExpr<any, Value>
881
+ ): expr is LiteralExpr<Value & LiteralValue> {
882
+ return (expr as LiteralExpr<Value>).type === 'literal';
883
+ }
884
+
885
+ /**
886
+ * Evaluate a BooleanExpr against a row at runtime for client-side filtering.
887
+ */
888
+ export function evaluateBooleanExpr(
889
+ expr: BooleanExpr<any>,
890
+ row: Record<string, any>
891
+ ): boolean {
892
+ return evaluateData(expr.data, row);
893
+ }
894
+
895
+ function evaluateData(
896
+ data: BooleanExprData<any>,
897
+ row: Record<string, any>
898
+ ): boolean {
899
+ switch (data.type) {
900
+ case 'eq':
901
+ return resolveValue(data.left, row) === resolveValue(data.right, row);
902
+ case 'ne':
903
+ return resolveValue(data.left, row) !== resolveValue(data.right, row);
904
+ case 'gt':
905
+ return resolveValue(data.left, row) > resolveValue(data.right, row);
906
+ case 'gte':
907
+ return resolveValue(data.left, row) >= resolveValue(data.right, row);
908
+ case 'lt':
909
+ return resolveValue(data.left, row) < resolveValue(data.right, row);
910
+ case 'lte':
911
+ return resolveValue(data.left, row) <= resolveValue(data.right, row);
912
+ case 'and':
913
+ return data.clauses.every(c => evaluateData(c, row));
914
+ case 'or':
915
+ return data.clauses.some(c => evaluateData(c, row));
916
+ case 'not':
917
+ return !evaluateData(data.clause, row);
918
+ }
919
+ }
920
+
921
+ function resolveValue(
922
+ expr: ValueExpr<any, any>,
923
+ row: Record<string, any>
924
+ ): any {
925
+ if (isLiteralExpr(expr)) {
926
+ return toComparableValue(expr.value);
927
+ }
928
+ return toComparableValue(row[expr.column]);
929
+ }
930
+
931
+ type TimestampLike = {
932
+ __timestamp_micros_since_unix_epoch__: bigint;
933
+ };
934
+
935
+ type HexSerializableLike = {
936
+ toHexString: () => string;
937
+ };
938
+
939
+ function isHexSerializableLike(value: unknown): value is HexSerializableLike {
940
+ return (
941
+ !!value &&
942
+ typeof value === 'object' &&
943
+ typeof (value as { toHexString?: unknown }).toHexString === 'function'
944
+ );
945
+ }
946
+
947
+ // Check if this value is a Timestamp-like object. This is here because
948
+ // running locally can end up with different versions of the Timestamp class,
949
+ // which breaks the simple instanceof version.
950
+ function isTimestampLike(value: unknown): value is TimestampLike {
951
+ if (!value || typeof value !== 'object') return false;
952
+
953
+ if (value instanceof Timestamp) return true;
954
+
955
+ const micros = (value as Record<string, unknown>)[
956
+ '__timestamp_micros_since_unix_epoch__'
957
+ ];
958
+ return typeof micros === 'bigint';
959
+ }
960
+
961
+ // Exported for tests.
962
+ export function toComparableValue(value: any): any {
963
+ // Handle `ConnectionId` and `Identity`.
964
+ if (isHexSerializableLike(value)) {
965
+ return value.toHexString();
966
+ }
967
+ if (isTimestampLike(value)) {
968
+ return value.__timestamp_micros_since_unix_epoch__;
969
+ }
970
+ return value;
971
+ }
972
+
973
+ /**
974
+ * Extract the table name from a query builder expression.
975
+ */
976
+ export function getQueryTableName(query: any): string {
977
+ if (query.table) return query.table.name; // FromBuilder
978
+ if (query.name) return query.name; // TableRefImpl
979
+ if (query.sourceQuery) return query.sourceQuery.table.name; // SemijoinImpl (source table)
980
+ throw new Error('Cannot extract table name from query');
981
+ }
982
+
983
+ /**
984
+ * Extract the accessor name from a query builder expression.
985
+ */
986
+ export function getQueryAccessorName(query: any): string {
987
+ if (query.table) return query.table.accessorName; // FromBuilder
988
+ if (query.accessorName) return query.accessorName; // TableRefImpl
989
+ if (query.sourceQuery) return query.sourceQuery.table.accessorName; // SemijoinImpl
990
+ throw new Error('Cannot extract accessor name from query');
991
+ }
992
+
993
+ /**
994
+ * Extract the BooleanExpr from a query builder, if any.
995
+ */
996
+ export function getQueryWhereClause(query: any): BooleanExpr<any> | undefined {
997
+ if (query.whereClause) return query.whereClause; // FromBuilder
998
+ return undefined; // TableRefImpl has no where clause
999
+ }
1000
+
1001
+ // TODO: Fix this.
1002
+ function _createIndexedRowExpr<TableDef extends TypedTableDef>(
1003
+ tableDef: TableDef,
1004
+ cols: RowExpr<TableDef>
1005
+ ): IndexedRowExpr<TableDef> {
1006
+ const indexed = new Set<ColumnNames<TableDef>>();
1007
+ for (const idx of tableDef.indexes) {
1008
+ if ('columns' in idx) {
1009
+ const [first] = idx.columns;
1010
+ if (first) indexed.add(first);
1011
+ } else if ('column' in idx) {
1012
+ indexed.add(idx.column);
1013
+ }
1014
+ }
1015
+ const pickedEntries = [...indexed].map(name => [name, cols[name]]);
1016
+ return Object.freeze(
1017
+ Object.fromEntries(pickedEntries)
1018
+ ) as IndexedRowExpr<TableDef>;
1019
+ }