import 'dart:convert'; import 'package:charcode/ascii.dart'; import 'package:intl/intl.dart' show DateFormat; import 'package:string_scanner/string_scanner.dart'; import 'query.dart'; final DateFormat dateYmd = new DateFormat('yyyy-MM-dd'); final DateFormat dateYmdHms = new DateFormat('yyyy-MM-dd HH:mm:ss'); /// The ORM prefers using substitution values, which allow for prepared queries, /// and prevent SQL injection attacks. @deprecated String sanitizeExpression(String unsafe) { var buf = new StringBuffer(); var scanner = new StringScanner(unsafe); int ch; while (!scanner.isDone) { // Ignore comment starts if (scanner.scan('--') || scanner.scan('/*')) continue; // Ignore all single quotes and attempted escape sequences else if (scanner.scan("'") || scanner.scan('\\')) continue; // Otherwise, add the next char, unless it's a null byte. else if ((ch = scanner.readChar()) != $nul && ch != null) buf.writeCharCode(ch); } return toSql(buf.toString(), withQuotes: false); } abstract class SqlExpressionBuilder<T> { final Query query; final String columnName; String _cast; bool _isProperty = false; String _substitution; SqlExpressionBuilder(this.query, this.columnName); String get substitution { var c = _isProperty ? 'prop' : columnName; return _substitution ??= query.reserveName(c); } bool get hasValue; String compile(); } class NumericSqlExpressionBuilder<T extends num> extends SqlExpressionBuilder<T> { bool _hasValue = false; String _op = '='; String _raw; T _value; NumericSqlExpressionBuilder(Query query, String columnName) : super(query, columnName); @override bool get hasValue => _hasValue; bool _change(String op, T value) { _raw = null; _op = op; _value = value; return _hasValue = true; } @override String compile() { if (_raw != null) return _raw; if (_value == null) return null; var v = _value.toString(); if (T == double) v = 'CAST ("$v" as decimal)'; if (_cast != null) v = 'CAST ($v AS $_cast)'; return '$_op $v'; } operator <(T value) => _change('<', value); operator >(T value) => _change('>', value); operator <=(T value) => _change('<=', value); operator >=(T value) => _change('>=', value); void lessThan(T value) { _change('<', value); } void lessThanOrEqualTo(T value) { _change('<=', value); } void greaterThan(T value) { _change('>', value); } void greaterThanOrEqualTo(T value) { _change('>=', value); } void equals(T value) { _change('=', value); } void notEquals(T value) { _change('!=', value); } void isBetween(T lower, T upper) { _raw = 'BETWEEN $lower AND $upper'; _hasValue = true; } void isNotBetween(T lower, T upper) { _raw = 'NOT BETWEEN $lower AND $upper'; _hasValue = true; } void isIn(Iterable<T> values) { _raw = 'IN (' + values.join(', ') + ')'; _hasValue = true; } void isNotIn(Iterable<T> values) { _raw = 'NOT IN (' + values.join(', ') + ')'; _hasValue = true; } } class EnumSqlExpressionBuilder<T> extends SqlExpressionBuilder<T> { final int Function(T) _getValue; bool _hasValue = false; String _op = '='; String _raw; int _value; EnumSqlExpressionBuilder(Query query, String columnName, this._getValue) : super(query, columnName); @override bool get hasValue => _hasValue; bool _change(String op, T value) { _raw = null; _op = op; _value = _getValue(value); return _hasValue = true; } UnsupportedError _unsupported() => UnsupportedError('Enums do not support this operation.'); @override String compile() { if (_raw != null) return _raw; if (_value == null) return null; return '$_op $_value'; } void isNull() { _hasValue = true; _raw = 'IS NOT NULL'; } void isNotNull() { _hasValue = true; _raw = 'IS NOT NULL'; } void equals(T value) { _change('=', value); } void notEquals(T value) { _change('!=', value); } void isBetween(T lower, T upper) => throw _unsupported(); void isNotBetween(T lower, T upper) => throw _unsupported(); void isIn(Iterable<T> values) { _raw = 'IN (' + values.map(_getValue).join(', ') + ')'; _hasValue = true; } void isNotIn(Iterable<T> values) { _raw = 'NOT IN (' + values.map(_getValue).join(', ') + ')'; _hasValue = true; } } class StringSqlExpressionBuilder extends SqlExpressionBuilder<String> { bool _hasValue = false; String _op = '=', _raw, _value; StringSqlExpressionBuilder(Query query, String columnName) : super(query, columnName); @override bool get hasValue => _hasValue; String get lowerName => '${substitution}_lower'; String get upperName => '${substitution}_upper'; bool _change(String op, String value) { _raw = null; _op = op; _value = value; query.substitutionValues[substitution] = _value; return _hasValue = true; } @override String compile() { if (_raw != null) return _raw; if (_value == null) return null; return "$_op @$substitution"; } void isEmpty() => equals(''); void equals(String value) { _change('=', value); } void notEquals(String value) { _change('!=', value); } /// Builds a `LIKE` predicate. /// /// To prevent injections, the [pattern] is called with a name that /// will be escaped by the underlying [QueryExecutor]. /// /// Example: /// ```dart /// carNameBuilder.like((name) => 'Mazda %$name%'); /// ``` void like(String Function(String) pattern) { _raw = 'LIKE \'' + pattern('@$substitution') + '\''; query.substitutionValues[substitution] = _value; _hasValue = true; } void isBetween(String lower, String upper) { query.substitutionValues[lowerName] = lower; query.substitutionValues[upperName] = upper; _raw = "BETWEEN @$lowerName AND @$upperName"; _hasValue = true; } void isNotBetween(String lower, String upper) { query.substitutionValues[lowerName] = lower; query.substitutionValues[upperName] = upper; _raw = "NOT BETWEEN @$lowerName AND @$upperName"; _hasValue = true; } String _in(Iterable<String> values) { return 'IN (' + values.map((v) { var name = query.reserveName('${columnName}_in_value'); query.substitutionValues[name] = v; return '@$name'; }).join(', ') + ')'; } void isIn(Iterable<String> values) { _raw = _in(values); _hasValue = true; } void isNotIn(Iterable<String> values) { _raw = 'NOT ' + _in(values); _hasValue = true; } } class BooleanSqlExpressionBuilder extends SqlExpressionBuilder<bool> { bool _hasValue = false; String _op = '=', _raw; bool _value; BooleanSqlExpressionBuilder(Query query, String columnName) : super(query, columnName); @override bool get hasValue => _hasValue; bool _change(String op, bool value) { _raw = null; _op = op; _value = value; return _hasValue = true; } @override String compile() { if (_raw != null) return _raw; if (_value == null) return null; var v = _value ? 'TRUE' : 'FALSE'; if (_cast != null) v = 'CAST ($v AS $_cast)'; return '$_op $v'; } Null get isTrue => equals(true); Null get isFalse => equals(false); void equals(bool value) { _change('=', value); } void notEquals(bool value) { _change('!=', value); } } class DateTimeSqlExpressionBuilder extends SqlExpressionBuilder<DateTime> { NumericSqlExpressionBuilder<int> _year, _month, _day, _hour, _minute, _second; String _raw; DateTimeSqlExpressionBuilder(Query query, String columnName) : super(query, columnName); NumericSqlExpressionBuilder<int> get year => _year ??= new NumericSqlExpressionBuilder(query, 'year'); NumericSqlExpressionBuilder<int> get month => _month ??= new NumericSqlExpressionBuilder(query, 'month'); NumericSqlExpressionBuilder<int> get day => _day ??= new NumericSqlExpressionBuilder(query, 'day'); NumericSqlExpressionBuilder<int> get hour => _hour ??= new NumericSqlExpressionBuilder(query, 'hour'); NumericSqlExpressionBuilder<int> get minute => _minute ??= new NumericSqlExpressionBuilder(query, 'minute'); NumericSqlExpressionBuilder<int> get second => _second ??= new NumericSqlExpressionBuilder(query, 'second'); @override bool get hasValue => _raw?.isNotEmpty == true || _year?.hasValue == true || _month?.hasValue == true || _day?.hasValue == true || _hour?.hasValue == true || _minute?.hasValue == true || _second?.hasValue == true; bool _change(String _op, DateTime dt, bool time) { var dateString = time ? dateYmdHms.format(dt) : dateYmd.format(dt); _raw = '$columnName $_op \'$dateString\''; return true; } operator <(DateTime value) => _change('<', value, true); operator <=(DateTime value) => _change('<=', value, true); operator >(DateTime value) => _change('>', value, true); operator >=(DateTime value) => _change('>=', value, true); void equals(DateTime value, {bool includeTime = true}) { _change('=', value, includeTime != false); } void lessThan(DateTime value, {bool includeTime = true}) { _change('<', value, includeTime != false); } void lessThanOrEqualTo(DateTime value, {bool includeTime = true}) { _change('<=', value, includeTime != false); } void greaterThan(DateTime value, {bool includeTime = true}) { _change('>', value, includeTime != false); } void greaterThanOrEqualTo(DateTime value, {bool includeTime = true}) { _change('>=', value, includeTime != false); } void isIn(Iterable<DateTime> values) { _raw = '$columnName IN (' + values.map(dateYmdHms.format).map((s) => '$s').join(', ') + ')'; } void isNotIn(Iterable<DateTime> values) { _raw = '$columnName NOT IN (' + values.map(dateYmdHms.format).map((s) => '$s').join(', ') + ')'; } void isBetween(DateTime lower, DateTime upper) { var l = dateYmdHms.format(lower), u = dateYmdHms.format(upper); _raw = "$columnName BETWEEN '$l' and '$u'"; } void isNotBetween(DateTime lower, DateTime upper) { var l = dateYmdHms.format(lower), u = dateYmdHms.format(upper); _raw = "$columnName NOT BETWEEN '$l' and '$u'"; } @override String compile() { if (_raw?.isNotEmpty == true) return _raw; List<String> parts = []; if (year?.hasValue == true) parts.add('YEAR($columnName) ${year.compile()}'); if (month?.hasValue == true) parts.add('MONTH($columnName) ${month.compile()}'); if (day?.hasValue == true) parts.add('DAY($columnName) ${day.compile()}'); if (hour?.hasValue == true) parts.add('HOUR($columnName) ${hour.compile()}'); if (minute?.hasValue == true) parts.add('MINUTE($columnName) ${minute.compile()}'); if (second?.hasValue == true) parts.add('SECOND($columnName) ${second.compile()}'); return parts.isEmpty ? null : parts.join(' AND '); } } abstract class JsonSqlExpressionBuilder<T, K> extends SqlExpressionBuilder<T> { final List<JsonSqlExpressionBuilderProperty> _properties = []; bool _hasValue = false; T _value; String _op; String _raw; JsonSqlExpressionBuilder(Query query, String columnName) : super(query, columnName); JsonSqlExpressionBuilderProperty operator [](K name) { var p = _property(name); _properties.add(p); return p; } JsonSqlExpressionBuilderProperty _property(K name); bool get hasRaw => _raw != null || _properties.any((p) => p.hasValue); @override bool get hasValue => _hasValue || _properties.any((p) => p.hasValue); _encodeValue(T v) => v; bool _change(String op, T value) { _raw = null; _op = op; _value = value; query.substitutionValues[substitution] = _encodeValue(_value); return _hasValue = true; } @override String compile() { var s = _compile(); if (!_properties.any((p) => p.hasValue)) return s; s ??= ''; for (var p in _properties) { if (p.hasValue) { var c = p.compile(); if (c != null) { _hasValue = true; s ??= ''; if (p.typed is! DateTimeSqlExpressionBuilder) { s += '${p.typed.columnName} '; } s += c; } } } return s; } String _compile() { if (_raw != null) return _raw; if (_value == null) return null; return "::jsonb $_op @$substitution::jsonb"; } void contains(T value) { _change('@>', value); } void equals(T value) { _change('=', value); } } class MapSqlExpressionBuilder extends JsonSqlExpressionBuilder<Map, String> { MapSqlExpressionBuilder(Query query, String columnName) : super(query, columnName); @override JsonSqlExpressionBuilderProperty _property(String name) { return JsonSqlExpressionBuilderProperty(this, name, false); } void containsKey(String key) { this[key].isNotNull(); } void containsPair(key, value) { contains({key: value}); } } class ListSqlExpressionBuilder extends JsonSqlExpressionBuilder<List, int> { ListSqlExpressionBuilder(Query query, String columnName) : super(query, columnName); @override _encodeValue(List v) => json.encode(v); @override JsonSqlExpressionBuilderProperty _property(int name) { return JsonSqlExpressionBuilderProperty(this, name.toString(), true); } } class JsonSqlExpressionBuilderProperty { final JsonSqlExpressionBuilder builder; final String name; final bool isInt; SqlExpressionBuilder _typed; JsonSqlExpressionBuilderProperty(this.builder, this.name, this.isInt); SqlExpressionBuilder get typed => _typed; bool get hasValue => _typed?.hasValue == true; String compile() => _typed?.compile(); T _set<T extends SqlExpressionBuilder>(T Function() value) { if (_typed is T) { return _typed as T; } else if (_typed != null) { throw StateError( '$nameString is already typed as $_typed, and cannot be changed.'); } else { _typed = value() .._cast = 'text' .._isProperty = true; return _typed as T; } } String get nameString { var n = isInt ? name : "'$name'"; return '${builder.columnName}::jsonb->>$n'; } void isNotNull() { builder .._hasValue = true .._raw ??= '' .._raw += "$nameString IS NOT NULL"; } void isNull() { builder .._hasValue = true .._raw ??= '' .._raw += "$nameString IS NULL"; } StringSqlExpressionBuilder get asString { return _set(() => StringSqlExpressionBuilder(builder.query, nameString)); } BooleanSqlExpressionBuilder get asBool { return _set(() => BooleanSqlExpressionBuilder(builder.query, nameString)); } DateTimeSqlExpressionBuilder get asDateTime { return _set(() => DateTimeSqlExpressionBuilder(builder.query, nameString)); } NumericSqlExpressionBuilder<double> get asDouble { return _set( () => NumericSqlExpressionBuilder<double>(builder.query, nameString)); } NumericSqlExpressionBuilder<int> get asInt { return _set( () => NumericSqlExpressionBuilder<int>(builder.query, nameString)); } MapSqlExpressionBuilder get asMap { return _set(() => MapSqlExpressionBuilder(builder.query, nameString)); } ListSqlExpressionBuilder get asList { return _set(() => ListSqlExpressionBuilder(builder.query, nameString)); } }