Skip to content

Commit d08bef0

Browse files
[TS]: Primary keys for query builder views
1 parent 2088fbc commit d08bef0

12 files changed

Lines changed: 524 additions & 41 deletions

File tree

crates/bindings-typescript/src/lib/query.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export const isRowTypedQuery = (val: unknown): val is RowTypedQuery<any, any> =>
5454
export const isTypedQuery = (val: unknown): val is TableTypedQuery<any> =>
5555
!!val && typeof val === 'object' && QueryBrand in (val as object);
5656

57-
export function toSql(q: Query<any>): string {
57+
export function toSql(q: RowTypedQuery<any, any>): string {
5858
return (q as unknown as { toSql(): string }).toSql();
5959
}
6060

crates/bindings-typescript/src/lib/schema.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
ArrayBuilder,
2020
OptionBuilder,
2121
ProductBuilder,
22+
QueryTypeBuilder,
2223
RefBuilder,
2324
ResultBuilder,
2425
RowBuilder,
@@ -307,6 +308,10 @@ export class ModuleContext {
307308
return new ArrayBuilder(
308309
this.registerTypesRecursively(typeBuilder.element)
309310
) as any;
311+
} else if (typeBuilder instanceof QueryTypeBuilder) {
312+
return new QueryTypeBuilder(
313+
this.registerTypesRecursively(typeBuilder.row)
314+
) as any;
310315
} else {
311316
return typeBuilder as any;
312317
}

crates/bindings-typescript/src/lib/type_builders.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import { Uuid, type UuidAlgebraicType } from './uuid';
1515
// Used in codegen files
1616
export { type AlgebraicTypeType } from './algebraic_type';
1717

18+
export const QUERY_VIEW_RETURN_TAG = '__query__' as const;
19+
1820
/**
1921
* Helper type to extract the TypeScript type from a TypeBuilder
2022
*/
@@ -1295,6 +1297,40 @@ export class ArrayBuilder<Element extends TypeBuilder<any, any>>
12951297
}
12961298
}
12971299

1300+
type QueryReturnType<Row extends TypeBuilder<object, any>> = {
1301+
tag: 'Product';
1302+
value: {
1303+
elements: [
1304+
{
1305+
name: typeof QUERY_VIEW_RETURN_TAG;
1306+
algebraicType: InferSpacetimeTypeOfTypeBuilder<Row>;
1307+
},
1308+
];
1309+
};
1310+
};
1311+
1312+
export class QueryTypeBuilder<Row extends TypeBuilder<object, any>>
1313+
extends TypeBuilder<readonly InferTypeOfTypeBuilder<Row>[], QueryReturnType<Row>>
1314+
{
1315+
readonly row: Row;
1316+
1317+
constructor(row: Row) {
1318+
super(
1319+
AlgebraicType.Product({
1320+
elements: [
1321+
{
1322+
name: QUERY_VIEW_RETURN_TAG,
1323+
get algebraicType() {
1324+
return row.algebraicType;
1325+
},
1326+
},
1327+
],
1328+
}) as QueryReturnType<Row>
1329+
);
1330+
this.row = row;
1331+
}
1332+
}
1333+
12981334
export class ByteArrayBuilder
12991335
extends TypeBuilder<
13001336
Uint8Array,
@@ -3871,6 +3907,16 @@ export const t = {
38713907
return new ArrayBuilder(e);
38723908
},
38733909

3910+
/**
3911+
* Creates a return type marker for query-builder views.
3912+
*
3913+
* This encodes as the special SATS product shape `{ __query__: T }`,
3914+
* where `T` is the row product type returned by the query.
3915+
*/
3916+
query<Row extends TypeBuilder<object, any>>(row: Row): QueryTypeBuilder<Row> {
3917+
return new QueryTypeBuilder(row);
3918+
},
3919+
38743920
enum: enumImpl,
38753921

38763922
/**

crates/bindings-typescript/src/server/view.test-d.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -71,10 +71,10 @@ const spacetime = schema({
7171
personWithMissing,
7272
});
7373

74-
const arrayRetValue = t.array(person.rowType);
74+
const queryRetValue = t.query(person.rowType);
7575
const optionalPerson = t.option(person.rowType);
7676

77-
spacetime.anonymousView({ name: 'v1', public: true }, arrayRetValue, ctx => {
77+
spacetime.anonymousView({ name: 'v1', public: true }, queryRetValue, ctx => {
7878
return ctx.from.person.build();
7979
});
8080

@@ -106,7 +106,7 @@ spacetime.anonymousView(
106106

107107
spacetime.anonymousView(
108108
{ name: 'v2', public: true },
109-
arrayRetValue,
109+
queryRetValue,
110110
// @ts-expect-error returns a query of the wrong type.
111111
ctx => {
112112
return ctx.from.order.build();
@@ -116,7 +116,7 @@ spacetime.anonymousView(
116116
// For queries, we can't return rows with extra fields.
117117
spacetime.anonymousView(
118118
{ name: 'v3', public: true },
119-
arrayRetValue,
119+
queryRetValue,
120120
// @ts-expect-error returns a query of the wrong type.
121121
ctx => {
122122
return ctx.from.personWithExtra.build();
@@ -126,7 +126,7 @@ spacetime.anonymousView(
126126
// Ideally this would fail, since we depend on the field ordering for serialization.
127127
spacetime.anonymousView(
128128
{ name: 'reorderedPerson', public: true },
129-
arrayRetValue,
129+
queryRetValue,
130130
// Comment this out if we can fix the types.
131131
// // @ts-expect-error returns a query of the wrong type.
132132
ctx => {
@@ -137,21 +137,21 @@ spacetime.anonymousView(
137137
// Fails because it is missing a field.
138138
spacetime.anonymousView(
139139
{ name: 'missingField', public: true },
140-
arrayRetValue,
140+
queryRetValue,
141141
// @ts-expect-error returns a query of the wrong type.
142142
ctx => {
143143
return ctx.from.personWithMissing.build();
144144
}
145145
);
146146

147-
spacetime.anonymousView({ name: 'v4', public: true }, arrayRetValue, ctx => {
147+
spacetime.anonymousView({ name: 'v4', public: true }, queryRetValue, ctx => {
148148
// @ts-expect-error returns a query of the wrong type.
149149
const _invalid = ctx.from.person.where(row => row.id.eq('string')).build();
150150
const _columnEqs = ctx.from.person.where(row => row.id.eq(row.id)).build();
151151
return ctx.from.person.where(row => row.id.eq(5)).build();
152152
});
153153

154-
spacetime.anonymousView({ name: 'v5', public: true }, arrayRetValue, ctx => {
154+
spacetime.anonymousView({ name: 'v5', public: true }, queryRetValue, ctx => {
155155
const _nonIndexedSemijoin = ctx.from.person
156156
.where(row => row.id.eq(5))
157157
// @ts-expect-error person_id is not indexed.

crates/bindings-typescript/src/server/views.ts

Lines changed: 41 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type { OptionAlgebraicType } from '../lib/option';
1010
import type { ParamsObj } from '../lib/reducers';
1111
import { type UntypedSchemaDef } from '../lib/schema';
1212
import {
13+
QueryTypeBuilder,
1314
RowBuilder,
1415
type Infer,
1516
type InferSpacetimeTypeOfTypeBuilder,
@@ -88,7 +89,41 @@ export type ViewOpts = {
8889
public: true;
8990
};
9091

91-
type FlattenedArray<T> = T extends readonly (infer E)[] ? E : never;
92+
type ProceduralViewReturnTypeBuilder =
93+
| TypeBuilder<
94+
readonly object[],
95+
{ tag: 'Array'; value: AlgebraicTypeVariants.Product }
96+
>
97+
| TypeBuilder<
98+
object | undefined,
99+
OptionAlgebraicType<AlgebraicTypeVariants.Product>
100+
>;
101+
102+
export type QueryViewReturnTypeBuilder = QueryTypeBuilder<
103+
TypeBuilder<object, any>
104+
>;
105+
106+
export type ViewReturnTypeBuilder =
107+
| ProceduralViewReturnTypeBuilder
108+
| QueryViewReturnTypeBuilder;
109+
110+
type ExtractProductFromTypeBuilder<T extends TypeBuilder<any, any>> =
111+
InferSpacetimeTypeOfTypeBuilder<T> extends { tag: 'Product'; value: infer P }
112+
? P
113+
: never;
114+
115+
type QueryReturnRow<Ret extends QueryViewReturnTypeBuilder> =
116+
Ret extends QueryTypeBuilder<infer Row> ? Infer<Row> : never;
117+
118+
type QueryReturnProduct<Ret extends QueryViewReturnTypeBuilder> =
119+
Ret extends QueryTypeBuilder<infer Row>
120+
? ExtractProductFromTypeBuilder<Row>
121+
: never;
122+
123+
type ViewReturn<Ret extends ViewReturnTypeBuilder> =
124+
Ret extends QueryViewReturnTypeBuilder
125+
? RowTypedQuery<QueryReturnRow<Ret>, QueryReturnProduct<Ret>>
126+
: Infer<Ret>;
92127

93128
// // If we allowed functions to return either.
94129
// type ViewReturn<Ret extends ViewReturnTypeBuilder> =
@@ -99,33 +134,16 @@ export type ViewFn<
99134
S extends UntypedSchemaDef,
100135
Params extends ParamsObj,
101136
Ret extends ViewReturnTypeBuilder,
102-
> =
103-
| ((ctx: ViewCtx<S>, params: InferTypeOfRow<Params>) => Infer<Ret>)
104-
| ((
105-
ctx: ViewCtx<S>,
106-
params: InferTypeOfRow<Params>
107-
) => RowTypedQuery<FlattenedArray<Infer<Ret>>, ExtractArrayProduct<Ret>>);
137+
> = (ctx: ViewCtx<S>, params: InferTypeOfRow<Params>) => ViewReturn<Ret>;
108138

109139
export type AnonymousViewFn<
110140
S extends UntypedSchemaDef,
111141
Params extends ParamsObj,
112142
Ret extends ViewReturnTypeBuilder,
113-
> =
114-
| ((ctx: AnonymousViewCtx<S>, params: InferTypeOfRow<Params>) => Infer<Ret>)
115-
| ((
116-
ctx: AnonymousViewCtx<S>,
117-
params: InferTypeOfRow<Params>
118-
) => RowTypedQuery<FlattenedArray<Infer<Ret>>, ExtractArrayProduct<Ret>>);
119-
120-
export type ViewReturnTypeBuilder =
121-
| TypeBuilder<
122-
readonly object[],
123-
{ tag: 'Array'; value: AlgebraicTypeVariants.Product }
124-
>
125-
| TypeBuilder<
126-
object | undefined,
127-
OptionAlgebraicType<AlgebraicTypeVariants.Product>
128-
>;
143+
> = (
144+
ctx: AnonymousViewCtx<S>,
145+
params: InferTypeOfRow<Params>
146+
) => ViewReturn<Ret>;
129147

130148
export function registerView<
131149
S extends UntypedSchemaDef,
@@ -202,12 +220,3 @@ type ViewInfo<F> = {
202220

203221
export type Views = ViewInfo<ViewFn<any, any, any>>[];
204222
export type AnonViews = ViewInfo<AnonymousViewFn<any, any, any>>[];
205-
206-
// A helper to get the product type out of a type builder.
207-
// This is only non-never if the type builder is an array.
208-
type ExtractArrayProduct<T extends TypeBuilder<any, any>> =
209-
InferSpacetimeTypeOfTypeBuilder<T> extends { tag: 'Array'; value: infer V }
210-
? V extends { tag: 'Product'; value: infer P }
211-
? P
212-
: never
213-
: never;

crates/bindings-typescript/test-app/src/module_bindings/all_view_pk_players_table.ts

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)