Resolve json functions when option is set (#235)

This commit is contained in:
Simon Binder 2019-11-28 21:50:55 +01:00
parent 8320ddc788
commit a0ce3421c9
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
15 changed files with 183 additions and 47 deletions

View File

@ -6,4 +6,6 @@ targets:
override_hash_and_equals_in_result_sets: true
use_column_name_as_json_key_when_defined_in_moor_file: true
generate_connect_constructor: true
write_from_json_string_constructor: true
write_from_json_string_constructor: true
sqlite_modules:
- json1

View File

@ -950,6 +950,13 @@ abstract class _$CustomTablesDb extends GeneratedDatabase {
readsFrom: {config}).map(_rowToConfig);
}
Selectable<Config> findValidJsons() {
return customSelectQuery(
'SELECT * FROM config WHERE json_valid(config_value)',
variables: [],
readsFrom: {config}).map(_rowToConfig);
}
ReadRowIdResult _rowToReadRowIdResult(QueryRow row) {
return ReadRowIdResult(
rowid: row.readInt('rowid'),

View File

@ -30,6 +30,7 @@ CREATE TABLE mytable (
readConfig: SELECT * FROM config WHERE config_key = ?;
readMultiple: SELECT * FROM config WHERE config_key IN ? ORDER BY $clause;
readDynamic: SELECT * FROM config WHERE $predicate;
findValidJsons: SELECT * FROM config WHERE json_valid(config_value);
readRowId: SELECT oid, * FROM config WHERE _rowid_ = $expr;

View File

@ -12,8 +12,8 @@ class MoorParser {
MoorParser(this.step);
Future<ParsedMoorFile> parseAndAnalyze() {
final result =
SqlEngine(useMoorExtensions: true).parseMoorFile(step.content);
final engine = step.task.session.spawnEngine();
final result = engine.parseMoorFile(step.content);
final parsedFile = result.rootNode as MoorFile;
final createdReaders = <CreateTableReader>[];

View File

@ -18,43 +18,48 @@ class MoorOptions {
/// Whether moor should generate a `fromJsonString` factory for data classes.
/// It basically wraps the regular `fromJson` constructor in a `json.decode`
/// call.
@JsonKey(name: 'write_from_json_string_constructor')
@JsonKey(name: 'write_from_json_string_constructor', defaultValue: false)
final bool generateFromJsonStringConstructor;
/// Overrides [Object.hashCode] and [Object.==] in classes generated for
/// custom queries.
@JsonKey(name: 'override_hash_and_equals_in_result_sets')
@JsonKey(name: 'override_hash_and_equals_in_result_sets', defaultValue: false)
final bool overrideHashAndEqualsInResultSets;
/// Also enable the compact query methods from moor files on queries defined
/// in a `UseMoor` annotation. Compact queries return a `Selectable` instead
/// of generating two methods (with one returning a stream and another
/// returning a future)
@JsonKey(name: 'compact_query_methods')
@JsonKey(name: 'compact_query_methods', defaultValue: false)
final bool compactQueryMethods;
/// Remove verification logic in the generated code.
@JsonKey(name: 'skip_verification_code')
@JsonKey(name: 'skip_verification_code', defaultValue: false)
final bool skipVerificationCode;
/// Use a `<data-class>Companion` pattern instead of `<table-class>Companion`
/// when naming companions.
@JsonKey(name: 'use_data_class_name_for_companions')
@JsonKey(name: 'use_data_class_name_for_companions', defaultValue: false)
final bool useDataClassNameForCompanions;
/// For a column defined in a moor file, use the name directly instead of
/// the transformed `camelCaseDartGetter`.
@JsonKey(name: 'use_column_name_as_json_key_when_defined_in_moor_file')
@JsonKey(
name: 'use_column_name_as_json_key_when_defined_in_moor_file',
defaultValue: false)
final bool useColumnNameAsJsonKeyWhenDefinedInMoorFile;
/// Generate a `connect` constructor in database superclasses. This is
/// required to run databases in a background isolate.
@JsonKey(name: 'generate_connect_constructor')
@JsonKey(name: 'generate_connect_constructor', defaultValue: false)
final bool generateConnectConstructor;
@JsonKey(name: 'sqlite_modules', defaultValue: [])
final List<SqlModule> modules;
/// Whether the [module] has been enabled in this configuration.
bool hasModule(SqlModule module) => modules.contains(module);
const MoorOptions(
{this.generateFromJsonStringConstructor = false,
this.overrideHashAndEqualsInResultSets = false,

View File

@ -20,21 +20,28 @@ MoorOptions _$MoorOptionsFromJson(Map<String, dynamic> json) {
]);
final val = MoorOptions(
generateFromJsonStringConstructor: $checkedConvert(
json, 'write_from_json_string_constructor', (v) => v as bool),
overrideHashAndEqualsInResultSets: $checkedConvert(
json, 'override_hash_and_equals_in_result_sets', (v) => v as bool),
json, 'write_from_json_string_constructor', (v) => v as bool) ??
false,
overrideHashAndEqualsInResultSets: $checkedConvert(json,
'override_hash_and_equals_in_result_sets', (v) => v as bool) ??
false,
compactQueryMethods:
$checkedConvert(json, 'compact_query_methods', (v) => v as bool),
$checkedConvert(json, 'compact_query_methods', (v) => v as bool) ??
false,
skipVerificationCode:
$checkedConvert(json, 'skip_verification_code', (v) => v as bool),
$checkedConvert(json, 'skip_verification_code', (v) => v as bool) ??
false,
useDataClassNameForCompanions: $checkedConvert(
json, 'use_data_class_name_for_companions', (v) => v as bool),
json, 'use_data_class_name_for_companions', (v) => v as bool) ??
false,
useColumnNameAsJsonKeyWhenDefinedInMoorFile: $checkedConvert(
json,
'use_column_name_as_json_key_when_defined_in_moor_file',
(v) => v as bool),
json,
'use_column_name_as_json_key_when_defined_in_moor_file',
(v) => v as bool) ??
false,
generateConnectConstructor: $checkedConvert(
json, 'generate_connect_constructor', (v) => v as bool),
json, 'generate_connect_constructor', (v) => v as bool) ??
false,
modules: $checkedConvert(
json,
'sqlite_modules',

View File

@ -4,6 +4,7 @@ import 'package:moor_generator/src/analyzer/runner/file_graph.dart';
import 'package:moor_generator/src/analyzer/runner/task.dart';
import 'package:moor_generator/src/backends/backend.dart';
import 'package:path/path.dart' as p;
import 'package:sqlparser/sqlparser.dart';
import 'options.dart';
@ -32,6 +33,14 @@ class MoorSession {
/// which assumes immutable files during a build run).
Stream<List<FoundFile>> get changedFiles => _changedFiles.stream;
/// Creates a properly configured [SqlEngine].
SqlEngine spawnEngine() {
return SqlEngine(
useMoorExtensions: true,
enableJson1Module: options.hasModule(SqlModule.json1),
);
}
FileType _findFileType(String path) {
final extension = p.extension(path);

View File

@ -18,10 +18,11 @@ class SqlParser {
final List<SqlQuery> foundQueries = [];
SqlParser(this.step, this.tables, this.definedQueries);
SqlParser(this.step, this.tables, this.definedQueries) {
_engine = step.task.session.spawnEngine();
}
void _spawnEngine() {
_engine = SqlEngine();
tables.map(_mapper.extractStructure).forEach(_engine.registerTable);
}
@ -34,20 +35,20 @@ class SqlParser {
AnalysisContext context;
if (query is DeclaredDartQuery) {
final sql = query.sql;
try {
try {
if (query is DeclaredDartQuery) {
final sql = query.sql;
context = _engine.analyze(sql);
} catch (e, s) {
step.reportError(MoorError(
severity: Severity.criticalError,
message: 'Error while trying to parse $name: $e, $s'));
return;
} else if (query is DeclaredMoorQuery) {
context =
_engine.analyzeNode(query.query, query.file.parseResult.sql);
declaredInMoor = true;
}
} else if (query is DeclaredMoorQuery) {
context = _engine.analyzeNode(query.query, query.file.parseResult.sql);
declaredInMoor = true;
} catch (e, s) {
step.reportError(MoorError(
severity: Severity.criticalError,
message: 'Error while trying to parse $name: $e, $s'));
continue;
}
for (var error in context.errors) {

View File

@ -1,3 +1,6 @@
## unreleased
- Optionally support the `json1` module
## 0.4.0
- Support common table expressions
- Handle special `rowid`, `oid`, `__rowid__` references

View File

@ -3,6 +3,7 @@ import 'dart:math' show min, max;
import 'package:meta/meta.dart';
import 'package:source_span/source_span.dart';
import 'package:sqlparser/sqlparser.dart';
import 'package:sqlparser/src/engine/options.dart';
import 'package:sqlparser/src/reader/tokenizer/token.dart';
import 'package:sqlparser/src/utils/meta.dart';

View File

@ -16,10 +16,11 @@ class AnalysisContext {
/// A resolver that can be used to obtain the type of a [Typeable]. This
/// mostly applies to [Expression]s, [Reference]s, [Variable]s and
/// [ResultSet.resolvedColumns] of a select statement.
final TypeResolver types = TypeResolver();
final TypeResolver types;
/// Constructs a new analysis context from the AST and the source sql.
AnalysisContext(this.root, this.sql);
AnalysisContext(this.root, this.sql, EngineOptions options)
: types = TypeResolver(options);
/// Reports an analysis error.
void reportError(AnalysisError error) {

View File

@ -15,6 +15,9 @@ const _comparisonOperators = [
class TypeResolver {
final Map<Typeable, ResolveResult> _results = {};
final EngineOptions options;
TypeResolver(this.options);
ResolveResult _cache<T extends Typeable>(
ResolveResult Function(T param) resolver, T typeable) {
@ -157,7 +160,9 @@ class TypeResolver {
}, l);
}
/// Expands the parameters
/// Returns the expanded parameters of a function [call]. When a
/// [StarFunctionParameter] is used, it's expanded to the
/// [ReferenceScope.availableColumns].
List<Typeable> _expandParameters(Invocation call) {
final sqlParameters = call.parameters;
if (sqlParameters is ExprFunctionParameters) {
@ -278,6 +283,34 @@ class TypeResolver {
])).withNullable(true);
}
if (options.enableJson1) {
switch (call.name.toLowerCase()) {
case 'json':
case 'json_array':
case 'json_insert':
case 'json_replace':
case 'json_set':
case 'json_object':
case 'json_patch':
case 'json_remove':
case 'json_quote':
case 'json_group_array':
case 'json_group_object':
return const ResolveResult(ResolvedType(type: BasicType.text));
case 'json_type':
return const ResolveResult(
ResolvedType(type: BasicType.text, nullable: true));
case 'json_valid':
return const ResolveResult(ResolvedType.bool());
case 'json_extract':
// return unknown so that we don't hide the state error. In reality
// we have no idea what json_extract returns
return const ResolveResult.unknown();
case 'json_array_length':
return const ResolveResult(ResolvedType(type: BasicType.int));
}
}
throw StateError('Unknown function: ${call.name}');
}, call);
}

View File

@ -0,0 +1,11 @@
class EngineOptions {
/// Moor extends the sql grammar a bit to support type converters and other
/// features. Enabling this flag will make this engine parse sql with these
/// extensions enabled.
final bool useMoorExtensions;
/// Enables functions declared in the `json1` module for analysis
final bool enableJson1;
EngineOptions(this.useMoorExtensions, this.enableJson1);
}

View File

@ -3,6 +3,7 @@ import 'dart:collection';
import 'package:sqlparser/src/analysis/analysis.dart';
import 'package:sqlparser/src/ast/ast.dart';
import 'package:sqlparser/src/engine/autocomplete/engine.dart';
import 'package:sqlparser/src/engine/options.dart';
import 'package:sqlparser/src/reader/parser/parser.dart';
import 'package:sqlparser/src/reader/tokenizer/scanner.dart';
import 'package:sqlparser/src/reader/tokenizer/token.dart';
@ -13,12 +14,11 @@ class SqlEngine {
/// All tables registered with [registerTable].
final List<Table> knownTables = [];
/// Moor extends the sql grammar a bit to support type converters and other
/// features. Enabling this flag will make this engine parse sql with these
/// extensions enabled.
final bool useMoorExtensions;
/// Internal options for this sql engine.
final EngineOptions options;
SqlEngine({this.useMoorExtensions = false}) {
SqlEngine({bool useMoorExtensions = false, bool enableJson1Module = false})
: options = EngineOptions(useMoorExtensions, enableJson1Module) {
registerTable(sqliteMaster);
registerTable(sqliteSequence);
}
@ -46,7 +46,7 @@ class SqlEngine {
/// you need to filter them. When using the methods in this class, this will
/// be taken care of automatically.
List<Token> tokenize(String source) {
final scanner = Scanner(source, scanMoorTokens: useMoorExtensions);
final scanner = Scanner(source, scanMoorTokens: options.useMoorExtensions);
final tokens = scanner.scanTokens();
if (scanner.errors.isNotEmpty) {
@ -60,7 +60,7 @@ class SqlEngine {
ParseResult parse(String sql) {
final tokens = tokenize(sql);
final tokensForParser = tokens.where((t) => !t.invisibleToParser).toList();
final parser = Parser(tokensForParser, useMoor: useMoorExtensions);
final parser = Parser(tokensForParser, useMoor: options.useMoorExtensions);
final stmt = parser.statement();
return ParseResult._(stmt, tokens, parser.errors, sql, null);
@ -69,7 +69,7 @@ class SqlEngine {
/// Parses a `.moor` file, which can consist of multiple statements and
/// additional components like import statements.
ParseResult parseMoorFile(String content) {
assert(useMoorExtensions);
assert(options.useMoorExtensions);
final tokens = tokenize(content);
final autoComplete = AutoCompleteEngine(tokens);
@ -104,7 +104,7 @@ class SqlEngine {
AnalysisContext analyzeParsed(ParseResult result) {
final node = result.rootNode;
final context = AnalysisContext(node, result.sql);
final context = AnalysisContext(node, result.sql, options);
_analyzeContext(context);
return context;
@ -119,7 +119,7 @@ class SqlEngine {
/// and result columns, so all known tables should be registered using
/// [registerTable] before calling this method.
AnalysisContext analyzeNode(AstNode node, String file) {
final context = AnalysisContext(node, file);
final context = AnalysisContext(node, file, options);
_analyzeContext(context);
return context;
}

View File

@ -0,0 +1,55 @@
import 'package:sqlparser/sqlparser.dart';
import 'package:test/test.dart';
void main() {
group('resolves return types of json functions', () {
ResolveResult findResult(String expression) {
final engine = SqlEngine(enableJson1Module: true);
final result = engine.analyze('SELECT $expression;');
final select = result.root as SelectStatement;
final column = select.resultSet.resolvedColumns.single;
return result.typeOf(column);
}
const resolvedString = ResolveResult(ResolvedType(type: BasicType.text));
test('create json', () {
expect(findResult("json('{}')"), resolvedString);
expect(findResult("json_array('foo', 'bar')"), resolvedString);
expect(findResult("json_insert('{}')"), resolvedString);
expect(findResult("json_replace('{}')"), resolvedString);
expect(findResult("json_set('{}')"), resolvedString);
expect(findResult('json_object()'), resolvedString);
expect(findResult("json_patch('{}', '{}')"), resolvedString);
expect(findResult("json_remove('{}', '{}')"), resolvedString);
expect(findResult("json_quote('foo')"), resolvedString);
expect(findResult('json_group_array()'), resolvedString);
expect(findResult('json_group_object()'), resolvedString);
});
test('json_type', () {
expect(
findResult("json_type('foo', 'bar')"),
const ResolveResult(ResolvedType(type: BasicType.text, nullable: true)),
);
});
test('json_valid', () {
expect(
findResult('json_valid()'), const ResolveResult(ResolvedType.bool()));
});
test('json_extract', () {
expect(findResult('json_extract()'), const ResolveResult.unknown());
});
test('json_array_length', () {
expect(
findResult('json_array_length()'),
const ResolveResult(ResolvedType(type: BasicType.int)),
);
});
});
}