Skip to content

Commit 262a43d

Browse files
authored
feat(zero-client): Normalize schema (rocicorp#2647)
A normalized schema has: - All the `Record`s are sorted. - The `tableName` is validated. - The `primaryKey` is sorted. - The `columns` are validated to contain all the primary keys and that the primary key columns are not optional and not null or undefined. - The recursive table schemas are resolved.
1 parent 44d646a commit 262a43d

File tree

24 files changed

+934
-188
lines changed

24 files changed

+934
-188
lines changed

packages/replicache-perf/src/benchmarks/replicache.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type {
1313
import {assert} from '../../../shared/src/asserts.js';
1414
import {deepEqual} from '../../../shared/src/json.js';
1515
import {randomUint64} from '../../../shared/src/random-uint64.js';
16+
import type {Writable} from '../../../shared/src/writable.js';
1617
import type {Bencher, Benchmark} from '../benchmark.js';
1718
import {
1819
type TestDataObject,
@@ -23,8 +24,6 @@ import {
2324

2425
const valSize = 1024;
2526

26-
type Writable<T> = {-readonly [P in keyof T]: T[P]};
27-
2827
export function benchmarkPopulate(opts: {
2928
numKeys: number;
3029
clean: boolean;
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import {stringCompare} from './string-compare.js';
2+
3+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
4+
export function sortedEntries<T extends Record<string, any>>(
5+
object: T,
6+
): [keyof T & string, T[keyof T]][] {
7+
return Object.entries(object).sort((a, b) => stringCompare(a[0], b[0]));
8+
}

packages/shared/src/tool/vitest-config.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,7 @@ export const config = {
2323
log.includes('REPLICACHE LICENSE NOT VALID') ||
2424
log.includes('enableAnalytics false') ||
2525
log.includes('no such entity') ||
26-
log.includes('TODO: addZQLSubscription') ||
27-
log.includes('TODO: removeZQLSubscription')
26+
log.includes(`Zero starting up with no server URL`)
2827
) {
2928
return false;
3029
}

packages/shared/src/writable.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export type Writable<T> = {
2+
-readonly [P in keyof T]: T[P];
3+
};

packages/zero-cache/src/config/config-query.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type {AST} from '../../../zql/src/zql/ast/ast.js';
22
import type {Format} from '../../../zql/src/zql/ivm/array-view.js';
3+
import type {NormalizedTableSchema} from '../../../zql/src/zql/query/normalize-table-schema.js';
34
import {AbstractQuery} from '../../../zql/src/zql/query/query-impl.js';
45
import type {
56
DefaultQueryResultRow,
@@ -15,7 +16,7 @@ export class ConfigQuery<
1516
TReturn extends QueryType = DefaultQueryResultRow<TTableSchema>,
1617
> extends AbstractQuery<TTableSchema, TReturn> {
1718
constructor(
18-
schema: TTableSchema,
19+
schema: NormalizedTableSchema,
1920
ast?: AST | undefined,
2021
format?: Format | undefined,
2122
) {
@@ -27,7 +28,7 @@ export class ConfigQuery<
2728
}
2829

2930
protected _newQuery<TSchema extends TableSchema, TReturn extends QueryType>(
30-
schema: TSchema,
31+
schema: NormalizedTableSchema,
3132
ast: AST,
3233
format: Format | undefined,
3334
): Query<TSchema, TReturn> {

packages/zero-cache/src/config/define-config.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*/
66
import fs from 'node:fs';
77
import path from 'node:path';
8+
import {normalizeSchema} from '../../../zero-client/src/client/normalized-schema.js';
89
import type {AST} from '../../../zql/src/zql/ast/ast.js';
910
import type {Query, SchemaToRow} from '../../../zql/src/zql/query/query.js';
1011
import type {TableSchema} from '../../../zql/src/zql/query/schema.js';
@@ -76,8 +77,9 @@ export function defineConfig<TAuthDataShape, TSchema extends Schema>(
7677
schema: TSchema,
7778
definer: (queries: Queries<TSchema>) => ZeroConfig<TAuthDataShape, TSchema>,
7879
) {
80+
const normalizedSchema = normalizeSchema(schema);
7981
const queries = {} as Record<string, Query<TableSchema>>;
80-
for (const [name, tableSchema] of Object.entries(schema.tables)) {
82+
for (const [name, tableSchema] of Object.entries(normalizedSchema.tables)) {
8183
queries[name] = new ConfigQuery(tableSchema);
8284
}
8385

packages/zero-client/src/client/crud.ts

Lines changed: 32 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import {promiseVoid} from '../../../shared/src/resolved-promises.js';
22
import type {MaybePromise} from '../../../shared/src/types.js';
3-
import {type PrimaryKeyValueRecord} from '../../../zero-protocol/src/primary-key.js';
3+
import {
4+
type PrimaryKeyValue,
5+
type PrimaryKeyValueRecord,
6+
} from '../../../zero-protocol/src/primary-key.js';
47
import {
58
CRUD_MUTATION_NAME,
69
type CreateOp,
@@ -13,9 +16,10 @@ import {
1316
} from '../../../zero-protocol/src/push.js';
1417
import type {Row} from '../../../zql/src/zql/ivm/data.js';
1518
import type {PrimaryKey} from '../../../zql/src/zql/ivm/schema.js';
19+
import type {NormalizedPrimaryKey} from '../../../zql/src/zql/query/normalize-table-schema.js';
1620
import type {SchemaToRow} from '../../../zql/src/zql/query/query.js';
1721
import {toPrimaryKeyString} from './keys.js';
18-
import {makeIDFromPrimaryKey} from './make-id-from-primary-key.js';
22+
import type {NormalizedSchema} from './normalized-schema.js';
1923
import type {MutatorDefs, WriteTransaction} from './replicache-types.js';
2024
import type {Schema} from './zero.js';
2125

@@ -76,7 +80,7 @@ type ZeroCRUDMutate = {
7680
* `label` properties.
7781
*/
7882
export function makeCRUDMutate<const S extends Schema>(
79-
schema: S,
83+
schema: NormalizedSchema,
8084
repMutate: ZeroCRUDMutate,
8185
): MakeCRUDMutate<S> {
8286
const {[CRUD_MUTATION_NAME]: zeroCRUD} = repMutate;
@@ -122,40 +126,10 @@ export function makeCRUDMutate<const S extends Schema>(
122126
return mutate as MakeCRUDMutate<S>;
123127
}
124128

125-
// type NiceIntersection<S, T> = {[K in keyof (S & T)]: (S & T)[K]};
126-
127-
// type MyRow = {
128-
// id: string;
129-
// n: number;
130-
// b: boolean;
131-
// null: null;
132-
// };
133-
134-
// type XXX = NiceIntersection<
135-
// Pick<MyRow, 'id' | 'n'>,
136-
// Partial<Omit<MyRow, 'id' | 'n'>>
137-
// >
138-
139-
// AsPrimaryKeyValueRecord<{
140-
// [K in PK[number]]: R[K] extends PrimaryKeyValue ? R[K] : never;
141-
// }>;
142-
//Pick<R, PK[number]>;
143-
144-
// const row = {
145-
// id: 'abc',
146-
// n: null,
147-
// b: true,
148-
// null: null,
149-
// };
150-
// const pk = ['id', 'n'] as const;
151-
// export type Test = DeleteID<typeof row, typeof pk>;
152-
153-
// export const id: Test = {id: 'a', n: 456};
154-
155129
/**
156130
* Creates the `{create, set, update, delete}` object for use outside a batch.
157131
*/
158-
function makeEntityCRUDMutate<R extends Row, PK extends PrimaryKey>(
132+
function makeEntityCRUDMutate<R extends Row, PK extends NormalizedPrimaryKey>(
159133
entityType: string,
160134
primaryKey: PK,
161135
zeroCRUD: CRUDMutate,
@@ -207,9 +181,12 @@ function makeEntityCRUDMutate<R extends Row, PK extends PrimaryKey>(
207181
/**
208182
* Creates the `{create, set, update, delete}` object for use inside a batch.
209183
*/
210-
export function makeBatchCRUDMutate<R extends Row, PK extends PrimaryKey>(
184+
export function makeBatchCRUDMutate<
185+
R extends Row,
186+
PK extends NormalizedPrimaryKey,
187+
>(
211188
tableName: string,
212-
schema: Schema,
189+
schema: NormalizedSchema,
213190
ops: CRUDOp[],
214191
): RowCRUDMutate<R, PK> {
215192
const {primaryKey} = schema.tables[tableName];
@@ -267,7 +244,7 @@ export type CRUDMutator = (
267244
crudArg: CRUDMutationArg,
268245
) => Promise<void>;
269246

270-
export function makeCRUDMutator<S extends Schema>(schema: S): CRUDMutator {
247+
export function makeCRUDMutator(schema: NormalizedSchema): CRUDMutator {
271248
return async function zeroCRUDMutator(
272249
tx: WriteTransaction,
273250
crudArg: CRUDMutationArg,
@@ -294,7 +271,7 @@ export function makeCRUDMutator<S extends Schema>(schema: S): CRUDMutator {
294271
async function createImpl(
295272
tx: WriteTransaction,
296273
arg: CreateOp,
297-
schema: Schema,
274+
schema: NormalizedSchema,
298275
): Promise<void> {
299276
const key = toPrimaryKeyString(
300277
arg.entityType,
@@ -306,10 +283,10 @@ async function createImpl(
306283
}
307284
}
308285

309-
export async function setImpl(
286+
async function setImpl(
310287
tx: WriteTransaction,
311288
arg: CreateOp | SetOp,
312-
schema: Schema,
289+
schema: NormalizedSchema,
313290
): Promise<void> {
314291
const key = toPrimaryKeyString(
315292
arg.entityType,
@@ -319,10 +296,10 @@ export async function setImpl(
319296
await tx.set(key, arg.value);
320297
}
321298

322-
export async function updateImpl(
299+
async function updateImpl(
323300
tx: WriteTransaction,
324301
arg: UpdateOp,
325-
schema: Schema,
302+
schema: NormalizedSchema,
326303
): Promise<void> {
327304
const key = toPrimaryKeyString(
328305
arg.entityType,
@@ -338,10 +315,10 @@ export async function updateImpl(
338315
await tx.set(key, next);
339316
}
340317

341-
export async function deleteImpl(
318+
async function deleteImpl(
342319
tx: WriteTransaction,
343320
arg: DeleteOp,
344-
schema: Schema,
321+
schema: NormalizedSchema,
345322
): Promise<void> {
346323
const key = toPrimaryKeyString(
347324
arg.entityType,
@@ -350,3 +327,14 @@ export async function deleteImpl(
350327
);
351328
await tx.del(key);
352329
}
330+
331+
function makeIDFromPrimaryKey(
332+
primaryKey: NormalizedPrimaryKey,
333+
id: PrimaryKeyValueRecord,
334+
): PrimaryKeyValueRecord {
335+
const rv: Record<string, PrimaryKeyValue> = {};
336+
for (const key of primaryKey) {
337+
rv[key] = id[key];
338+
}
339+
return rv;
340+
}

packages/zero-client/src/client/keys.test.ts

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,25 @@
11
import fc from 'fast-check';
22
import {expect, test} from 'vitest';
3-
import type {PrimaryKey} from '../../../zero-protocol/src/primary-key.js';
4-
import {toPrimaryKeyString} from './keys.js';
3+
import type {
4+
PrimaryKey,
5+
PrimaryKeyValueRecord,
6+
} from '../../../zero-protocol/src/primary-key.js';
7+
import {normalizePrimaryKey} from '../../../zql/src/zql/query/normalize-table-schema.js';
8+
import {toPrimaryKeyString as toPrimaryKeyStringImpl} from './keys.js';
59

610
test('toPrimaryKeyString', () => {
11+
function toPrimaryKeyString(
12+
tableName: string,
13+
primaryKey: PrimaryKey,
14+
id: PrimaryKeyValueRecord,
15+
) {
16+
return toPrimaryKeyStringImpl(
17+
tableName,
18+
normalizePrimaryKey(primaryKey),
19+
id,
20+
);
21+
}
22+
723
expect(
824
toPrimaryKeyString('issue', ['id'], {id: 'issue1'}),
925
).toMatchInlineSnapshot(`"e/issue/issue1"`);
@@ -90,8 +106,16 @@ test('no clashes - single pk', () => {
90106
fc.tuple(fc.boolean(), fc.boolean()),
91107
),
92108
([a, b]) => {
93-
const keyA = toPrimaryKeyString('issue', ['id'], {id: a});
94-
const keyB = toPrimaryKeyString('issue', ['id'], {id: b});
109+
const keyA = toPrimaryKeyStringImpl(
110+
'issue',
111+
normalizePrimaryKey(['id']),
112+
{id: a},
113+
);
114+
const keyB = toPrimaryKeyStringImpl(
115+
'issue',
116+
normalizePrimaryKey(['id']),
117+
{id: b},
118+
);
95119
if (a === b) {
96120
expect(keyA).toBe(keyB);
97121
} else {
@@ -103,7 +127,7 @@ test('no clashes - single pk', () => {
103127
});
104128

105129
test('no clashes - multiple pk', () => {
106-
const primaryKey: PrimaryKey = ['id', 'name'];
130+
const primaryKey = normalizePrimaryKey(['id', 'name']);
107131
fc.assert(
108132
fc.property(
109133
fc.tuple(
@@ -113,11 +137,11 @@ test('no clashes - multiple pk', () => {
113137
fc.oneof(fc.string(), fc.double(), fc.boolean()),
114138
),
115139
([a1, a2, b1, b2]) => {
116-
const keyA = toPrimaryKeyString('issue', primaryKey, {
140+
const keyA = toPrimaryKeyStringImpl('issue', primaryKey, {
117141
id: a1,
118142
name: a2,
119143
});
120-
const keyB = toPrimaryKeyString('issue', primaryKey, {
144+
const keyB = toPrimaryKeyStringImpl('issue', primaryKey, {
121145
id: b1,
122146
name: b2,
123147
});

packages/zero-client/src/client/keys.ts

Lines changed: 5 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import {h64WithReverse} from '../../../shared/src/h64-with-reverse.js';
2-
import type {
3-
PrimaryKey,
4-
PrimaryKeyValueRecord,
5-
} from '../../../zero-protocol/src/primary-key.js';
2+
import type {PrimaryKeyValueRecord} from '../../../zero-protocol/src/primary-key.js';
3+
import type {NormalizedPrimaryKey} from '../../../zql/src/zql/query/normalize-table-schema.js';
64

75
export const CLIENTS_KEY_PREFIX = 'c/';
86
export const DESIRED_QUERIES_KEY_PREFIX = 'd/';
@@ -25,32 +23,17 @@ export function toGotQueriesKey(hash: string): string {
2523
return GOT_QUERIES_KEY_PREFIX + hash;
2624
}
2725

28-
/**
29-
* This returns a new array if the array is not already sorted.
30-
*/
31-
function maybeSort<T>(arr: readonly T[]): readonly T[] {
32-
for (let i = 1; i < arr.length; i++) {
33-
if (arr[i] < arr[i - 1]) {
34-
return [...arr].sort();
35-
}
36-
}
37-
return arr;
38-
}
39-
4026
export function toPrimaryKeyString(
4127
tableName: string,
42-
primaryKey: PrimaryKey,
28+
primaryKey: NormalizedPrimaryKey,
4329
id: PrimaryKeyValueRecord,
4430
): string {
4531
if (primaryKey.length === 1) {
4632
return ENTITIES_KEY_PREFIX + tableName + '/' + id[primaryKey[0]];
4733
}
4834

49-
// TODO: Assert that PrimaryKey is always sorted at a higher level.
50-
const sorted = maybeSort(primaryKey);
51-
52-
const arr = sorted.map(k => id[k]);
53-
const str = JSON.stringify(arr);
35+
const values = primaryKey.map(k => id[k]);
36+
const str = JSON.stringify(values);
5437

5538
const idSegment = h64WithReverse(str);
5639
return ENTITIES_KEY_PREFIX + tableName + '/' + idSegment;

packages/zero-client/src/client/make-id-from-primary-key.ts

Lines changed: 0 additions & 19 deletions
This file was deleted.

0 commit comments

Comments
 (0)