schematic-pg 0.1.19 → 0.1.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -2
- package/dist/cli/templates/agents.md +1 -1
- package/dist/schema-dsl/ast.d.ts +8 -1
- package/dist/schema-dsl/ast.js +3 -1
- package/dist/schema-dsl/parser.d.ts +2 -0
- package/dist/schema-dsl/parser.js +48 -1
- package/dist/sql-generator/generators/functions.js +2 -2
- package/dist/sql-generator/utils/ast-helpers.d.ts +9 -1
- package/dist/sql-generator/utils/ast-helpers.js +31 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -390,6 +390,15 @@ functions {
|
|
|
390
390
|
"""
|
|
391
391
|
}
|
|
392
392
|
|
|
393
|
+
function searchProducts(query: TEXT): TABLE(id: UUID, name: TEXT, price: DECIMAL) {
|
|
394
|
+
language: sql
|
|
395
|
+
volatility: STABLE
|
|
396
|
+
execute: """
|
|
397
|
+
SELECT id, name, price FROM product
|
|
398
|
+
WHERE name ILIKE '%' || query || '%'
|
|
399
|
+
"""
|
|
400
|
+
}
|
|
401
|
+
|
|
393
402
|
function setUpdatedAt(): TRIGGER {
|
|
394
403
|
language: plpgsql
|
|
395
404
|
execute: """
|
|
@@ -407,11 +416,11 @@ functions {
|
|
|
407
416
|
| `security` | `INVOKER`, `DEFINER` | `INVOKER` |
|
|
408
417
|
| `execute` | Triple-quoted SQL / PL/pgSQL | required |
|
|
409
418
|
|
|
410
|
-
Return types are PostgreSQL types (`INTEGER`, `UUID`, `JSONB`, …), `TRIGGER`, or `
|
|
419
|
+
Return types are PostgreSQL types (`INTEGER`, `UUID`, `JSONB`, …), `TRIGGER`, `VOID`, or `TABLE(col: Type, …)` for set-returning functions. Body keys may be newline-separated or comma-separated. TABLE column names must be unique and must not collide with input parameter names.
|
|
411
420
|
|
|
412
421
|
`language: plpgsql` wraps the body in `BEGIN` / `END` unless it already starts with `DECLARE` or `BEGIN`. Function names must be unique in the schema.
|
|
413
422
|
|
|
414
|
-
`db:diff` treats body, language, volatility, and security changes as `CREATE OR REPLACE`. Argument or return-type changes drop the old function, then create the new one.
|
|
423
|
+
`db:diff` treats body, language, volatility, and security changes as `CREATE OR REPLACE`. Argument or return-type changes (including TABLE column changes) drop the old function, then create the new one.
|
|
415
424
|
|
|
416
425
|
Functions are database objects only in this release — they are not REST endpoints. Call them with `db.$queryRaw`:
|
|
417
426
|
|
|
@@ -420,6 +429,11 @@ const [row] = await db.$queryRaw<{ get_user_balance: number }>(
|
|
|
420
429
|
'SELECT get_user_balance($1)',
|
|
421
430
|
[userId],
|
|
422
431
|
);
|
|
432
|
+
|
|
433
|
+
const products = await db.$queryRaw<{ id: string; name: string; price: string }>(
|
|
434
|
+
'SELECT * FROM search_products($1)',
|
|
435
|
+
[query],
|
|
436
|
+
);
|
|
423
437
|
```
|
|
424
438
|
|
|
425
439
|
---
|
|
@@ -100,7 +100,7 @@ models {
|
|
|
100
100
|
- **Policies:** `@policy(role: ..., allow: [select|insert|update|delete|all], where: "...")` — `where` supports `{{auth.user.id}}`.
|
|
101
101
|
- **Validation:** `@regex(...)`, `@range(min: ..., max: ...)` flow into generated Zod schemas.
|
|
102
102
|
- **Indexes / triggers:** `@@index(...)`, `@@trigger { timing, event, level, execute: """...""" }`.
|
|
103
|
-
- **SQL functions:** optional `functions { function name(args): ReturnType { execute: """...""" } }` after `models`. Names snake_case in SQL; call with `db.$queryRaw`.
|
|
103
|
+
- **SQL functions:** optional `functions { function name(args): ReturnType { execute: """...""" } }` after `models`. `ReturnType` may be a scalar (`INTEGER`, `TRIGGER`, `VOID`, …) or `TABLE(col: Type, …)`. Names snake_case in SQL; call scalars with `SELECT fn($1)`, table functions with `SELECT * FROM fn($1)` via `db.$queryRaw`.
|
|
104
104
|
|
|
105
105
|
## Database Client
|
|
106
106
|
|
package/dist/schema-dsl/ast.d.ts
CHANGED
|
@@ -36,7 +36,7 @@ export interface SqlFunction {
|
|
|
36
36
|
kind: 'SqlFunction';
|
|
37
37
|
name: string;
|
|
38
38
|
params: FunctionParam[];
|
|
39
|
-
returns:
|
|
39
|
+
returns: FunctionReturn;
|
|
40
40
|
language?: string;
|
|
41
41
|
volatility?: string;
|
|
42
42
|
security?: string;
|
|
@@ -49,6 +49,13 @@ export interface FunctionParam {
|
|
|
49
49
|
type: TypeExpr;
|
|
50
50
|
loc: SourceLocation;
|
|
51
51
|
}
|
|
52
|
+
export interface TableReturn {
|
|
53
|
+
kind: 'TableReturn';
|
|
54
|
+
columns: FunctionParam[];
|
|
55
|
+
loc: SourceLocation;
|
|
56
|
+
}
|
|
57
|
+
export type FunctionReturn = TypeExpr | TableReturn;
|
|
58
|
+
export declare function isTableReturn(returns: FunctionReturn): returns is TableReturn;
|
|
52
59
|
export interface Field {
|
|
53
60
|
kind: 'Field';
|
|
54
61
|
name: string;
|
package/dist/schema-dsl/ast.js
CHANGED
|
@@ -22,6 +22,8 @@ export declare class Parser {
|
|
|
22
22
|
private parseModelsSection;
|
|
23
23
|
private parseFunctionsSection;
|
|
24
24
|
parseFunction(existingNames?: Set<string>): SqlFunction;
|
|
25
|
+
private parseFunctionReturn;
|
|
26
|
+
private parseTableReturn;
|
|
25
27
|
private parseFunctionParams;
|
|
26
28
|
private parseFunctionParam;
|
|
27
29
|
private parseFunctionBody;
|
|
@@ -137,7 +137,7 @@ export class Parser {
|
|
|
137
137
|
const params = this.parseFunctionParams();
|
|
138
138
|
this.expect(TokenType.RPAREN, "')'");
|
|
139
139
|
this.expect(TokenType.COLON, "':'");
|
|
140
|
-
const returns = this.
|
|
140
|
+
const returns = this.parseFunctionReturn(params);
|
|
141
141
|
this.expect(TokenType.LBRACE, "'{'");
|
|
142
142
|
const body = this.parseFunctionBody();
|
|
143
143
|
this.expect(TokenType.RBRACE, "'}'");
|
|
@@ -153,6 +153,53 @@ export class Parser {
|
|
|
153
153
|
loc: this.loc(start),
|
|
154
154
|
};
|
|
155
155
|
}
|
|
156
|
+
parseFunctionReturn(params) {
|
|
157
|
+
const current = this.current();
|
|
158
|
+
if (current.type === TokenType.IDENT &&
|
|
159
|
+
current.value === 'TABLE' &&
|
|
160
|
+
this.peekType(1) === TokenType.LPAREN) {
|
|
161
|
+
return this.parseTableReturn(params);
|
|
162
|
+
}
|
|
163
|
+
if (current.type === TokenType.IDENT && current.value === 'TABLE') {
|
|
164
|
+
throw new ParseError("TABLE column list '(...)'", current);
|
|
165
|
+
}
|
|
166
|
+
return this.parseTypeExpr();
|
|
167
|
+
}
|
|
168
|
+
parseTableReturn(params) {
|
|
169
|
+
const start = this.expect(TokenType.IDENT, "'TABLE'");
|
|
170
|
+
this.expect(TokenType.LPAREN, "'('");
|
|
171
|
+
if (this.check(TokenType.RPAREN)) {
|
|
172
|
+
throw new ParseError('at least one TABLE column', this.current());
|
|
173
|
+
}
|
|
174
|
+
const columns = [];
|
|
175
|
+
const columnNames = new Set();
|
|
176
|
+
const paramNames = new Set(params.map((param) => param.name));
|
|
177
|
+
do {
|
|
178
|
+
const nameToken = this.expect(TokenType.IDENT, 'TABLE column name');
|
|
179
|
+
if (columnNames.has(nameToken.value)) {
|
|
180
|
+
throw new ParseError(`unique TABLE column name, "${nameToken.value}" already defined`, nameToken);
|
|
181
|
+
}
|
|
182
|
+
if (paramNames.has(nameToken.value)) {
|
|
183
|
+
throw new ParseError(`TABLE column name distinct from parameter "${nameToken.value}"`, nameToken);
|
|
184
|
+
}
|
|
185
|
+
columnNames.add(nameToken.value);
|
|
186
|
+
this.expect(TokenType.COLON, "':'");
|
|
187
|
+
const type = this.parseTypeExpr();
|
|
188
|
+
columns.push({
|
|
189
|
+
kind: 'FunctionParam',
|
|
190
|
+
name: nameToken.value,
|
|
191
|
+
type,
|
|
192
|
+
loc: this.loc(nameToken),
|
|
193
|
+
});
|
|
194
|
+
} while (this.match(TokenType.COMMA) && !this.check(TokenType.RPAREN));
|
|
195
|
+
this.consumeTrailingComma();
|
|
196
|
+
this.expect(TokenType.RPAREN, "')'");
|
|
197
|
+
return {
|
|
198
|
+
kind: 'TableReturn',
|
|
199
|
+
columns,
|
|
200
|
+
loc: this.loc(start),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
156
203
|
parseFunctionParams() {
|
|
157
204
|
if (this.check(TokenType.RPAREN)) {
|
|
158
205
|
return [];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getEnumNames, normalizeFunction, } from '../utils/ast-helpers.js';
|
|
1
|
+
import { formatNormalizedFunctionReturn, getEnumNames, normalizeFunction, } from '../utils/ast-helpers.js';
|
|
2
2
|
import { joinSection } from '../utils/format.js';
|
|
3
3
|
import { quoteIdentifier } from '../utils/snake-case.js';
|
|
4
4
|
export function generateCreateFunction(normalized) {
|
|
@@ -8,7 +8,7 @@ export function generateCreateFunction(normalized) {
|
|
|
8
8
|
.join(', ');
|
|
9
9
|
const clauses = [
|
|
10
10
|
`CREATE OR REPLACE FUNCTION ${functionName}(${params})`,
|
|
11
|
-
`RETURNS ${normalized.returns}`,
|
|
11
|
+
`RETURNS ${formatNormalizedFunctionReturn(normalized.returns)}`,
|
|
12
12
|
`LANGUAGE ${normalized.language}`,
|
|
13
13
|
];
|
|
14
14
|
if (normalized.volatility !== 'VOLATILE') {
|
|
@@ -61,16 +61,24 @@ export interface NormalizedFunctionParam {
|
|
|
61
61
|
sqlName: string;
|
|
62
62
|
sqlType: string;
|
|
63
63
|
}
|
|
64
|
+
export type NormalizedFunctionReturn = {
|
|
65
|
+
kind: 'scalar';
|
|
66
|
+
sqlType: string;
|
|
67
|
+
} | {
|
|
68
|
+
kind: 'table';
|
|
69
|
+
columns: NormalizedFunctionParam[];
|
|
70
|
+
};
|
|
64
71
|
export interface NormalizedFunction {
|
|
65
72
|
name: string;
|
|
66
73
|
sqlName: string;
|
|
67
74
|
params: NormalizedFunctionParam[];
|
|
68
|
-
returns:
|
|
75
|
+
returns: NormalizedFunctionReturn;
|
|
69
76
|
language: string;
|
|
70
77
|
volatility: string;
|
|
71
78
|
security: string;
|
|
72
79
|
execute: string;
|
|
73
80
|
}
|
|
74
81
|
export declare function normalizeFunction(sqlFunction: SqlFunction, enumNames: Set<string>): NormalizedFunction;
|
|
82
|
+
export declare function formatNormalizedFunctionReturn(returns: NormalizedFunctionReturn): string;
|
|
75
83
|
export declare function functionIdentity(normalized: NormalizedFunction): string;
|
|
76
84
|
export declare function functionSignature(normalized: NormalizedFunction): string;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { isTableReturn } from '../../schema-dsl/ast.js';
|
|
1
2
|
import { formatDefaultValue, serializeValue } from './value-formatter.js';
|
|
2
3
|
import { mapColumnType } from './type-mapper.js';
|
|
3
|
-
import { toSnakeCase, toTableName } from './snake-case.js';
|
|
4
|
+
import { toSnakeCase, toTableName, quoteIdentifier } from './snake-case.js';
|
|
4
5
|
export function getModelNames(schema) {
|
|
5
6
|
return new Set(schema.models.map((model) => model.name));
|
|
6
7
|
}
|
|
@@ -251,21 +252,44 @@ export function resolveTriggerNames(model, timing, event) {
|
|
|
251
252
|
};
|
|
252
253
|
}
|
|
253
254
|
export function normalizeFunction(sqlFunction, enumNames) {
|
|
255
|
+
const params = sqlFunction.params.map((param) => ({
|
|
256
|
+
name: param.name,
|
|
257
|
+
sqlName: toSnakeCase(param.name),
|
|
258
|
+
sqlType: serializeColumnType(param.type, enumNames),
|
|
259
|
+
}));
|
|
260
|
+
const returns = isTableReturn(sqlFunction.returns)
|
|
261
|
+
? {
|
|
262
|
+
kind: 'table',
|
|
263
|
+
columns: sqlFunction.returns.columns.map((column) => ({
|
|
264
|
+
name: column.name,
|
|
265
|
+
sqlName: toSnakeCase(column.name),
|
|
266
|
+
sqlType: serializeColumnType(column.type, enumNames),
|
|
267
|
+
})),
|
|
268
|
+
}
|
|
269
|
+
: {
|
|
270
|
+
kind: 'scalar',
|
|
271
|
+
sqlType: serializeColumnType(sqlFunction.returns, enumNames),
|
|
272
|
+
};
|
|
254
273
|
return {
|
|
255
274
|
name: sqlFunction.name,
|
|
256
275
|
sqlName: toSnakeCase(sqlFunction.name),
|
|
257
|
-
params
|
|
258
|
-
|
|
259
|
-
sqlName: toSnakeCase(param.name),
|
|
260
|
-
sqlType: serializeColumnType(param.type, enumNames),
|
|
261
|
-
})),
|
|
262
|
-
returns: serializeColumnType(sqlFunction.returns, enumNames),
|
|
276
|
+
params,
|
|
277
|
+
returns,
|
|
263
278
|
language: (sqlFunction.language ?? 'sql').toLowerCase(),
|
|
264
279
|
volatility: (sqlFunction.volatility ?? 'VOLATILE').toUpperCase(),
|
|
265
280
|
security: (sqlFunction.security ?? 'INVOKER').toUpperCase(),
|
|
266
281
|
execute: sqlFunction.execute.trim(),
|
|
267
282
|
};
|
|
268
283
|
}
|
|
284
|
+
export function formatNormalizedFunctionReturn(returns) {
|
|
285
|
+
if (returns.kind === 'scalar') {
|
|
286
|
+
return returns.sqlType;
|
|
287
|
+
}
|
|
288
|
+
const columns = returns.columns
|
|
289
|
+
.map((column) => `${quoteIdentifier(column.sqlName)} ${column.sqlType}`)
|
|
290
|
+
.join(', ');
|
|
291
|
+
return `TABLE (${columns})`;
|
|
292
|
+
}
|
|
269
293
|
export function functionIdentity(normalized) {
|
|
270
294
|
return JSON.stringify({
|
|
271
295
|
sqlName: normalized.sqlName,
|