Skip to content

Commit 8637d15

Browse files
committed
Refactored relationship validation to include type validation and partial matches
1 parent ee1fa6f commit 8637d15

21 files changed

+3593
-2276
lines changed

plan.md

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

src/packages/dumbo/src/core/schema/components/columnSchemaComponent.ts

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,23 @@ import {
66
} from '../schemaComponent';
77

88
export type ColumnURNType = 'sc:dumbo:column';
9-
export type ColumnURN = `${ColumnURNType}:${string}`;
9+
export type ColumnURN<ColumnName extends string = string> =
10+
`${ColumnURNType}:${ColumnName}`;
1011

1112
export const ColumnURNType: ColumnURNType = 'sc:dumbo:column';
12-
export const ColumnURN = ({ name }: { name: string }): ColumnURN =>
13-
`${ColumnURNType}:${name}`;
13+
export const ColumnURN = <ColumnName extends string = string>({
14+
name,
15+
}: {
16+
name: ColumnName;
17+
}): ColumnURN<ColumnName> => `${ColumnURNType}:${name}`;
1418

1519
export type ColumnSchemaComponent<
1620
ColumnType extends AnyColumnTypeToken | string = AnyColumnTypeToken | string,
21+
ColumnName extends string = string,
1722
> = SchemaComponent<
18-
ColumnURN,
23+
ColumnURN<ColumnName>,
1924
Readonly<{
20-
columnName: string;
25+
columnName: ColumnName;
2126
}>
2227
> &
2328
SQLColumnToken<ColumnType>;
@@ -31,14 +36,17 @@ export type ColumnSchemaComponentOptions<
3136
SchemaComponentOptions;
3237

3338
export const columnSchemaComponent = <
34-
ColumnType extends AnyColumnTypeToken | string = AnyColumnTypeToken | string,
35-
TOptions extends
39+
const ColumnType extends AnyColumnTypeToken | string =
40+
| AnyColumnTypeToken
41+
| string,
42+
const TOptions extends
3643
ColumnSchemaComponentOptions<ColumnType> = ColumnSchemaComponentOptions<ColumnType>,
44+
const ColumnName extends string = string,
3745
>(
3846
params: {
39-
columnName: string;
47+
columnName: ColumnName;
4048
} & TOptions,
41-
): ColumnSchemaComponent<ColumnType> &
49+
): ColumnSchemaComponent<ColumnType, ColumnName> &
4250
(TOptions extends { notNull: true } | { primaryKey: true }
4351
? { notNull: true }
4452
: { notNull?: false }) => {
@@ -66,7 +74,7 @@ export const columnSchemaComponent = <
6674
type,
6775
};
6876

69-
return result as ColumnSchemaComponent<ColumnType> &
77+
return result as ColumnSchemaComponent<ColumnType, ColumnName> &
7078
(TOptions extends { notNull: true } | { primaryKey: true }
7179
? { notNull: true }
7280
: { notNull?: false });

src/packages/dumbo/src/core/schema/components/databaseSchemaSchemaComponent.ts

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,45 +12,51 @@ import {
1212
} from './tableSchemaComponent';
1313

1414
export type DatabaseSchemaURNType = 'sc:dumbo:database_schema';
15-
export type DatabaseSchemaURN = `${DatabaseSchemaURNType}:${string}`;
15+
export type DatabaseSchemaURN<SchemaName extends string = string> =
16+
`${DatabaseSchemaURNType}:${SchemaName}`;
1617

1718
export const DatabaseSchemaURNType: DatabaseSchemaURNType =
1819
'sc:dumbo:database_schema';
19-
export const DatabaseSchemaURN = ({
20+
export const DatabaseSchemaURN = <SchemaName extends string = string>({
2021
name,
2122
}: {
22-
name: string;
23-
}): DatabaseSchemaURN => `${DatabaseSchemaURNType}:${name}`;
23+
name: SchemaName;
24+
}): DatabaseSchemaURN<SchemaName> => `${DatabaseSchemaURNType}:${name}`;
2425

2526
export type DatabaseSchemaTables<
2627
Tables extends AnyTableSchemaComponent = AnyTableSchemaComponent,
2728
> = Record<string, Tables>;
2829

2930
export type DatabaseSchemaSchemaComponent<
3031
Tables extends DatabaseSchemaTables = DatabaseSchemaTables,
32+
SchemaName extends string = string,
3133
> = SchemaComponent<
32-
DatabaseSchemaURN,
34+
DatabaseSchemaURN<SchemaName>,
3335
Readonly<{
34-
schemaName: string;
36+
schemaName: SchemaName;
3537
tables: ReadonlyMap<string, TableSchemaComponent> & Tables;
3638
addTable: (table: string | TableSchemaComponent) => TableSchemaComponent;
3739
}>
3840
>;
3941

4042
export type AnyDatabaseSchemaSchemaComponent =
4143
// eslint-disable-next-line @typescript-eslint/no-explicit-any
42-
DatabaseSchemaSchemaComponent<any>;
44+
DatabaseSchemaSchemaComponent<any, any>;
4345

4446
export const databaseSchemaSchemaComponent = <
45-
Tables extends DatabaseSchemaTables = DatabaseSchemaTables,
47+
const Tables extends DatabaseSchemaTables = DatabaseSchemaTables,
48+
const SchemaName extends string = string,
4649
>({
4750
schemaName,
4851
tables,
4952
...migrationsOrComponents
5053
}: {
51-
schemaName: string;
54+
schemaName: SchemaName;
5255
tables?: Tables;
53-
} & SchemaComponentOptions): DatabaseSchemaSchemaComponent<Tables> => {
56+
} & SchemaComponentOptions): DatabaseSchemaSchemaComponent<
57+
Tables,
58+
SchemaName
59+
> => {
5460
const base = schemaComponent(DatabaseSchemaURN({ name: schemaName }), {
5561
migrations: migrationsOrComponents.migrations ?? [],
5662
components: [
Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
import { describe, it } from 'node:test';
2+
import { SQL } from '../../../sql';
3+
import type { Equals, Expect } from '../../../testing';
4+
import { dumboSchema } from '../../dumboSchema';
5+
import type { SchemaColumnName } from './relationshipTypes';
6+
import type {
7+
CollectReferencesErrors,
8+
ColumnReferenceExistanceError,
9+
ColumnReferenceTypeMismatchError,
10+
} from './relationshipValidation';
11+
12+
const { column, table, schema } = dumboSchema;
13+
const { BigInteger, Varchar, Integer } = SQL.column.type;
14+
15+
void describe('CollectReferencesErrors', () => {
16+
const usersTable = table('users', {
17+
columns: {
18+
id: column('id', BigInteger),
19+
name: column('name', Varchar('max')),
20+
age: column('age', Integer),
21+
},
22+
});
23+
24+
const postsTable = table('posts', {
25+
columns: {
26+
id: column('id', BigInteger),
27+
user_id: column('user_id', BigInteger),
28+
title: column('title', Varchar('max')),
29+
},
30+
});
31+
32+
const _publicSchema = schema('public', {
33+
users: usersTable,
34+
posts: postsTable,
35+
});
36+
37+
type TestSchemas = {
38+
public: typeof _publicSchema;
39+
};
40+
41+
void it('returns empty array when all references are valid', () => {
42+
type Columns = readonly [SchemaColumnName<'public', 'posts', 'user_id'>];
43+
type References = readonly [SchemaColumnName<'public', 'users', 'id'>];
44+
45+
type Result = CollectReferencesErrors<
46+
Columns,
47+
References,
48+
'public',
49+
'posts',
50+
TestSchemas
51+
>;
52+
53+
type _Then = Expect<Equals<Result, []>>;
54+
});
55+
56+
void it('returns empty array for multiple valid references', () => {
57+
type Columns = readonly [
58+
SchemaColumnName<'public', 'posts', 'user_id'>,
59+
SchemaColumnName<'public', 'posts', 'title'>,
60+
];
61+
type References = readonly [
62+
SchemaColumnName<'public', 'users', 'id'>,
63+
SchemaColumnName<'public', 'users', 'name'>,
64+
];
65+
66+
type Result = CollectReferencesErrors<
67+
Columns,
68+
References,
69+
'public',
70+
'posts',
71+
TestSchemas
72+
>;
73+
74+
type _Then = Expect<Equals<Result, []>>;
75+
});
76+
77+
void it('collects error for missing schema', () => {
78+
type Columns = readonly [SchemaColumnName<'public', 'posts', 'user_id'>];
79+
type References = readonly [SchemaColumnName<'nonexistent', 'users', 'id'>];
80+
81+
type Result = CollectReferencesErrors<
82+
Columns,
83+
References,
84+
'public',
85+
'posts',
86+
TestSchemas
87+
>;
88+
89+
type Expected = [
90+
ColumnReferenceExistanceError<'missing_schema', 'nonexistent.users.id'>,
91+
];
92+
93+
type _Then = Expect<Equals<Result, Expected>>;
94+
});
95+
96+
void it('collects error for missing table', () => {
97+
type Columns = readonly [SchemaColumnName<'public', 'posts', 'user_id'>];
98+
type References = readonly [
99+
SchemaColumnName<'public', 'nonexistent', 'id'>,
100+
];
101+
102+
type Result = CollectReferencesErrors<
103+
Columns,
104+
References,
105+
'public',
106+
'posts',
107+
TestSchemas
108+
>;
109+
110+
type Expected = [
111+
ColumnReferenceExistanceError<'missing_table', 'public.nonexistent.id'>,
112+
];
113+
114+
type _Then = Expect<Equals<Result, Expected>>;
115+
});
116+
117+
void it('collects error for missing column', () => {
118+
type Columns = readonly [SchemaColumnName<'public', 'posts', 'user_id'>];
119+
type References = readonly [
120+
SchemaColumnName<'public', 'users', 'nonexistent'>,
121+
];
122+
123+
type Result = CollectReferencesErrors<
124+
Columns,
125+
References,
126+
'public',
127+
'posts',
128+
TestSchemas
129+
>;
130+
131+
type Expected = [
132+
ColumnReferenceExistanceError<
133+
'missing_column',
134+
'public.users.nonexistent'
135+
>,
136+
];
137+
138+
type _Then = Expect<Equals<Result, Expected>>;
139+
});
140+
141+
void it('collects error for type mismatch', () => {
142+
type Columns = readonly [SchemaColumnName<'public', 'posts', 'user_id'>];
143+
type References = readonly [SchemaColumnName<'public', 'users', 'name'>];
144+
145+
type Result = CollectReferencesErrors<
146+
Columns,
147+
References,
148+
'public',
149+
'posts',
150+
TestSchemas
151+
>;
152+
153+
type Expected = [
154+
ColumnReferenceTypeMismatchError<
155+
'public.users.name',
156+
'VARCHAR',
157+
'BIGINT'
158+
>,
159+
];
160+
161+
type _Then = Expect<Equals<Result, Expected>>;
162+
});
163+
164+
void it('collects multiple errors for different invalid references', () => {
165+
type Columns = readonly [
166+
SchemaColumnName<'public', 'posts', 'user_id'>,
167+
SchemaColumnName<'public', 'posts', 'title'>,
168+
];
169+
type References = readonly [
170+
SchemaColumnName<'public', 'users', 'nonexistent'>,
171+
SchemaColumnName<'public', 'users', 'age'>,
172+
];
173+
174+
type Result = CollectReferencesErrors<
175+
Columns,
176+
References,
177+
'public',
178+
'posts',
179+
TestSchemas
180+
>;
181+
182+
type Expected = [
183+
ColumnReferenceExistanceError<
184+
'missing_column',
185+
'public.users.nonexistent'
186+
>,
187+
ColumnReferenceTypeMismatchError<
188+
'public.users.age',
189+
'INTEGER',
190+
'VARCHAR'
191+
>,
192+
];
193+
194+
type _Then = Expect<Equals<Result, Expected>>;
195+
});
196+
197+
void it('collects only errors, skipping valid references', () => {
198+
type Columns = readonly [
199+
SchemaColumnName<'public', 'posts', 'user_id'>,
200+
SchemaColumnName<'public', 'posts', 'title'>,
201+
SchemaColumnName<'public', 'posts', 'id'>,
202+
];
203+
type References = readonly [
204+
SchemaColumnName<'public', 'users', 'id'>,
205+
SchemaColumnName<'public', 'users', 'age'>,
206+
SchemaColumnName<'public', 'users', 'id'>,
207+
];
208+
209+
type Result = CollectReferencesErrors<
210+
Columns,
211+
References,
212+
'public',
213+
'posts',
214+
TestSchemas
215+
>;
216+
217+
type Expected = [
218+
ColumnReferenceTypeMismatchError<
219+
'public.users.age',
220+
'INTEGER',
221+
'VARCHAR'
222+
>,
223+
];
224+
225+
type _Then = Expect<Equals<Result, Expected>>;
226+
});
227+
228+
void it('returns empty array for empty input tuples', () => {
229+
type Columns = readonly [];
230+
type References = readonly [];
231+
232+
type Result = CollectReferencesErrors<
233+
Columns,
234+
References,
235+
'public',
236+
'posts',
237+
TestSchemas
238+
>;
239+
240+
type _Then = Expect<Equals<Result, []>>;
241+
});
242+
243+
void it('accumulates errors with pre-existing errors', () => {
244+
type Columns = readonly [SchemaColumnName<'public', 'posts', 'user_id'>];
245+
type References = readonly [
246+
SchemaColumnName<'public', 'users', 'nonexistent'>,
247+
];
248+
type ExistingError = ColumnReferenceExistanceError<
249+
'missing_column',
250+
'public.foo.bar'
251+
>;
252+
253+
type Result = CollectReferencesErrors<
254+
Columns,
255+
References,
256+
'public',
257+
'posts',
258+
TestSchemas,
259+
[ExistingError]
260+
>;
261+
262+
type Expected = [
263+
ExistingError,
264+
ColumnReferenceExistanceError<
265+
'missing_column',
266+
'public.users.nonexistent'
267+
>,
268+
];
269+
270+
type _Then = Expect<Equals<Result, Expected>>;
271+
});
272+
});

0 commit comments

Comments
 (0)