Add static analysis for the `IIF` function (#2392)

This commit is contained in:
Simon Binder 2023-04-18 12:42:51 +02:00
parent a988b38ec1
commit 11b563f9de
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
8 changed files with 91 additions and 3 deletions

View File

@ -1,3 +1,7 @@
## 0.28.2-dev
- Support resolving `IIF` functions.
## 0.28.1
- Fix false-positive warnings about `AS` aliases in subqueries used in triggers.

View File

@ -65,6 +65,7 @@ enum AnalysisErrorType {
ambiguousReference,
synctactic,
unknownFunction,
invalidAmountOfParameters,
starColumnWithoutTable,
compoundColumnCountMismatch,
cteColumnCountMismatch,

View File

@ -518,6 +518,17 @@ class TypeResolver extends RecursiveVisitor<TypeExpectation, void> {
session._addRelation(NullableIfSomeOtherIs(e, params));
}
void checkArgumentCount(int expectedArgs) {
if (params.length != expectedArgs) {
session.context.reportError(AnalysisError(
type: AnalysisErrorType.invalidAmountOfParameters,
message:
'${e.name} expects $expectedArgs arguments, got ${params.length}.',
relevantNode: e.parameters,
));
}
}
final lowercaseName = e.name.toLowerCase();
switch (lowercaseName) {
case 'round':
@ -594,6 +605,18 @@ class TypeResolver extends RecursiveVisitor<TypeExpectation, void> {
case 'likely':
case 'unlikely':
session._addRelation(CopyTypeFrom(e, params.first));
return null;
case 'iif':
checkArgumentCount(3);
if (params.length == 3) {
// IIF(a, b, c) is essentially CASE WHEN a THEN b ELSE c END
final cases = [params[1], params[2]];
session
.._addRelation(CopyEncapsulating(e, cases))
.._addRelation(HaveSameType(cases));
}
return null;
case 'coalesce':
case 'ifnull':
@ -645,6 +668,15 @@ class TypeResolver extends RecursiveVisitor<TypeExpectation, void> {
final name = e.name.toLowerCase();
switch (name) {
case 'iif':
if (params.isNotEmpty) {
final condition = params[0];
if (condition is Expression) {
visited.add(condition);
visit(condition, _expectCondition);
}
}
break;
case 'nth_value':
if (params.length >= 2 && params[1] is Expression) {
// the second argument of nth_value is always an integer

View File

@ -856,7 +856,8 @@ class Parser {
} else {
_error(
'Expected an expression here, but got a reserved keyword. Did you '
'mean to use it as an identifier? Try wrapping it in double quotes.',
'mean to use it as a column? Try wrapping it in double quotes '
'("${_peek.lexeme}").',
);
}
} else {

View File

@ -0,0 +1,18 @@
import 'package:sqlparser/sqlparser.dart';
import 'package:test/test.dart';
import 'utils.dart';
void main() {
test('is forbidden on older sqlite versions', () {
final engine = SqlEngine();
final result = engine.analyze('SELECT iif (0, 1)');
expect(result.errors, [
analysisErrorWith(
lexeme: '0, 1',
type: AnalysisErrorType.invalidAmountOfParameters,
message: 'iif expects 3 arguments, got 2.'),
]);
});
}

View File

@ -14,7 +14,7 @@ extension ExpectErrors on AnalysisContext {
}
}
Matcher analysisErrorWith({String? lexeme, AnalysisErrorType? type}) {
Matcher analysisErrorWith({String? lexeme, AnalysisErrorType? type, message}) {
var matcher = isA<AnalysisError>();
if (lexeme != null) {
@ -23,6 +23,9 @@ Matcher analysisErrorWith({String? lexeme, AnalysisErrorType? type}) {
if (type != null) {
matcher = matcher.having((e) => e.type, 'type', type);
}
if (message != null) {
matcher = matcher.having((e) => e.message, 'message', message);
}
return matcher;
}

View File

@ -83,6 +83,35 @@ void main() {
});
});
group('iif', () {
test('has type of arguments', () {
expect(resolveResultColumn('SELECT IIF(false, 0, 1)'),
const ResolvedType(type: BasicType.int));
});
test('is nullable if argument is', () {
expect(resolveResultColumn('SELECT IIF(false, NULL, 1)'),
const ResolvedType(type: BasicType.int, nullable: true));
});
test('is not nullable just because the condition is', () {
expect(resolveResultColumn('SELECT IIF(NULL, 0, 1)'),
const ResolvedType(type: BasicType.int));
});
test('infers one argument based on the other', () {
expect(resolveFirstVariable('SELECT IIF(false, ?, 1)'),
const ResolvedType(type: BasicType.int));
expect(resolveFirstVariable('SELECT IIF(false, 0, ?)'),
const ResolvedType(type: BasicType.int));
});
test('infers condition', () {
expect(resolveFirstVariable('SELECT IIF(?, 0, 1)'),
const ResolvedType(type: BasicType.int, hint: IsBoolean()));
});
});
group('types in insert statements', () {
test('for VALUES', () {
final resolver =

View File

@ -20,7 +20,7 @@ void main() {
test('as identifiers', () {
expectError('SELECT group FROM foo;', [
isParsingError(
message: contains('Did you mean to use it as an identifier?'),
message: contains('Did you mean to use it as a column?'),
lexeme: 'group',
),
]);