Skip to content

Commit 400252f

Browse files
authored
Fix for multi-column indexes in typescript modules (#3589)
# Description of Changes This updates a bunch of types to make sure we generate the right functions for indexes with multiple columns. Most of the changes are making the array of column names readonly. This also has a fix for `AllUnique`, because we were treating composite indexes as unique when the first field had a unique index. FWIW, we should also make sure that the range scanning functions still exist in that case, but I didn't try to add that here. # API and ABI breaking changes Any change to these types is probably technically a breaking API change, but no one should consider these types stable. # Expected complexity level and risk 1.5. I'm still not super confident with some of these types, but this shouldn't change any runtime behavior. # Testing I added some uses of indexes in `schema.test-d.ts`, so `pnpm run build:types` would throw errors if this were broken.
1 parent 75c6e67 commit 400252f

File tree

4 files changed

+117
-36
lines changed

4 files changed

+117
-36
lines changed

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

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,15 @@ import type { ColumnMetadata } from './type_builders';
66
*/
77
export type AllUnique<
88
TableDef extends UntypedTableDef,
9-
Columns extends Array<keyof TableDef['columns']>,
10-
> = {
11-
[i in keyof Columns]: ColumnIsUnique<
12-
TableDef['columns'][Columns[i]]['columnMetadata']
13-
>;
14-
} extends true[]
15-
? true
16-
: false;
9+
Columns extends ReadonlyArray<keyof TableDef['columns']>,
10+
> = Columns extends readonly [
11+
infer Head extends keyof TableDef['columns'],
12+
...infer Tail extends ReadonlyArray<keyof TableDef['columns']>,
13+
]
14+
? ColumnIsUnique<TableDef['columns'][Head]['columnMetadata']> extends true
15+
? AllUnique<TableDef, Tail>
16+
: false
17+
: true;
1718

1819
/**
1920
* A helper type to determine if a column is unique based on its metadata.

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

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,22 +18,22 @@ export type IndexOpts<AllowedCol extends string> = {
1818
/**
1919
* An untyped representation of an index definition.
2020
*/
21-
type UntypedIndex<AllowedCol extends string> = {
21+
export type UntypedIndex<AllowedCol extends string> = {
2222
name: string;
2323
unique: boolean;
2424
algorithm: 'btree' | 'direct';
25-
columns: AllowedCol[];
25+
columns: readonly AllowedCol[];
2626
};
2727

2828
/**
2929
* A helper type to extract the column names from an index definition.
3030
*/
3131
export type IndexColumns<I extends IndexOpts<any>> = I extends {
32-
columns: string[];
32+
columns: readonly string[];
3333
}
34-
? I['columns']
35-
: I extends { column: string }
36-
? [I['column']]
34+
? readonly [...I['columns']]
35+
: I extends { column: infer Name extends string }
36+
? readonly [Name]
3737
: never;
3838

3939
/**
@@ -95,9 +95,18 @@ export type IndexVal<
9595
/**
9696
* A helper type to extract the types of the columns that make up an index.
9797
*/
98-
type _IndexVal<TableDef extends UntypedTableDef, Columns extends string[]> = {
99-
[i in keyof Columns]: TableDef['columns'][Columns[i]]['typeBuilder']['type'];
100-
};
98+
type _IndexVal<
99+
TableDef extends UntypedTableDef,
100+
Columns extends readonly string[],
101+
> = Columns extends readonly [
102+
infer Head extends string,
103+
...infer Tail extends readonly string[],
104+
]
105+
? [
106+
TableDef['columns'][Head]['typeBuilder']['type'],
107+
..._IndexVal<TableDef, Tail>,
108+
]
109+
: [];
101110

102111
/**
103112
* A helper type to define the bounds for scanning an index.
@@ -115,7 +124,9 @@ export type IndexScanRangeBounds<
115124
* It supports omitting trailing columns if the index is multi-column.
116125
* This version only allows omitting the array if the index is single-column to avoid ambiguity.
117126
*/
118-
type _IndexScanRangeBounds<Columns extends any[]> = Columns extends [infer Term]
127+
type _IndexScanRangeBounds<Columns extends readonly any[]> = Columns extends [
128+
infer Term,
129+
]
119130
? Term | Range<Term>
120131
: _IndexScanRangeBoundsCase<Columns>;
121132

@@ -124,12 +135,10 @@ type _IndexScanRangeBounds<Columns extends any[]> = Columns extends [infer Term]
124135
* This type allows for specifying exact values or ranges for each column in the index.
125136
* It supports omitting trailing columns if the index is multi-column.
126137
*/
127-
type _IndexScanRangeBoundsCase<Columns extends any[]> = Columns extends [
128-
...infer Prefix,
129-
infer Term,
130-
]
131-
? [...Prefix, Term | Range<Term>] | _IndexScanRangeBounds<Prefix>
132-
: never;
138+
type _IndexScanRangeBoundsCase<Columns extends readonly any[]> =
139+
Columns extends [...infer Prefix, infer Term]
140+
? readonly [...Prefix, Term | Range<Term>] | _IndexScanRangeBounds<Prefix>
141+
: never;
133142

134143
/**
135144
* A helper type representing a column index definition.
@@ -141,7 +150,7 @@ export type ColumnIndex<
141150
{
142151
name: Name;
143152
unique: ColumnIsUnique<M>;
144-
columns: [Name];
153+
columns: readonly [Name];
145154
algorithm: 'btree' | 'direct';
146155
} & (M extends {
147156
indexType: infer I extends NonNullable<IndexTypes>;
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { schema } from './schema';
2+
import { table } from './table';
3+
import t from './type_builders';
4+
5+
const person = table(
6+
{
7+
name: 'person',
8+
indexes: [
9+
{
10+
name: 'id_name_idx',
11+
algorithm: 'btree',
12+
columns: ['id', 'name'] as const,
13+
},
14+
{
15+
name: 'id_name2_idx',
16+
algorithm: 'btree',
17+
columns: ['id', 'name2'] as const,
18+
},
19+
{
20+
name: 'name_idx',
21+
algorithm: 'btree',
22+
columns: ['name'] as const,
23+
},
24+
],
25+
},
26+
{
27+
id: t.u32().primaryKey(),
28+
name: t.string(),
29+
name2: t.string().unique(),
30+
married: t.bool(),
31+
id2: t.identity(),
32+
age: t.u32(),
33+
age2: t.u16(),
34+
}
35+
);
36+
37+
const spacetimedb = schema(person);
38+
39+
spacetimedb.init(ctx => {
40+
ctx.db.person.id_name_idx.filter(1);
41+
ctx.db.person.id_name_idx.filter([1, 'aname']);
42+
// ctx.db.person.id_name2_idx.find
43+
44+
// @ts-expect-error id2 is not indexed, so this should not exist at all.
45+
const _id2 = ctx.db.person.id2;
46+
47+
ctx.db.person.id.find(2);
48+
});

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

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -54,26 +54,49 @@ type CoerceArray<X extends IndexOpts<any>[]> = X;
5454
export type UntypedTableDef = {
5555
name: string;
5656
columns: Record<string, ColumnBuilder<any, any, ColumnMetadata<any>>>;
57-
indexes: IndexOpts<any>[];
57+
indexes: readonly IndexOpts<any>[];
5858
};
5959

6060
/**
6161
* A type representing the indexes defined on a table.
6262
*/
6363
export type TableIndexes<TableDef extends UntypedTableDef> = {
64-
[k in keyof TableDef['columns'] & string]: ColumnIndex<
65-
k,
66-
TableDef['columns'][k]['columnMetadata']
67-
>;
64+
[K in keyof TableDef['columns'] & string as ColumnIndex<
65+
K,
66+
TableDef['columns'][K]['columnMetadata']
67+
> extends never
68+
? never
69+
: K]: ColumnIndex<K, TableDef['columns'][K]['columnMetadata']>;
6870
} & {
69-
[I in TableDef['indexes'][number] as I['name'] & {}]: {
70-
name: I['name'];
71-
unique: AllUnique<TableDef, IndexColumns<I>>;
72-
algorithm: Lowercase<I['algorithm']>;
73-
columns: IndexColumns<I>;
74-
};
71+
[I in TableDef['indexes'][number] as I['name'] & {}]: TableIndexFromDef<
72+
TableDef,
73+
I
74+
>;
7575
};
7676

77+
type TableIndexFromDef<
78+
TableDef extends UntypedTableDef,
79+
I extends IndexOpts<keyof TableDef['columns'] & string>,
80+
> =
81+
NormalizeIndexColumns<TableDef, I> extends infer Cols extends ReadonlyArray<
82+
keyof TableDef['columns'] & string
83+
>
84+
? {
85+
name: I['name'];
86+
unique: AllUnique<TableDef, Cols>;
87+
algorithm: Lowercase<I['algorithm']>;
88+
columns: Cols;
89+
}
90+
: never;
91+
92+
type NormalizeIndexColumns<
93+
TableDef extends UntypedTableDef,
94+
I extends IndexOpts<keyof TableDef['columns'] & string>,
95+
> =
96+
IndexColumns<I> extends ReadonlyArray<keyof TableDef['columns'] & string>
97+
? IndexColumns<I>
98+
: never;
99+
77100
/**
78101
* Options for configuring a database table.
79102
* - `name`: The name of the table.

0 commit comments

Comments
 (0)