Skip to content
This repository was archived by the owner on Mar 1, 2026. It is now read-only.

Commit 99f68e2

Browse files
authored
feat(orm): mysql support (#616)
* WIP(orm): mysql support * WIP: more progress with fixing tests * WIP: get all client api tests pass * WIP: get all tests pass * fix executor * add MySQL to CI matrix * fix sqlite test runs * fix test * fix delete readback check * set mysql container max connections * fix tests * fix test * refactor: extract duplicated mysql/pg code into base class * address PR comments * refactor: remove order by duplicated code * refactor: optimize stripTableReference * addressing PR comments * fix tests
1 parent 9e819e8 commit 99f68e2

File tree

85 files changed

+3376
-1432
lines changed

Some content is hidden

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

85 files changed

+3376
-1432
lines changed

.github/workflows/build-test.yml

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,23 @@ jobs:
3131
ports:
3232
- 5432:5432
3333

34+
mysql:
35+
image: mysql:8.4
36+
env:
37+
MYSQL_ROOT_PASSWORD: mysql
38+
ports:
39+
- 3306:3306
40+
# Set health checks to wait until mysql has started
41+
options: >-
42+
--health-cmd="mysqladmin ping --silent"
43+
--health-interval=10s
44+
--health-timeout=5s
45+
--health-retries=3
46+
3447
strategy:
3548
matrix:
3649
node-version: [22.x]
37-
provider: [sqlite, postgresql]
50+
provider: [sqlite, postgresql, mysql]
3851

3952
steps:
4053
- name: Checkout
@@ -81,5 +94,9 @@ jobs:
8194
- name: Lint
8295
run: pnpm run lint
8396

97+
- name: Set MySQL max_connections
98+
run: |
99+
mysql -h 127.0.0.1 -uroot -pmysql -e "SET GLOBAL max_connections=500;"
100+
84101
- name: Test
85102
run: TEST_DB_PROVIDER=${{ matrix.provider }} pnpm run test

TODO.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
- [ ] ZModel
1919
- [x] Import
2020
- [ ] View support
21-
- [ ] Datasource provider-scoped attributes
2221
- [ ] ORM
2322
- [x] Create
2423
- [x] Input validation
@@ -72,7 +71,7 @@
7271
- [x] Query builder API
7372
- [x] Computed fields
7473
- [x] Plugin
75-
- [ ] Custom procedures
74+
- [x] Custom procedures
7675
- [ ] Misc
7776
- [x] JSDoc for CRUD methods
7877
- [x] Cache validation schemas
@@ -110,4 +109,4 @@
110109
- [x] SQLite
111110
- [x] PostgreSQL
112111
- [x] Multi-schema
113-
- [ ] MySQL
112+
- [x] MySQL

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@
99
"watch": "turbo run watch build",
1010
"lint": "turbo run lint",
1111
"test": "turbo run test",
12-
"test:all": "pnpm run test:sqlite && pnpm run test:pg",
12+
"test:all": "pnpm run test:sqlite && pnpm run test:pg && pnpm run test:mysql",
1313
"test:pg": "TEST_DB_PROVIDER=postgresql turbo run test",
14+
"test:mysql": "TEST_DB_PROVIDER=mysql turbo run test",
1415
"test:sqlite": "TEST_DB_PROVIDER=sqlite turbo run test",
1516
"test:coverage": "vitest run --coverage",
1617
"format": "prettier --write \"**/*.{ts,tsx,md}\"",

packages/language/src/constants.ts

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,7 @@
11
/**
22
* Supported db providers
33
*/
4-
export const SUPPORTED_PROVIDERS = [
5-
'sqlite',
6-
'postgresql',
7-
// TODO: other providers
8-
// 'mysql',
9-
// 'sqlserver',
10-
// 'cockroachdb',
11-
];
4+
export const SUPPORTED_PROVIDERS = ['sqlite', 'postgresql', 'mysql'];
125

136
/**
147
* All scalar types
@@ -41,3 +34,8 @@ export enum ExpressionContext {
4134
ValidationRule = 'ValidationRule',
4235
Index = 'Index',
4336
}
37+
38+
/**
39+
* Database providers that support list field types.
40+
*/
41+
export const DB_PROVIDERS_SUPPORTING_LIST_TYPE = ['postgresql'];

packages/language/src/document.ts

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { invariant } from '@zenstackhq/common-helpers';
12
import {
23
isAstNode,
34
TextDocument,
@@ -10,10 +11,18 @@ import {
1011
import fs from 'node:fs';
1112
import path from 'node:path';
1213
import { fileURLToPath } from 'node:url';
13-
import { isDataSource, type Model } from './ast';
14-
import { STD_LIB_MODULE_NAME } from './constants';
14+
import { isDataModel, isDataSource, type Model } from './ast';
15+
import { DB_PROVIDERS_SUPPORTING_LIST_TYPE, STD_LIB_MODULE_NAME } from './constants';
1516
import { createZModelServices } from './module';
16-
import { getDataModelAndTypeDefs, getDocument, hasAttribute, resolveImport, resolveTransitiveImports } from './utils';
17+
import {
18+
getAllFields,
19+
getDataModelAndTypeDefs,
20+
getDocument,
21+
getLiteral,
22+
hasAttribute,
23+
resolveImport,
24+
resolveTransitiveImports,
25+
} from './utils';
1726
import type { ZModelFormatter } from './zmodel-formatter';
1827

1928
/**
@@ -207,6 +216,24 @@ function validationAfterImportMerge(model: Model) {
207216
if (authDecls.length > 1) {
208217
errors.push('Validation error: Multiple `@@auth` declarations are not allowed');
209218
}
219+
220+
// check for usages incompatible with the datasource provider
221+
const provider = getDataSourceProvider(model);
222+
invariant(provider !== undefined, 'Datasource provider should be defined at this point');
223+
224+
for (const decl of model.declarations.filter(isDataModel)) {
225+
const fields = getAllFields(decl, true);
226+
for (const field of fields) {
227+
if (field.type.array && !isDataModel(field.type.reference?.ref)) {
228+
if (!DB_PROVIDERS_SUPPORTING_LIST_TYPE.includes(provider)) {
229+
errors.push(
230+
`Validation error: List type is not supported for "${provider}" provider (model: "${decl.name}", field: "${field.name}")`,
231+
);
232+
}
233+
}
234+
}
235+
}
236+
210237
return errors;
211238
}
212239

@@ -226,3 +253,15 @@ export async function formatDocument(content: string) {
226253
const edits = await formatter.formatDocument(document, { options, textDocument: identifier });
227254
return TextDocument.applyEdits(document.textDocument, edits);
228255
}
256+
257+
function getDataSourceProvider(model: Model) {
258+
const dataSource = model.declarations.find(isDataSource);
259+
if (!dataSource) {
260+
return undefined;
261+
}
262+
const provider = dataSource?.fields.find((f) => f.name === 'provider');
263+
if (!provider) {
264+
return undefined;
265+
}
266+
return getLiteral<string>(provider.value);
267+
}

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

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,16 @@ import {
55
ArrayExpr,
66
DataField,
77
DataModel,
8-
Model,
98
ReferenceExpr,
109
TypeDef,
1110
isDataModel,
12-
isDataSource,
1311
isEnum,
14-
isModel,
1512
isStringLiteral,
1613
isTypeDef,
1714
} from '../generated/ast';
1815
import {
1916
getAllAttributes,
2017
getAllFields,
21-
getLiteral,
2218
getModelIdFields,
2319
getModelUniqueFields,
2420
getUniqueFields,
@@ -105,13 +101,6 @@ export default class DataModelValidator implements AstValidator<DataModel> {
105101
accept('error', 'Unsupported type argument must be a string literal', { node: field.type.unsupported });
106102
}
107103

108-
if (field.type.array && !isDataModel(field.type.reference?.ref)) {
109-
const provider = this.getDataSourceProvider(AstUtils.getContainerOfType(field, isModel)!);
110-
if (provider === 'sqlite') {
111-
accept('error', `List type is not supported for "${provider}" provider.`, { node: field.type });
112-
}
113-
}
114-
115104
field.attributes.forEach((attr) => validateAttributeApplication(attr, accept));
116105

117106
if (isTypeDef(field.type.reference?.ref)) {
@@ -121,18 +110,6 @@ export default class DataModelValidator implements AstValidator<DataModel> {
121110
}
122111
}
123112

124-
private getDataSourceProvider(model: Model) {
125-
const dataSource = model.declarations.find(isDataSource);
126-
if (!dataSource) {
127-
return undefined;
128-
}
129-
const provider = dataSource?.fields.find((f) => f.name === 'provider');
130-
if (!provider) {
131-
return undefined;
132-
}
133-
return getLiteral<string>(provider.value);
134-
}
135-
136113
private validateAttributes(dm: DataModel, accept: ValidationAcceptor) {
137114
getAllAttributes(dm).forEach((attr) => validateAttributeApplication(attr, accept, dm));
138115
}

packages/orm/package.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,16 @@
5656
"default": "./dist/dialects/postgres.cjs"
5757
}
5858
},
59+
"./dialects/mysql": {
60+
"import": {
61+
"types": "./dist/dialects/mysql.d.ts",
62+
"default": "./dist/dialects/mysql.js"
63+
},
64+
"require": {
65+
"types": "./dist/dialects/mysql.d.cts",
66+
"default": "./dist/dialects/mysql.cjs"
67+
}
68+
},
5969
"./dialects/sql.js": {
6070
"import": {
6171
"types": "./dist/dialects/sql.js.d.ts",
@@ -100,6 +110,7 @@
100110
"peerDependencies": {
101111
"better-sqlite3": "catalog:",
102112
"pg": "catalog:",
113+
"mysql2": "catalog:",
103114
"sql.js": "catalog:",
104115
"zod": "catalog:"
105116
},
@@ -110,6 +121,9 @@
110121
"pg": {
111122
"optional": true
112123
},
124+
"mysql2": {
125+
"optional": true
126+
},
113127
"sql.js": {
114128
"optional": true
115129
}

packages/orm/src/client/client-impl.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import { FindOperationHandler } from './crud/operations/find';
3131
import { GroupByOperationHandler } from './crud/operations/group-by';
3232
import { UpdateOperationHandler } from './crud/operations/update';
3333
import { InputValidator } from './crud/validator';
34-
import { createConfigError, createNotFoundError } from './errors';
34+
import { createConfigError, createNotFoundError, createNotSupportedError } from './errors';
3535
import { ZenStackDriver } from './executor/zenstack-driver';
3636
import { ZenStackQueryExecutor } from './executor/zenstack-query-executor';
3737
import * as BuiltinFunctions from './functions';
@@ -564,6 +564,11 @@ function createModelCrudHandler(
564564
},
565565

566566
createManyAndReturn: (args: unknown) => {
567+
if (client.$schema.provider.type === 'mysql') {
568+
throw createNotSupportedError(
569+
'"createManyAndReturn" is not supported by "mysql" provider. Use "createMany" or multiple "create" calls instead.',
570+
);
571+
}
567572
return createPromise(
568573
'createManyAndReturn',
569574
'createManyAndReturn',
@@ -594,6 +599,11 @@ function createModelCrudHandler(
594599
},
595600

596601
updateManyAndReturn: (args: unknown) => {
602+
if (client.$schema.provider.type === 'mysql') {
603+
throw createNotSupportedError(
604+
'"updateManyAndReturn" is not supported by "mysql" provider. Use "updateMany" or multiple "update" calls instead.',
605+
);
606+
}
597607
return createPromise(
598608
'updateManyAndReturn',
599609
'updateManyAndReturn',

0 commit comments

Comments
 (0)