Skip to content

Commit 321508a

Browse files
author
mvtthew
committed
Fixup validation
- validation logic for the Function element validator - new error codes - new function interpreter test
1 parent c8bdb93 commit 321508a

3 files changed

Lines changed: 261 additions & 2 deletions

File tree

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import { CompileErrorCode } from '@/index';
2+
import { interpret } from '@tests/utils';
3+
4+
describe('[example - function] function blocks', () => {
5+
test('should report error for unrecognized return type', () => {
6+
const source = `
7+
Function simple_add {
8+
schema public
9+
returns integerrr
10+
args [a: integer, b: integer]
11+
body \`
12+
BEGIN
13+
RETURN a + b;
14+
END;
15+
\`
16+
language plpgsql
17+
behavior immutable
18+
security definer
19+
}
20+
`;
21+
22+
const result = interpret(source);
23+
const errors = result.getErrors();
24+
25+
expect(errors.length).toBe(1);
26+
expect(errors[0].code).toBe(CompileErrorCode.INVALID_FUNCTION_FIELD_VALUE);
27+
expect(errors[0].diagnostic).toBe("'returns' must be a valid return type (e.g. void, integer, text, ...)");
28+
});
29+
30+
test('should report error for invalid arg types', () => {
31+
const source = `
32+
Function simple_add {
33+
schema public
34+
returns integer
35+
args [a: void, b: integer]
36+
body \`
37+
BEGIN
38+
RETURN a + b;
39+
END;
40+
\`
41+
language plpgsql
42+
behavior immutable
43+
security definer
44+
}
45+
`;
46+
47+
const result = interpret(source);
48+
const errors = result.getErrors();
49+
50+
expect(errors.length).toBe(1);
51+
expect(errors[0].code).toBe(CompileErrorCode.INVALID_FUNCTION_FIELD_VALUE);
52+
expect(errors[0].diagnostic).toBe("Argument type 'void' is not valid");
53+
});
54+
55+
test('should report error for unrecognized language', () => {
56+
const source = `
57+
Function simple_add {
58+
schema public
59+
returns integer
60+
args [a: integer, b: integer]
61+
body \`
62+
BEGIN
63+
RETURN a + b;
64+
END;
65+
\`
66+
language javascript
67+
behavior immutable
68+
security definer
69+
}
70+
`;
71+
72+
const result = interpret(source);
73+
const errors = result.getErrors();
74+
75+
expect(errors.length).toBe(1);
76+
expect(errors[0].code).toBe(CompileErrorCode.INVALID_FUNCTION_FIELD_VALUE);
77+
expect(errors[0].diagnostic).toBe("'language' must be one of: plpgsql, sql, c, internal");
78+
});
79+
80+
test('should report error for unrecognized behavior parameter', () => {
81+
const source = `
82+
Function simple_add {
83+
schema public
84+
returns integer
85+
args [a: integer, b: integer]
86+
body \`
87+
BEGIN
88+
RETURN a + b;
89+
END;
90+
\`
91+
language plpgsql
92+
behavior behavior
93+
security definer
94+
}
95+
`;
96+
97+
const result = interpret(source);
98+
const errors = result.getErrors();
99+
100+
expect(errors.length).toBe(1);
101+
expect(errors[0].code).toBe(CompileErrorCode.INVALID_FUNCTION_FIELD_VALUE);
102+
expect(errors[0].diagnostic).toBe("'behavior' must be one of: volatile, immutable, stable");
103+
});
104+
105+
test('should report error for unrecognized security parameter', () => {
106+
const source = `
107+
Function simple_add {
108+
schema public
109+
returns integer
110+
args [a: integer, b: integer]
111+
body \`
112+
BEGIN
113+
RETURN a + b;
114+
END;
115+
\`
116+
language plpgsql
117+
behavior immutable
118+
security security
119+
}
120+
`;
121+
122+
const result = interpret(source);
123+
const errors = result.getErrors();
124+
125+
expect(errors.length).toBe(1);
126+
expect(errors[0].code).toBe(CompileErrorCode.INVALID_FUNCTION_FIELD_VALUE);
127+
expect(errors[0].diagnostic).toBe("'security' must be one of: invoker, definer");
128+
});
129+
});

packages/dbml-parse/src/core/analyzer/validator/elementValidators/function.ts

Lines changed: 127 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,36 @@
1+
import { partition } from 'lodash-es';
12
import SymbolFactory from '@/core/analyzer/symbol/factory';
23
import { CompileError, CompileErrorCode } from '@/core/errors';
34
import {
45
BlockExpressionNode,
56
ElementDeclarationNode,
67
FunctionApplicationNode,
8+
FunctionExpressionNode,
9+
IdentiferStreamNode,
710
ListExpressionNode,
811
} from '@/core/parser/nodes';
912
import { SyntaxToken } from '@/core/lexer/tokens';
1013
import { ElementValidator } from '@/core/analyzer/validator/types';
1114
import SymbolTable from '@/core/analyzer/symbol/symbolTable';
15+
import { extractVariableFromExpression } from '@/core/analyzer/utils';
16+
17+
const VALID_ARG_TYPES = new Set([
18+
'integer', 'bool', 'bytea', 'date', 'double_precision',
19+
'float4', 'float8', 'int2', 'int4', 'int8', 'json', 'jsonb',
20+
'numeric', 'text', 'time', 'timestamp', 'timestamptz', 'timetz',
21+
'uuid', 'varchar', 'vector',
22+
]);
23+
24+
const VALID_RETURN_TYPES = new Set([
25+
'void', 'record', 'trigger',
26+
...VALID_ARG_TYPES,
27+
]);
28+
29+
const VALID_LANGUAGES = new Set(['plpgsql', 'sql', 'c', 'internal']);
30+
const VALID_BEHAVIORS = new Set(['volatile', 'immutable', 'stable']);
31+
const VALID_SECURITIES = new Set(['invoker', 'definer']);
32+
33+
const SINGLE_OCCURRENCE_FIELDS = new Set(['schema', 'returns', 'args', 'body', 'language', 'behavior', 'security']);
1234

1335
export default class FunctionValidator implements ElementValidator {
1436
private declarationNode: ElementDeclarationNode & { type: SyntaxToken };
@@ -32,7 +54,7 @@ export default class FunctionValidator implements ElementValidator {
3254

3355
private validateContext (): CompileError[] {
3456
if (this.declarationNode.parent instanceof ElementDeclarationNode) {
35-
return [new CompileError(CompileErrorCode.INVALID_POLICY_CONTEXT, 'A Function can only appear top-level', this.declarationNode)];
57+
return [new CompileError(CompileErrorCode.INVALID_FUNCTION_CONTEXT, 'A Function can only appear top-level', this.declarationNode)];
3658
}
3759

3860
return [];
@@ -62,6 +84,109 @@ export default class FunctionValidator implements ElementValidator {
6284
return [new CompileError(CompileErrorCode.UNEXPECTED_SIMPLE_BODY, 'A Function\'s body must be a block', body)];
6385
}
6486

65-
return [];
87+
const [fields] = partition(body.body, (e) => e instanceof FunctionApplicationNode);
88+
return this.validateFields(fields as FunctionApplicationNode[]);
89+
}
90+
91+
private validateFields (fields: FunctionApplicationNode[]): CompileError[] {
92+
const seen = new Set<string>();
93+
return fields.flatMap((field) => {
94+
if (!field.callee) return [];
95+
96+
const fieldName = extractVariableFromExpression(field.callee).unwrap_or('').toLowerCase();
97+
const errors: CompileError[] = [];
98+
99+
if (SINGLE_OCCURRENCE_FIELDS.has(fieldName)) {
100+
if (seen.has(fieldName)) {
101+
errors.push(new CompileError(CompileErrorCode.DUPLICATE_FUNCTION_FIELD, `'${fieldName}' can only appear once`, field));
102+
}
103+
seen.add(fieldName);
104+
}
105+
106+
switch (fieldName) {
107+
case 'schema':
108+
break;
109+
case 'returns': {
110+
const value = extractVariableFromExpression(field.args[0]).unwrap_or('');
111+
if (!VALID_RETURN_TYPES.has(value.toLowerCase())) {
112+
errors.push(new CompileError(
113+
CompileErrorCode.INVALID_FUNCTION_FIELD_VALUE,
114+
`'returns' must be a valid return type (e.g. void, integer, text, ...)`,
115+
field.args[0] || field,
116+
));
117+
}
118+
break;
119+
}
120+
case 'args': {
121+
const arg = field.args[0];
122+
if (arg instanceof ListExpressionNode) {
123+
arg.elementList.forEach((attr) => {
124+
const argType = extractVariableFromExpression(attr.value as any).unwrap_or('');
125+
if (argType && !VALID_ARG_TYPES.has(argType.toLowerCase())) {
126+
errors.push(new CompileError(
127+
CompileErrorCode.INVALID_FUNCTION_FIELD_VALUE,
128+
`Argument type '${argType}' is not valid`,
129+
attr.value || attr,
130+
));
131+
}
132+
});
133+
}
134+
break;
135+
}
136+
case 'body':
137+
if (field.args[0] && !(field.args[0] instanceof FunctionExpressionNode)) {
138+
const value = extractVariableFromExpression(field.args[0]).unwrap_or('');
139+
if (!value) {
140+
errors.push(new CompileError(
141+
CompileErrorCode.INVALID_FUNCTION_FIELD_VALUE,
142+
'\'body\' must be an expression or a backtick string',
143+
field.args[0],
144+
));
145+
}
146+
}
147+
break;
148+
case 'language': {
149+
const value = extractVariableFromExpression(field.args[0]).unwrap_or('');
150+
if (!VALID_LANGUAGES.has(value.toLowerCase())) {
151+
errors.push(new CompileError(
152+
CompileErrorCode.INVALID_FUNCTION_FIELD_VALUE,
153+
`'language' must be one of: ${[...VALID_LANGUAGES].join(', ')}`,
154+
field.args[0] || field,
155+
));
156+
}
157+
break;
158+
}
159+
case 'behavior': {
160+
const value = extractVariableFromExpression(field.args[0]).unwrap_or('');
161+
if (!VALID_BEHAVIORS.has(value.toLowerCase())) {
162+
errors.push(new CompileError(
163+
CompileErrorCode.INVALID_FUNCTION_FIELD_VALUE,
164+
`'behavior' must be one of: ${[...VALID_BEHAVIORS].join(', ')}`,
165+
field.args[0] || field,
166+
));
167+
}
168+
break;
169+
}
170+
case 'security': {
171+
const value = extractVariableFromExpression(field.args[0]).unwrap_or('');
172+
if (!VALID_SECURITIES.has(value.toLowerCase())) {
173+
errors.push(new CompileError(
174+
CompileErrorCode.INVALID_FUNCTION_FIELD_VALUE,
175+
`'security' must be one of: ${[...VALID_SECURITIES].join(', ')}`,
176+
field.args[0] || field,
177+
));
178+
}
179+
break;
180+
}
181+
default:
182+
errors.push(new CompileError(
183+
CompileErrorCode.UNKNOWN_FUNCTION_FIELD,
184+
`Unknown Function field '${fieldName}'`,
185+
field,
186+
));
187+
}
188+
189+
return errors;
190+
});
66191
}
67192
}

packages/dbml-parse/src/core/errors.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,11 @@ export enum CompileErrorCode {
117117

118118
INVALID_POLICY_CONTEXT,
119119

120+
INVALID_FUNCTION_CONTEXT,
121+
UNKNOWN_FUNCTION_FIELD,
122+
DUPLICATE_FUNCTION_FIELD,
123+
INVALID_FUNCTION_FIELD_VALUE,
124+
120125
BINDING_ERROR = 4000,
121126

122127
UNSUPPORTED = 5000,

0 commit comments

Comments
 (0)