Override hashCode and operator == in common expressions

This commit is contained in:
Simon Binder 2019-11-16 20:54:34 +01:00
parent 7609df34f0
commit 0a96769dcb
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
22 changed files with 218 additions and 6 deletions

View File

@ -55,4 +55,12 @@ class _NotExpression extends Expression<bool, BoolType> {
context.buffer.write('NOT ');
writeInner(context, inner);
}
@override
int get hashCode => inner.hashCode << 1;
@override
bool operator ==(other) {
return other is _NotExpression && other.inner == inner;
}
}

View File

@ -104,4 +104,17 @@ class _BetweenExpression extends Expression<bool, BoolType> {
context.buffer.write(' AND ');
writeInner(context, higher);
}
@override
int get hashCode => $mrjf($mrjc(target.hashCode,
$mrjc(lower.hashCode, $mrjc(higher.hashCode, not.hashCode))));
@override
bool operator ==(other) {
return other is _BetweenExpression &&
other.target == target &&
other.not == not &&
other.lower == lower &&
other.higher == higher;
}
}

View File

@ -16,4 +16,14 @@ class CustomExpression<D, S extends SqlType<D>> extends Expression<D, S> {
@override
void writeInto(GenerationContext context) => context.buffer.write(content);
@override
int get hashCode => content.hashCode * 3;
@override
bool operator ==(other) {
return other.runtimeType == runtimeType &&
// ignore: test_types_in_equals
(other as CustomExpression).content == content;
}
}

View File

@ -103,4 +103,14 @@ class _StrftimeSingleFieldExpression extends Expression<int, IntType> {
date.writeInto(context);
context.buffer.write(', "unixepoch") AS INTEGER)');
}
@override
int get hashCode => $mrjf($mrjc(format.hashCode, date.hashCode));
@override
bool operator ==(other) {
return other is _StrftimeSingleFieldExpression &&
other.format == format &&
other.date == date;
}
}

View File

@ -1,5 +1,7 @@
part of '../query_builder.dart';
const _equality = ListEquality();
/// Any sql expression that evaluates to some generic value. This does not
/// include queries (which might evaluate to multiple values) but individual
/// columns, functions and operators.
@ -38,13 +40,13 @@ abstract class Expression<D, T extends SqlType<D>> implements Component {
/// An expression that is true if `this` resolves to any of the values in
/// [values].
Expression<bool, BoolType> isIn(Iterable<D> values) {
return _InExpression(this, values, false);
return _InExpression(this, values.toList(), false);
}
/// An expression that is true if `this` does not resolve to any of the values
/// in [values].
Expression<bool, BoolType> isNotIn(Iterable<D> values) {
return _InExpression(this, values, true);
return _InExpression(this, values.toList(), true);
}
/// Writes this expression into the [GenerationContext], assuming that there's
@ -175,6 +177,18 @@ abstract class _InfixOperator<D, T extends SqlType<D>>
context.writeWhitespace();
writeInner(context, right);
}
@override
int get hashCode =>
$mrjf($mrjc(left.hashCode, $mrjc(right.hashCode, operator.hashCode)));
@override
bool operator ==(other) {
return other is _InfixOperator &&
other.left == left &&
other.right == right &&
other.operator == operator;
}
}
class _BaseInfixOperator<D, T extends SqlType<D>> extends _InfixOperator<D, T> {
@ -263,6 +277,14 @@ class _UnaryMinus<DT, ST extends SqlType<DT>> extends Expression<DT, ST> {
context.buffer.write('-');
inner.writeInto(context);
}
@override
int get hashCode => inner.hashCode * 5;
@override
bool operator ==(other) {
return other is _UnaryMinus && other.inner == inner;
}
}
class _CastExpression<D1, D2, S1 extends SqlType<D1>, S2 extends SqlType<D2>>
@ -281,6 +303,14 @@ class _CastExpression<D1, D2, S1 extends SqlType<D1>, S2 extends SqlType<D2>>
void writeInto(GenerationContext context) {
return inner.writeInto(context);
}
@override
int get hashCode => inner.hashCode * 7;
@override
bool operator ==(other) {
return other is _CastExpression && other.inner == inner;
}
}
class _FunctionCallExpression<R, S extends SqlType<R>>
@ -308,4 +338,15 @@ class _FunctionCallExpression<R, S extends SqlType<R>>
context.buffer.write(')');
}
@override
int get hashCode =>
$mrjf($mrjc(functionName.hashCode, _equality.hash(arguments)));
@override
bool operator ==(other) {
return other is _FunctionCallExpression &&
other.functionName == functionName &&
_equality.equals(other.arguments, arguments);
}
}

View File

@ -23,7 +23,7 @@ Expression<bool, BoolType> isNotIn<X extends SqlType<T>, T>(
class _InExpression<X extends SqlType<T>, T>
extends Expression<bool, BoolType> {
final Expression<T, X> _expression;
final Iterable<T> _values;
final List<T> _values;
final bool _not;
@override
@ -57,4 +57,16 @@ class _InExpression<X extends SqlType<T>, T>
context.buffer.write(')');
}
@override
int get hashCode => $mrjf($mrjc(
_expression.hashCode, $mrjc(_equality.hash(_values), _not.hashCode)));
@override
bool operator ==(other) {
return other is _InExpression &&
other._expression == _expression &&
other._values == _values &&
other._not == _not;
}
}

View File

@ -30,4 +30,14 @@ class _NullCheck extends Expression<bool, BoolType> {
}
context.buffer.write('NULL');
}
@override
int get hashCode => $mrjf($mrjc(_inner.hashCode, _isNull.hashCode));
@override
bool operator ==(other) {
return other is _NullCheck &&
other._inner == _inner &&
other._isNull == _isNull;
}
}

View File

@ -72,6 +72,16 @@ class _LikeOperator extends Expression<bool, BoolType> {
context.buffer.write(' LIKE ');
writeInner(context, regex);
}
@override
int get hashCode => $mrjf($mrjc(target.hashCode, regex.hashCode));
@override
bool operator ==(other) {
return other is _LikeOperator &&
other.target == target &&
other.regex == regex;
}
}
/// Builtin collating functions from sqlite.
@ -116,6 +126,16 @@ class _CollateOperator extends Expression<String, StringType> {
context.buffer..write(' COLLATE ')..write(_operatorNames[collate]);
}
@override
int get hashCode => $mrjf($mrjc(inner.hashCode, collate.hashCode));
@override
bool operator ==(other) {
return other is _CollateOperator &&
other.inner == inner &&
other.collate == collate;
}
static const Map<Collate, String> _operatorNames = {
Collate.binary: 'BINARY',
Collate.noCase: 'NOCASE',

View File

@ -6,6 +6,9 @@ class Variable<T, S extends SqlType<T>> extends Expression<T, S> {
/// The Dart value that will be sent to the database
final T value;
// note that we keep the identity hash/equals here because each variable would
// get its own index in sqlite and is thus different.
@override
final Precedence precedence = Precedence.primary;
@ -82,4 +85,14 @@ class Constant<T, S extends SqlType<T>> extends Expression<T, S> {
final type = context.typeSystem.forDartType<T>();
context.buffer.write(type.mapToSqlConstant(value));
}
@override
int get hashCode => value.hashCode;
@override
bool operator ==(other) {
return other.runtimeType == runtimeType &&
// ignore: test_types_in_equals
(other as Constant<T, S>).value == value;
}
}

View File

@ -1,6 +1,7 @@
// Mega compilation unit that includes all Dart apis related to generating SQL
// at runtime.
import 'package:collection/collection.dart';
import 'package:meta/meta.dart';
// hidden because of https://github.com/dart-lang/sdk/issues/39262
import 'package:moor/moor.dart'

View File

@ -101,6 +101,18 @@ abstract class GeneratedColumn<T, S extends SqlType<T>> extends Column<T, S> {
bool get isRequired {
return !$nullable && defaultValue == null;
}
@override
int get hashCode => $mrjf($mrjc(tableName.hashCode, $name.hashCode));
@override
bool operator ==(other) {
if (other.runtimeType != runtimeType) return false;
// ignore: test_types_in_equals
final typedOther = other as GeneratedColumn;
return typedOther.tableName == tableName && typedOther.$name == $name;
}
}
/// Implementation for [TextColumn].

View File

@ -15,7 +15,7 @@ mixin TableInfo<TableDsl extends Table, D extends DataClass> on Table {
/// also contains auto-increment integers, which are primary key by default.
Set<GeneratedColumn> get $primaryKey => null;
// ensure the primaryKey getter is consistent with $primarKey, which can
// ensure the primaryKey getter is consistent with $primaryKey, which can
// contain additional columns.
@override
Set<Column> get primaryKey => $primaryKey;

View File

@ -73,6 +73,11 @@ class CustomSelectStatement with Selectable<QueryRow> {
/// For custom select statements, represents a row in the result set.
class QueryRow {
/// The raw data in this row.
///
/// Note that the values in this map aren't mapped to Dart yet. For instance,
/// a [DateTime] would be stored as an [int] in [data] because that's the way
/// it's stored in the database. To read a value, use any of the [read]
/// methods.
final Map<String, dynamic> data;
final QueryEngine _db;

View File

@ -77,6 +77,7 @@ class SimpleSelectStatement<T extends Table, D extends DataClass>
return statement;
}
/// {@macro moor_select_addColumns}
JoinedSelectStatement addColumns(List<Expression> expressions) {
return join(const [])..addColumns(expressions);
}
@ -133,8 +134,10 @@ class TypedResult {
return _parsedData[table] as D;
}
/// Reads a single column from an [expr].
/// todo expand documentation
/// Reads a single column from an [expr]. The expression must have been added
/// as a column, for instance via [JoinedSelectStatement.addColumns].
///
/// To access the underlying columns directly, use
D read<D, T extends SqlType<D>>(Expression<D, T> expr) {
if (_parsedExpressions != null) {
return _parsedExpressions[expr] as D;

View File

@ -109,14 +109,25 @@ class JoinedSelectStatement<FirstT extends Table, FirstD extends DataClass>
orderByExpr = OrderBy(terms);
}
/// {@template moor_select_addColumns}
/// Adds a custom expression to the query.
///
/// The database will evaluate the [Expression] for each row found for this
/// query. The value of the expression can be extracted from the [TypedResult]
/// by passing it to [TypedResult.read].
///
/// As an example, we could calculate the length of a column on the database:
/// ```dart
/// final contentLength = todos.content.length;
/// final results = await select(todos).addColumns([contentLength]).get();
///
/// // we can now read the result of a column added to addColumns
/// final lengthOfFirst = results.first.read(contentLength);
/// ```
///
/// See also:
/// - The docs on expressions: https://moor.simonbinder.eu/docs/getting-started/expressions/
/// {@endtemplate}
void addColumns(Iterable<Expression> expressions) {
_selectedColumns.addAll(expressions);
}

View File

@ -0,0 +1,11 @@
import 'package:test/test.dart';
void expectEquals(dynamic a, dynamic expected) {
expect(a, equals(expected));
expect(a.hashCode, equals(expected.hashCode));
}
void expectNotEquals(dynamic a, dynamic expected) {
expect(a, isNot(equals(expected)));
expect(a.hashCode, isNot(equals(expected.hashCode)));
}

View File

@ -1,6 +1,7 @@
import 'package:moor/moor.dart';
import 'package:test/test.dart';
import '../data/utils/expect_equality.dart';
import '../data/utils/expect_generated.dart';
void main() {
@ -16,6 +17,9 @@ void main() {
(i1 - i2).expectGenerates('i1 - i2');
(i1 - -i2).expectGenerates('i1 - -i2');
(i1 / i2).expectGenerates('i1 / i2');
expectEquals(i1 + i2, i1 + i2);
expectNotEquals(i1 + i2, i2 + i1);
});
test('string concatenation', () {

View File

@ -1,6 +1,7 @@
import 'package:moor/moor.dart';
import 'package:test/test.dart';
import '../data/utils/expect_equality.dart';
import '../data/utils/expect_generated.dart';
// ignore_for_file: deprecated_member_use_from_same_package
@ -13,11 +14,17 @@ void main() {
(a | b).expectGenerates('a OR b');
(a & b).expectGenerates('a AND b');
a.not().expectGenerates('NOT a');
expectEquals(a & b, a & b);
expectNotEquals(a | b, b | a);
});
test('boolean expressions via top-level methods', () {
or(a, b).expectGenerates('a OR b');
and(a, b).expectGenerates('a AND b');
not(a).expectGenerates('NOT a');
expectEquals(not(a), not(a));
expectNotEquals(not(a), not(b));
});
}

View File

@ -2,6 +2,7 @@ import 'package:moor/moor.dart';
import 'package:test/test.dart';
import '../data/tables/todos.dart';
import '../data/utils/expect_equality.dart';
void main() {
final expression = GeneratedIntColumn('col', null, false);
@ -31,6 +32,8 @@ void main() {
fn(compare).writeInto(ctx);
expect(ctx.sql, 'col $value compare');
expectEquals(fn(compare), fn(compare));
});
});
});

View File

@ -2,6 +2,7 @@ import 'package:moor/moor.dart';
import 'package:test/test.dart';
import '../data/tables/todos.dart';
import '../data/utils/expect_equality.dart';
void main() {
group('string literals', () {
@ -17,6 +18,16 @@ void main() {
testStringMapping('\\\$"', "'\\\$\"'");
});
});
test('constant hash and equals', () {
// these shouldn't be identical, so no const constructor
final first = Constant('hi'); // ignore: prefer_const_constructors
final alsoFirst = Constant('hi'); // ignore: prefer_const_constructors
final second = const Constant(3);
expectEquals(first, alsoFirst);
expectNotEquals(first, second);
});
}
void testStringMapping(String dart, String expectedLiteral) {

View File

@ -1,6 +1,8 @@
import 'package:moor/moor.dart';
import 'package:test/test.dart';
import '../data/utils/expect_equality.dart';
// ignore_for_file: deprecated_member_use_from_same_package
typedef Expression<int, IntType> _Extractor(
@ -26,6 +28,8 @@ void main() {
key(column).writeInto(ctx);
expect(ctx.sql, value);
expectEquals(key(column), key(column));
});
});
});

View File

@ -3,6 +3,7 @@ import 'package:moor/moor.dart' as moor;
import 'package:test/test.dart';
import '../data/tables/todos.dart';
import '../data/utils/expect_equality.dart';
void main() {
final innerExpression = GeneratedTextColumn('name', null, true);
@ -23,5 +24,7 @@ void main() {
expr.writeInto(context);
expect(context.sql, 'name IS NOT NULL');
expectEquals(expr, moor.isNotNull(innerExpression));
});
}