Skip to content

Commit 7ecbe4c

Browse files
authored
postgres array type support (#9)
1 parent 9896d69 commit 7ecbe4c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+684
-220
lines changed

TODO.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
- [ ] Array update
4646
- [x] Upsert
4747
- [x] Delete
48-
- [ ] Aggregation
48+
- [x] Aggregation
4949
- [x] Count
5050
- [x] Aggregate
5151
- [x] Group by
@@ -62,6 +62,8 @@
6262
- [ ] Error system
6363
- [x] Custom table name
6464
- [x] Custom field name
65+
- [ ] Empty AND/OR/NOT behavior
66+
- [ ] Strict undefined check
6567
- [ ] Access Policy
6668
- [ ] Short-circuit pre-create check for scalar-field only policies
6769
- [ ] Polymorphism

packages/cli/src/actions/generate.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,10 @@ export async function run(options: Options) {
3838
console.log(`You can now create a ZenStack client with it.
3939
4040
\`\`\`
41-
import { createClient } from '@zenstackhq/runtime';
41+
import { ZenStackClient } from '@zenstackhq/runtime';
4242
import { schema } from '${outputPath}/schema';
4343
44-
const db = createClient(schema);
44+
const db = new ZenStackClient(schema);
4545
\`\`\`
4646
`);
4747
}

packages/language/src/validators/datamodel-validator.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,23 @@ import {
44
type DiagnosticInfo,
55
type ValidationAcceptor,
66
} from 'langium';
7+
import { IssueCodes, SCALAR_TYPES } from '../constants';
78
import {
89
ArrayExpr,
910
DataModel,
1011
DataModelField,
12+
Model,
1113
ReferenceExpr,
1214
isDataModel,
15+
isDataSource,
1316
isEnum,
17+
isModel,
1418
isStringLiteral,
1519
isTypeDef,
1620
} from '../generated/ast';
1721
import {
1822
findUpInheritance,
23+
getLiteral,
1924
getModelFieldsWithBases,
2025
getModelIdFields,
2126
getModelUniqueFields,
@@ -25,7 +30,6 @@ import {
2530
} from '../utils';
2631
import { validateAttributeApplication } from './attribute-application-validator';
2732
import { validateDuplicatedDeclarations, type AstValidator } from './common';
28-
import { IssueCodes, SCALAR_TYPES } from '../constants';
2933

3034
/**
3135
* Validates data model declarations.
@@ -147,6 +151,19 @@ export default class DataModelValidator implements AstValidator<DataModel> {
147151
);
148152
}
149153

154+
if (field.type.array && !isDataModel(field.type.reference?.ref)) {
155+
const provider = this.getDataSourceProvider(
156+
AstUtils.getContainerOfType(field, isModel)!
157+
);
158+
if (provider === 'sqlite') {
159+
accept(
160+
'error',
161+
`Array type is not supported for "${provider}" provider.`,
162+
{ node: field.type }
163+
);
164+
}
165+
}
166+
150167
field.attributes.forEach((attr) =>
151168
validateAttributeApplication(attr, accept)
152169
);
@@ -162,6 +179,18 @@ export default class DataModelValidator implements AstValidator<DataModel> {
162179
}
163180
}
164181

182+
private getDataSourceProvider(model: Model) {
183+
const dataSource = model.declarations.find(isDataSource);
184+
if (!dataSource) {
185+
return undefined;
186+
}
187+
const provider = dataSource?.fields.find((f) => f.name === 'provider');
188+
if (!provider) {
189+
return undefined;
190+
}
191+
return getLiteral<string>(provider.value);
192+
}
193+
165194
private validateAttributes(dm: DataModel, accept: ValidationAcceptor) {
166195
dm.attributes.forEach((attr) =>
167196
validateAttributeApplication(attr, accept)

packages/runtime/package.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,26 @@
5757
"default": "./dist/plugins/policy.cjs"
5858
}
5959
},
60+
"./utils/pg-utils": {
61+
"import": {
62+
"types": "./dist/utils/pg-utils.d.ts",
63+
"default": "./dist/utils/pg-utils.js"
64+
},
65+
"require": {
66+
"types": "./dist/utils/pg-utils.d.cts",
67+
"default": "./dist/utils/pg-utils.cjs"
68+
}
69+
},
70+
"./utils/sqlite-utils": {
71+
"import": {
72+
"types": "./dist/utils/sqlite-utils.d.ts",
73+
"default": "./dist/utils/sqlite-utils.js"
74+
},
75+
"require": {
76+
"types": "./dist/utils/sqlite-utils.d.cts",
77+
"default": "./dist/utils/sqlite-utils.cjs"
78+
}
79+
},
6080
"./package.json": {
6181
"import": "./package.json",
6282
"require": "./package.json"
@@ -67,6 +87,7 @@
6787
"decimal.js": "^10.4.3",
6888
"kysely": "^0.27.5",
6989
"nanoid": "^5.0.9",
90+
"pg-connection-string": "^2.9.0",
7091
"tiny-invariant": "^1.3.3",
7192
"ts-pattern": "^5.6.0",
7293
"ulid": "^3.0.0",

packages/runtime/src/client/crud/dialects/base.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1023,6 +1023,8 @@ export abstract class BaseCrudDialect<Schema extends SchemaDef> {
10231023
array: Expression<unknown>
10241024
): ExpressionWrapper<any, any, number>;
10251025

1026+
abstract buildArrayLiteralSQL(values: unknown[]): string;
1027+
10261028
get supportsUpdateWithLimit() {
10271029
return true;
10281030
}

packages/runtime/src/client/crud/dialects/postgresql.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,4 +335,10 @@ export class PostgresCrudDialect<
335335
): ExpressionWrapper<any, any, number> {
336336
return eb.fn('array_length', [array]);
337337
}
338+
339+
override buildArrayLiteralSQL(values: unknown[]): string {
340+
return `ARRAY[${values.map((v) =>
341+
typeof v === 'string' ? `'${v}'` : v
342+
)}]`;
343+
}
338344
}

packages/runtime/src/client/crud/dialects/sqlite.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,4 +289,8 @@ export class SqliteCrudDialect<
289289
): ExpressionWrapper<any, any, number> {
290290
return eb.fn('json_array_length', [array]);
291291
}
292+
293+
override buildArrayLiteralSQL(_values: unknown[]): string {
294+
throw new Error('SQLite does not support array literals');
295+
}
292296
}

packages/runtime/src/client/crud/validator.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -730,6 +730,10 @@ export class InputValidator<Schema extends SchemaDef> {
730730
fieldDef.type
731731
);
732732

733+
if (fieldDef.array) {
734+
fieldSchema = z.array(fieldSchema).optional();
735+
}
736+
733737
if (fieldDef.optional || fieldHasDefaultValue(fieldDef)) {
734738
fieldSchema = fieldSchema.optional();
735739
}
@@ -1200,7 +1204,9 @@ export class InputValidator<Schema extends SchemaDef> {
12001204
const bys = typeof value.by === 'string' ? [value.by] : value.by;
12011205
if (
12021206
value.having &&
1203-
Object.keys(value.having).some((key) => !bys.includes(key))
1207+
Object.keys(value.having)
1208+
.filter((f) => !f.startsWith('_'))
1209+
.some((key) => !bys.includes(key))
12041210
) {
12051211
return false;
12061212
} else {
@@ -1212,7 +1218,9 @@ export class InputValidator<Schema extends SchemaDef> {
12121218
const bys = typeof value.by === 'string' ? [value.by] : value.by;
12131219
if (
12141220
value.orderBy &&
1215-
Object.keys(value.orderBy).some((key) => !bys.includes(key))
1221+
Object.keys(value.orderBy)
1222+
.filter((f) => !f.startsWith('_'))
1223+
.some((key) => !bys.includes(key))
12161224
) {
12171225
return false;
12181226
} else {

packages/runtime/src/client/executor/name-mapper.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,6 @@ export class QueryNameMapper extends OperationNodeTransformer {
166166
for (const selection of selections) {
167167
let selectAllFromModel: string | undefined = undefined;
168168
let isSelectAll = false;
169-
let selectAllWithAlias = false;
170169

171170
if (SelectAllNode.is(selection.selection)) {
172171
selectAllFromModel = this.currentModel;
@@ -179,7 +178,6 @@ export class QueryNameMapper extends OperationNodeTransformer {
179178
selection.selection.table?.table.identifier.name ??
180179
this.currentModel;
181180
isSelectAll = true;
182-
selectAllWithAlias = true;
183181
}
184182

185183
if (isSelectAll) {
@@ -190,11 +188,17 @@ export class QueryNameMapper extends OperationNodeTransformer {
190188
contextNode,
191189
selectAllFromModel
192190
);
191+
const fromModelDef = requireModel(
192+
this.schema,
193+
selectAllFromModel
194+
);
195+
const mappedTableName =
196+
this.getMappedName(fromModelDef) ?? selectAllFromModel;
193197
result.push(
194198
...scalarFields.map((fieldName) => {
195199
const fieldRef = ReferenceNode.create(
196200
ColumnNode.create(this.mapFieldName(fieldName)),
197-
TableNode.create(selectAllFromModel)
201+
TableNode.create(mappedTableName)
198202
);
199203
return SelectionNode.create(
200204
this.fieldHasMappedName(fieldName)

packages/runtime/src/client/helpers/schema-db-pusher.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ export class SchemaDbPusher<Schema extends SchemaDef> {
178178
}
179179

180180
const type = fieldDef.type as BuiltinType;
181-
let result = match(type)
181+
let result = match<BuiltinType, ColumnDataType>(type)
182182
.with('String', () => 'text')
183183
.with('Boolean', () => 'boolean')
184184
.with('Int', () => 'integer')
@@ -192,10 +192,13 @@ export class SchemaDbPusher<Schema extends SchemaDef> {
192192
.otherwise(() => {
193193
throw new Error(`Unsupported field type: ${type}`);
194194
});
195+
195196
if (fieldDef.array) {
196-
result = `${result}[]`;
197+
// Kysely doesn't support array type natively
198+
return sql.raw(`${result}[]`);
199+
} else {
200+
return result as ColumnDataType;
197201
}
198-
return result as ColumnDataType;
199202
}
200203

201204
private addForeignKeyConstraint(

0 commit comments

Comments
 (0)