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.
- package/LICENSE.txt +759 -759
- package/README.md +211 -211
- package/dist/angular/index.cjs.map +1 -1
- package/dist/angular/index.mjs.map +1 -1
- package/dist/browser/angular/index.mjs.map +1 -1
- package/dist/browser/react/index.mjs +129 -57
- package/dist/browser/react/index.mjs.map +1 -1
- package/dist/browser/solid/index.mjs +120 -50
- package/dist/browser/solid/index.mjs.map +1 -1
- package/dist/browser/svelte/index.mjs.map +1 -1
- package/dist/browser/vue/index.mjs.map +1 -1
- package/dist/index.browser.mjs +10 -2
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.cjs +10 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +10 -2
- package/dist/index.mjs.map +1 -1
- package/dist/min/index.browser.mjs +1 -1
- package/dist/min/index.browser.mjs.map +1 -1
- package/dist/min/react/index.mjs +1 -1
- package/dist/min/react/index.mjs.map +1 -1
- package/dist/min/sdk/index.browser.mjs +1 -1
- package/dist/min/sdk/index.browser.mjs.map +1 -1
- package/dist/react/index.cjs +129 -57
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.mjs +129 -57
- package/dist/react/index.mjs.map +1 -1
- package/dist/react/useTable.d.ts.map +1 -1
- package/dist/sdk/connection_manager.d.ts +8 -0
- package/dist/sdk/connection_manager.d.ts.map +1 -1
- package/dist/sdk/db_connection_impl.d.ts +7 -0
- package/dist/sdk/db_connection_impl.d.ts.map +1 -1
- package/dist/sdk/index.browser.mjs +10 -2
- package/dist/sdk/index.browser.mjs.map +1 -1
- package/dist/sdk/index.cjs +10 -2
- package/dist/sdk/index.cjs.map +1 -1
- package/dist/sdk/index.mjs +10 -2
- package/dist/sdk/index.mjs.map +1 -1
- package/dist/sdk/websocket_test_adapter.d.ts +2 -1
- package/dist/sdk/websocket_test_adapter.d.ts.map +1 -1
- package/dist/server/index.mjs.map +1 -1
- package/dist/server/runtime.d.ts.map +1 -1
- package/dist/solid/index.cjs +120 -50
- package/dist/solid/index.cjs.map +1 -1
- package/dist/solid/index.mjs +120 -50
- package/dist/solid/index.mjs.map +1 -1
- package/dist/svelte/index.cjs.map +1 -1
- package/dist/svelte/index.mjs.map +1 -1
- package/dist/tanstack/index.cjs +120 -50
- package/dist/tanstack/index.cjs.map +1 -1
- package/dist/tanstack/index.mjs +120 -50
- package/dist/tanstack/index.mjs.map +1 -1
- package/dist/vue/index.cjs.map +1 -1
- package/dist/vue/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/angular/connection_state.ts +19 -19
- package/src/angular/index.ts +3 -3
- package/src/angular/injectors/index.ts +4 -4
- package/src/angular/injectors/inject-reducer.ts +62 -62
- package/src/angular/injectors/inject-spacetimedb-connected.ts +13 -13
- package/src/angular/injectors/inject-spacetimedb.ts +10 -10
- package/src/angular/injectors/inject-table.ts +234 -234
- package/src/angular/providers/index.ts +1 -1
- package/src/angular/providers/provide-spacetimedb.ts +96 -96
- package/src/index.ts +16 -16
- package/src/lib/algebraic_type.ts +819 -819
- package/src/lib/algebraic_type_variants.ts +26 -26
- package/src/lib/algebraic_value.ts +10 -10
- package/src/lib/autogen/types.ts +746 -746
- package/src/lib/binary_reader.ts +188 -188
- package/src/lib/binary_writer.ts +213 -213
- package/src/lib/connection_id.ts +102 -102
- package/src/lib/constraints.ts +48 -48
- package/src/lib/errors.ts +26 -26
- package/src/lib/filter.ts +195 -195
- package/src/lib/identity.ts +83 -83
- package/src/lib/indexes.ts +251 -251
- package/src/lib/option.ts +34 -34
- package/src/lib/query.ts +1019 -1019
- package/src/lib/reducer_schema.ts +38 -38
- package/src/lib/reducers.ts +116 -116
- package/src/lib/result.ts +36 -36
- package/src/lib/schedule_at.ts +86 -86
- package/src/lib/schema.ts +420 -420
- package/src/lib/table.ts +548 -548
- package/src/lib/table_schema.ts +64 -64
- package/src/lib/time_duration.ts +77 -77
- package/src/lib/timestamp.ts +148 -148
- package/src/lib/type_builders.test-d.ts +128 -128
- package/src/lib/type_builders.ts +4014 -4014
- package/src/lib/type_util.ts +124 -124
- package/src/lib/util.ts +196 -196
- package/src/lib/uuid.ts +337 -337
- package/src/react/SpacetimeDBProvider.ts +84 -84
- package/src/react/connection_state.ts +6 -6
- package/src/react/index.ts +5 -5
- package/src/react/useProcedure.ts +60 -60
- package/src/react/useReducer.ts +53 -53
- package/src/react/useSpacetimeDB.ts +18 -18
- package/src/react/useTable.ts +256 -251
- package/src/sdk/client_api/index.ts +114 -114
- package/src/sdk/client_api/types/procedures.ts +8 -8
- package/src/sdk/client_api/types/reducers.ts +8 -8
- package/src/sdk/client_api/types.ts +288 -288
- package/src/sdk/client_cache.ts +129 -129
- package/src/sdk/client_table.ts +179 -179
- package/src/sdk/connection_manager.ts +352 -237
- package/src/sdk/db_connection_builder.ts +290 -290
- package/src/sdk/db_connection_impl.ts +1356 -1347
- package/src/sdk/db_context.ts +28 -28
- package/src/sdk/db_view.ts +12 -12
- package/src/sdk/decompress.ts +51 -51
- package/src/sdk/event.ts +18 -18
- package/src/sdk/event_context.ts +51 -51
- package/src/sdk/event_emitter.ts +32 -32
- package/src/sdk/index.ts +14 -14
- package/src/sdk/internal.ts +2 -2
- package/src/sdk/json_api.ts +46 -46
- package/src/sdk/logger.ts +134 -134
- package/src/sdk/message_types.ts +46 -46
- package/src/sdk/procedures.ts +83 -83
- package/src/sdk/reducer_event.ts +20 -20
- package/src/sdk/reducer_handle.ts +12 -12
- package/src/sdk/reducers.ts +159 -159
- package/src/sdk/schema.ts +45 -45
- package/src/sdk/spacetime_module.ts +28 -28
- package/src/sdk/subscription_builder_impl.ts +275 -275
- package/src/sdk/table_cache.ts +581 -581
- package/src/sdk/type_utils.ts +19 -19
- package/src/sdk/version.ts +133 -133
- package/src/sdk/websocket_decompress_adapter.ts +63 -63
- package/src/sdk/websocket_protocols.ts +25 -25
- package/src/sdk/websocket_test_adapter.ts +107 -100
- package/src/sdk/websocket_v3_frames.ts +126 -126
- package/src/sdk/ws.ts +105 -105
- package/src/server/console.ts +81 -81
- package/src/server/db_view.ts +21 -21
- package/src/server/errors.ts +138 -138
- package/src/server/http.test-d.ts +80 -80
- package/src/server/http.ts +14 -14
- package/src/server/http_handlers.ts +413 -413
- package/src/server/http_internal.ts +79 -79
- package/src/server/http_shared.ts +186 -186
- package/src/server/index.ts +37 -37
- package/src/server/polyfills.ts +4 -4
- package/src/server/procedures.ts +239 -239
- package/src/server/query.ts +1 -1
- package/src/server/range.ts +53 -53
- package/src/server/reducers.ts +113 -113
- package/src/server/rng.ts +113 -113
- package/src/server/runtime.ts +1102 -1102
- package/src/server/schema.test-d.ts +99 -99
- package/src/server/schema.ts +663 -663
- package/src/server/sys.d.ts +125 -125
- package/src/server/view.test-d.ts +194 -194
- package/src/server/views.ts +340 -340
- package/src/solid/SpacetimeDBProvider.ts +97 -97
- package/src/solid/connection_state.ts +6 -6
- package/src/solid/index.ts +5 -5
- package/src/solid/useProcedure.ts +57 -57
- package/src/solid/useReducer.ts +50 -50
- package/src/solid/useSpacetimeDB.ts +18 -18
- package/src/solid/useTable.ts +203 -203
- package/src/svelte/SpacetimeDBProvider.ts +101 -101
- package/src/svelte/connection_state.ts +16 -16
- package/src/svelte/index.ts +4 -4
- package/src/svelte/useReducer.ts +61 -61
- package/src/svelte/useSpacetimeDB.ts +22 -22
- package/src/svelte/useTable.ts +218 -218
- package/src/tanstack/SpacetimeDBQueryClient.ts +330 -330
- package/src/tanstack/hooks.ts +83 -83
- package/src/tanstack/index.ts +16 -16
- package/src/util-stub.ts +1 -1
- package/src/vue/SpacetimeDBProvider.ts +157 -157
- package/src/vue/connection_state.ts +19 -19
- package/src/vue/index.ts +5 -5
- package/src/vue/useProcedure.ts +62 -62
- package/src/vue/useReducer.ts +55 -55
- package/src/vue/useSpacetimeDB.ts +18 -18
- 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
|
+
}
|