Tests for generalized nested columns

This commit is contained in:
Simon Binder 2023-07-24 20:08:49 +02:00
parent 5b7e011021
commit 941409381b
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
8 changed files with 450 additions and 217 deletions

View File

@ -198,18 +198,6 @@ class _LintingVisitor extends RecursiveVisitor<void, void> {
relevantNode: e,
));
}
// check that it actually refers to a table
final result = e.resultSet?.unalias();
if (result is! Table && result is! View) {
linter.sqlParserErrors.add(AnalysisError(
type: AnalysisErrorType.other,
message: 'Nested star columns must refer to a table directly. They '
"can't refer to a table-valued function or another select "
'statement.',
relevantNode: e,
));
}
}
if (e is NestedQueryColumn) {

View File

@ -561,8 +561,11 @@ class InferredResultSet {
) {
return switch (column) {
ScalarResultColumn() => column,
NestedResultTable() => column.innerResultSet
NestedResultTable() => StructuredFromNestedColumn(
column,
column.innerResultSet
.mappingToRowClass(column.nameForGeneratedRowClass, options),
),
NestedResultQuery() => MappedNestedListQuery(
column,
column.query.queryRowType(options),

View File

@ -130,21 +130,40 @@ class QueryWriter {
(sqlPrefix: prefix, isNullable: argument.nullable),
);
case MappedNestedListQuery():
final queryRow = _emitter.drift('QueryRow');
_buffer.write('await ');
_writeCustomSelectStatement(argument.column.query,
includeMappingToDart: false);
_buffer.write('.map(');
_buffer.write('($queryRow row) => ');
_writeArgumentExpression(argument.nestedType, resultSet, context);
_buffer.write(').get()');
final query = argument.column.query;
_writeCustomSelectStatement(query);
_buffer.write('.get()');
case QueryRowType():
final singleValue = argument.singleValue;
if (singleValue != null) {
return _writeArgumentExpression(singleValue, resultSet, context);
}
if (context.isNullable) {
// If this structed type is nullable, it's coming from an OUTER join
// which means that, even if the individual components making up the
// structure are non-nullable, they might all be null in SQL. We
// detect this case by looking for a non-nullable column and, if it's
// null, return null directly instead of creating the structured type.
for (final arg in argument.positionalArguments
.followedBy(argument.namedArguments.values)
.whereType<ScalarResultColumn>()) {
if (!arg.nullable) {
final keyInMap = context.applyPrefix(arg.name);
_buffer.write(
'row.data[${asDartLiteral(keyInMap)}] == null ? null : ');
}
}
}
final _ArgumentContext childContext = (
sqlPrefix: context.sqlPrefix,
// Individual fields making up this query row type aren't covered by
// the outer nullability.
isNullable: false,
);
if (!argument.isRecord) {
// We're writing a constructor, so let's start with the class name.
_emitter.writeDart(argument.rowType);
@ -159,12 +178,12 @@ class QueryWriter {
_buffer.write('(');
for (final positional in argument.positionalArguments) {
_writeArgumentExpression(positional, resultSet, context);
_writeArgumentExpression(positional, resultSet, childContext);
_buffer.write(', ');
}
argument.namedArguments.forEach((name, parameter) {
_buffer.write('$name: ');
_writeArgumentExpression(parameter, resultSet, context);
_writeArgumentExpression(parameter, resultSet, childContext);
_buffer.write(', ');
});
@ -179,7 +198,12 @@ class QueryWriter {
final specialName = _transformer.newNameFor(column.sqlParserColumn!);
final isNullable = context.isNullable || column.nullable;
final dartLiteral = asDartLiteral(specialName ?? column.name);
var name = specialName ?? column.name;
if (context.sqlPrefix != null) {
name = '${context.sqlPrefix}.$name';
}
final dartLiteral = asDartLiteral(name);
final method = isNullable ? 'readNullable' : 'read';
final rawDartType =
_emitter.dartCode(AnnotatedDartCode([dartTypeNames[column.sqlType]!]));
@ -213,22 +237,20 @@ class QueryWriter {
context.isNullable ? 'mapFromRowOrNull' : 'mapFromRow';
final sqlPrefix = context.sqlPrefix;
_emitter.write('${table.dbGetterName}.$mappingMethod(row');
_emitter.write('await ${table.dbGetterName}.$mappingMethod(row');
if (sqlPrefix != null) {
_emitter.write(', tablePrefix: ${asDartLiteral(sqlPrefix)}');
}
_emitter.write(')');
} else {
final sqlPrefix = context.sqlPrefix;
// If the entire table can be nullable, we can check whether a non-nullable
// column from the table is null. If it is, the entire table is null. This
// can happen when the table comes from an outer join.
if (context.isNullable) {
for (final MapEntry(:key, :value) in match.aliasToColumn.entries) {
if (!value.nullable) {
final mapKey = sqlPrefix == null ? key : '$sqlPrefix.$key';
final mapKey = context.applyPrefix(key);
_emitter
.write('row.data[${asDartLiteral(mapKey)}] == null ? null : ');
@ -239,13 +261,8 @@ class QueryWriter {
_emitter.write('${table.dbGetterName}.mapFromRowWithAlias(row, const {');
for (final alias in match.aliasToColumn.entries) {
var sqlKey = alias.key;
if (sqlPrefix != null) {
sqlKey = '$sqlPrefix.';
}
_emitter
..write(asDartLiteral(sqlKey))
..write(asDartLiteral(context.applyPrefix(alias.key)))
..write(': ')
..write(asDartLiteral(alias.value.nameInSql))
..write(', ');
@ -280,14 +297,12 @@ class QueryWriter {
_buffer.write(';\n}\n');
}
void _writeCustomSelectStatement(SqlSelectQuery select,
{bool includeMappingToDart = true}) {
void _writeCustomSelectStatement(SqlSelectQuery select) {
_buffer.write(' customSelect(${_queryCode(select)}, ');
_writeVariables(select);
_buffer.write(', ');
_writeReadsFrom(select);
if (includeMappingToDart) {
if (select.needsAsyncMapping) {
_buffer.write(').asyncMap(');
} else {
@ -295,8 +310,6 @@ class QueryWriter {
}
_writeMappingLambda(select);
}
_buffer.write(')');
}
@ -811,3 +824,12 @@ String? _defaultForDartPlaceholder(
return null;
}
}
extension on _ArgumentContext {
String applyPrefix(String originalName) {
return switch (sqlPrefix) {
null => originalName,
var s => '$s.$originalName',
};
}
}

View File

@ -1,4 +1,6 @@
import 'package:drift_dev/src/analysis/options.dart';
import 'package:drift_dev/src/analysis/results/results.dart';
import 'package:sqlparser/sqlparser.dart';
import 'package:test/test.dart';
import '../../test_utils.dart';
@ -225,16 +227,17 @@ class MyQueryRow {
);
});
test('nested - single column type', () async {
group('nested column', () {
test('single column into field', () async {
final state = TestBackend.inTest({
'a|lib/a.drift': '''
import 'a.dart';
foo WITH MyQueryRow: SELECT 1 a, LIST(SELECT 2 AS b) c;
foo WITH MyQueryRow: SELECT 1 AS a, b.** FROM (SELECT 2 AS b) b;
''',
'a|lib/a.dart': '''
class MyQueryRow {
MyQueryRow(int a, List<int> c);
MyQueryRow(int a, int b);
}
''',
});
@ -249,10 +252,8 @@ class MyQueryRow {
type: 'MyQueryRow',
positional: [
scalarColumn('a'),
nestedListQuery(
'c',
structedFromNested(
isExistingRowType(
type: 'int',
singleValue: scalarColumn('b'),
),
),
@ -261,7 +262,88 @@ class MyQueryRow {
);
});
test('nested - table', () async {
test('single column into single-element record', () async {
final state = TestBackend.inTest({
'a|lib/a.drift': '''
import 'a.dart';
foo WITH MyQueryRow: SELECT 1 AS a, b.** FROM (SELECT 2 AS b) b;
''',
'a|lib/a.dart': '''
class MyQueryRow {
MyQueryRow(int a, (int) b);
}
''',
});
final file = await state.analyze('package:a/a.drift');
state.expectNoErrors();
final query = file.fileAnalysis!.resolvedQueries.values.single;
expect(
query.resultSet?.existingRowType,
isExistingRowType(
type: 'MyQueryRow',
positional: [
scalarColumn('a'),
structedFromNested(
isExistingRowType(
positional: [scalarColumn('b')],
isRecord: isTrue,
),
),
],
),
);
});
test('custom result set', () async {
final state = TestBackend.inTest(
{
'a|lib/a.drift': '''
import 'a.dart';
foo WITH MyQueryRow: SELECT 1 AS id, j.** FROM json_each('') AS j;
''',
'a|lib/a.dart': '''
class MyQueryRow {
MyQueryRow(int id, JsonStructure j);
}
class JsonStructure {
JsonStructure(DriftAny key, DriftAny value, String type);
}
''',
},
options: const DriftOptions.defaults(
sqliteAnalysisOptions: SqliteAnalysisOptions(
// Make sure json_each is supported
version: SqliteVersion.v3(38),
),
),
);
final file = await state.analyze('package:a/a.drift');
state.expectNoErrors();
final query = file.fileAnalysis!.resolvedQueries.values.single;
expect(
query.resultSet?.existingRowType,
isExistingRowType(
type: 'MyQueryRow',
positional: [
scalarColumn('id'),
structedFromNested(
isExistingRowType(
type: 'JsonStructure',
),
),
],
),
);
});
test('table', () async {
final state = TestBackend.inTest({
'a|lib/a.drift': '''
import 'a.dart';
@ -299,7 +381,7 @@ class MyRow {
);
});
test('nested - table as alternative to row class', () async {
test('table as alternative to row class', () async {
final state = TestBackend.inTest(
{
'a|lib/a.drift': '''
@ -339,8 +421,46 @@ class MyRow {
),
);
});
});
test('nested - custom result set with class', () async {
group('nested LIST query', () {
test('single column type', () async {
final state = TestBackend.inTest({
'a|lib/a.drift': '''
import 'a.dart';
foo WITH MyQueryRow: SELECT 1 a, LIST(SELECT 2 AS b) c;
''',
'a|lib/a.dart': '''
class MyQueryRow {
MyQueryRow(int a, List<int> c);
}
''',
});
final file = await state.analyze('package:a/a.drift');
state.expectNoErrors();
final query = file.fileAnalysis!.resolvedQueries.values.single;
expect(
query.resultSet?.existingRowType,
isExistingRowType(
type: 'MyQueryRow',
positional: [
scalarColumn('a'),
nestedListQuery(
'c',
isExistingRowType(
type: 'int',
singleValue: scalarColumn('b'),
),
),
],
),
);
});
test('custom result set with class', () async {
final state = TestBackend.inTest({
'a|lib/a.drift': '''
import 'a.dart';
@ -382,7 +502,7 @@ class MyNestedTable {
);
});
test('nested - custom result set with record', () async {
test('custom result set with record', () async {
final state = TestBackend.inTest(
{
'a|lib/a.drift': '''
@ -422,6 +542,7 @@ class MyRow {
),
);
});
});
test('into record', () async {
final state = TestBackend.inTest(

View File

@ -75,22 +75,6 @@ q: SELECT * FROM t WHERE i IN ?1;
expect(result.allErrors, isEmpty);
});
test('warns when nested results refer to table-valued functions', () async {
final result = await TestBackend.analyzeSingle(
"a: SELECT json_each.** FROM json_each('');",
options: DriftOptions.defaults(modules: [SqlModule.json1]),
);
expect(
result.allErrors,
[
isDriftError(
contains('Nested star columns must refer to a table directly.'))
.withSpan('json_each.**')
],
);
});
test('warns about default values outside of expressions', () async {
final state = TestBackend.inTest({
'foo|lib/a.drift': r'''

View File

@ -108,6 +108,36 @@ query: SELECT foo.**, bar.** FROM my_view foo, my_view bar;
);
});
test('infers nested result sets for custom result sets', () async {
final state = TestBackend.inTest({
'foo|lib/main.drift': r'''
query: SELECT 1 AS a, b.** FROM (SELECT 2 AS b, 3 AS c) AS b;
''',
});
final file = await state.analyze('package:foo/main.drift');
state.expectNoErrors();
final query = file.fileAnalysis!.resolvedQueries.values.single;
expect(
query.resultSet!.mappingToRowClass('Row', const DriftOptions.defaults()),
isExistingRowType(
type: 'Row',
named: {
'a': scalarColumn('a'),
'b': isExistingRowType(
type: 'QueryNestedColumn0',
named: {
'b': scalarColumn('b'),
'c': scalarColumn('c'),
},
)
},
),
);
});
for (final dateTimeAsText in [false, true]) {
test('analyzing date times (stored as text: $dateTimeAsText)', () async {
final state = TestBackend.inTest(

View File

@ -33,6 +33,7 @@ TypeMatcher<QueryRowType> isExistingRowType({
Object? singleValue,
Object? positional,
Object? named,
Object? isRecord,
}) {
var matcher = isA<QueryRowType>();
@ -53,6 +54,9 @@ TypeMatcher<QueryRowType> isExistingRowType({
if (named != null) {
matcher = matcher.having((e) => e.namedArguments, 'namedArguments', named);
}
if (isRecord != null) {
matcher = matcher.having((e) => e.isRecord, 'isRecord', isRecord);
}
return matcher;
}

View File

@ -3,6 +3,7 @@ import 'package:drift_dev/src/analysis/options.dart';
import 'package:drift_dev/src/writer/import_manager.dart';
import 'package:drift_dev/src/writer/queries/query_writer.dart';
import 'package:drift_dev/src/writer/writer.dart';
import 'package:sqlparser/sqlparser.dart';
import 'package:test/test.dart';
import '../../analysis/test_utils.dart';
@ -14,6 +15,7 @@ void main() {
final state =
TestBackend.inTest({'a|lib/main.drift': driftFile}, options: options);
final file = await state.analyze('package:a/main.drift');
state.expectNoErrors();
final writer = Writer(
const DriftOptions.defaults(generateNamedParameters: true),
@ -55,7 +57,8 @@ void main() {
);
});
test('generates correct name for renamed nested star columns', () async {
group('nested star column', () {
test('get renamed in SQL', () async {
final generated = await generateForQueryInDriftFile('''
CREATE TABLE tbl (
id INTEGER NULL
@ -72,15 +75,94 @@ void main() {
);
});
test('generates correct returning mapping', () async {
test('makes single columns nullable if from outer join', () async {
final generated = await generateForQueryInDriftFile('''
query: SELECT 1 AS r, joined.** FROM (SELECT 1)
LEFT OUTER JOIN (SELECT 2 AS b) joined;
''');
expect(
generated,
allOf(
contains("joined: row.readNullable<int>('nested_0.b')"),
contains('final int? joined;'),
),
);
});
test('checks for nullable column in nested table', () async {
final generated = await generateForQueryInDriftFile('''
CREATE TABLE tbl (
id INTEGER NULL
);
query: SELECT 1 AS a, tbl.** FROM (SELECT 1) LEFT OUTER JOIN tbl;
''');
expect(
generated,
allOf(
contains(
"tbl: await tbl.mapFromRowOrNull(row, tablePrefix: 'nested_0')"),
contains('final TblData? tbl;'),
),
);
});
test('checks for nullable column in nested table with alias', () async {
final generated = await generateForQueryInDriftFile('''
CREATE TABLE tbl (
id INTEGER NULL,
col TEXT NOT NULL
);
query: SELECT 1 AS a, tbl.** FROM (SELECT 1) LEFT OUTER JOIN (SELECT id AS a, col AS b from tbl) tbl;
''');
expect(
generated,
allOf(
contains("tbl: row.data['nested_0.b'] == null ? null : "
'tbl.mapFromRowWithAlias(row'),
contains('final TblData? tbl;'),
),
);
});
test('checks for nullable column in nested result set', () async {
final generated = await generateForQueryInDriftFile('''
query: SELECT 1 AS r, joined.** FROM (SELECT 1)
LEFT OUTER JOIN (SELECT NULL AS b, 3 AS c) joined;
''');
expect(
generated,
allOf(
contains("joined: row.data['nested_0.c'] == null ? null : "
"QueryNestedColumn0(b: row.readNullable<String>('nested_0.b'), "
"c: row.read<int>('nested_0.c'), )"),
contains('final QueryNestedColumn0? joined;'),
),
);
});
});
test('generates correct returning mapping', () async {
final generated = await generateForQueryInDriftFile(
'''
CREATE TABLE tbl (
id INTEGER,
text TEXT
);
query: INSERT INTO tbl (id, text) VALUES(10, 'test') RETURNING id;
''');
''',
options: const DriftOptions.defaults(
sqliteAnalysisOptions:
// Assuming 3.35 because dso that returning works.
SqliteAnalysisOptions(version: SqliteVersion.v3(35)),
),
);
expect(generated, contains('.toList()'));
});
@ -346,8 +428,7 @@ failQuery:
],
readsFrom: {
t,
}).asyncMap((i0.QueryRow row) async {
return FailQueryResult(
}).asyncMap((i0.QueryRow row) async => FailQueryResult(
a: row.readNullable<double>('a'),
b: row.readNullable<int>('b'),
nestedQuery0: await customSelect(
@ -358,8 +439,8 @@ failQuery:
readsFrom: {
t,
}).asyncMap(t.mapFromRow).get(),
);
});
));
}
'''))
}, outputs.dartOutputs, outputs);
});