diff --git a/docs/content/en/docs/Advanced Features/expressions.md b/docs/content/en/docs/Advanced Features/expressions.md index fb380908..98572884 100644 --- a/docs/content/en/docs/Advanced Features/expressions.md +++ b/docs/content/en/docs/Advanced Features/expressions.md @@ -76,6 +76,13 @@ that you can use operators and comparisons on them. To obtain the current date or the current time as an expression, use the `currentDate` and `currentDateAndTime` constants provided by moor. +You can also use the `+` and `-` operators to add or subtract a duration from a time column: + +```dart +final toNextWeek = TasksCompanion.custom(dueDate: tasks.dueDate + Duration(weeks: 1)); +update(tasks).write(toNextWeek); +``` + ## `IN` and `NOT IN` You can check whether an expression is in a list of values by using the `isIn` and `isNotIn` methods: diff --git a/moor/lib/src/runtime/query_builder/expressions/datetimes.dart b/moor/lib/src/runtime/query_builder/expressions/datetimes.dart index 0e6e3185..e8a305d8 100644 --- a/moor/lib/src/runtime/query_builder/expressions/datetimes.dart +++ b/moor/lib/src/runtime/query_builder/expressions/datetimes.dart @@ -44,6 +44,18 @@ extension DateTimeExpressions on Expression { // for moor, date times are just unix timestamps, so we don't need to rewrite // anything when converting Expression get secondsSinceEpoch => dartCast(); + + /// Adds [duration] from this date. + Expression operator +(Duration duration) { + return _BaseInfixOperator(this, '+', Variable(duration.inSeconds), + precedence: Precedence.plusMinus); + } + + /// Subtracts [duration] from this date. + Expression operator -(Duration duration) { + return _BaseInfixOperator(this, '-', Variable(duration.inSeconds), + precedence: Precedence.plusMinus); + } } /// Expression that extracts components out of a date time by using the builtin diff --git a/moor/lib/src/runtime/query_builder/expressions/expression.dart b/moor/lib/src/runtime/query_builder/expressions/expression.dart index de9e657d..33591513 100644 --- a/moor/lib/src/runtime/query_builder/expressions/expression.dart +++ b/moor/lib/src/runtime/query_builder/expressions/expression.dart @@ -203,13 +203,13 @@ abstract class _InfixOperator extends Expression { class _BaseInfixOperator extends _InfixOperator { @override - final Expression left; + final Expression left; @override final String operator; @override - final Expression right; + final Expression right; @override final Precedence precedence; diff --git a/moor/lib/src/runtime/types/sql_types.dart b/moor/lib/src/runtime/types/sql_types.dart index 93ccf174..de42e339 100644 --- a/moor/lib/src/runtime/types/sql_types.dart +++ b/moor/lib/src/runtime/types/sql_types.dart @@ -84,7 +84,10 @@ class IntType extends SqlType { const IntType(); @override - int mapFromDatabaseResponse(dynamic response) => response as int; + int mapFromDatabaseResponse(dynamic response) { + if (response is int) return response; + return int.parse(response.toString()); + } @override String mapToSqlConstant(int content) => content?.toString() ?? 'NULL'; diff --git a/moor/test/data/utils/expect_generated.dart b/moor/test/data/utils/expect_generated.dart index 876504db..dc7309cd 100644 --- a/moor/test/data/utils/expect_generated.dart +++ b/moor/test/data/utils/expect_generated.dart @@ -4,7 +4,7 @@ import 'package:test/test.dart'; /// Matcher for [Component]-subclasses. Expect that a component generates the /// matching [sql] and, optionally, the matching [variables]. Matcher generates(dynamic sql, [dynamic variables]) { - final variablesMatcher = variables != null ? wrapMatcher(variables) : null; + final variablesMatcher = variables != null ? wrapMatcher(variables) : isEmpty; return _GeneratesSqlMatcher(wrapMatcher(sql), variablesMatcher); } @@ -19,8 +19,9 @@ class _GeneratesSqlMatcher extends Matcher { description = description.add('generates sql ').addDescriptionOf(_matchSql); if (_matchVariables != null) { - description = - description.add('and variables').addDescriptionOf(_matchVariables); + description = description + .add(' and variables that ') + .addDescriptionOf(_matchVariables); } return description; } @@ -36,15 +37,15 @@ class _GeneratesSqlMatcher extends Matcher { mismatchDescription = mismatchDescription.add('generated $sql, which '); mismatchDescription = _matchSql.describeMismatch( - sql, mismatchDescription, matchState, verbose); + sql, mismatchDescription, matchState['sql_match'] as Map, verbose); } if (matchState.containsKey('vars')) { final vars = matchState['vars'] as List; mismatchDescription = - mismatchDescription.add('used variables $vars, which '); + mismatchDescription.add('generated variables $vars, which '); mismatchDescription = _matchVariables.describeMismatch( - vars, mismatchDescription, matchState, verbose); + vars, mismatchDescription, matchState['vars_match'] as Map, verbose); } return mismatchDescription; } @@ -52,7 +53,7 @@ class _GeneratesSqlMatcher extends Matcher { @override bool matches(dynamic item, Map matchState) { if (item is! Component) { - addStateInfo(matchState, {'wrong_type': true}); + matchState['wrong_type'] = true; return false; } @@ -62,14 +63,18 @@ class _GeneratesSqlMatcher extends Matcher { var matches = true; - if (!_matchSql.matches(ctx.sql, matchState)) { - addStateInfo(matchState, {'sql': ctx.sql}); + final sqlMatchState = {}; + if (!_matchSql.matches(ctx.sql, sqlMatchState)) { + matchState['sql'] = ctx.sql; + matchState['sql_match'] = sqlMatchState; matches = false; } + final argsMatchState = {}; if (_matchVariables != null && - !_matchVariables.matches(ctx.boundVariables, matchState)) { - addStateInfo(matchState, {'vars': ctx.boundVariables}); + !_matchVariables.matches(ctx.boundVariables, argsMatchState)) { + matchState['vars'] = ctx.boundVariables; + matchState['vars_match'] = argsMatchState; matches = false; } diff --git a/moor/test/expressions/datetime_expression_test.dart b/moor/test/expressions/datetime_expression_test.dart index 50e4adbd..df94d4a3 100644 --- a/moor/test/expressions/datetime_expression_test.dart +++ b/moor/test/expressions/datetime_expression_test.dart @@ -2,6 +2,7 @@ import 'package:moor/moor.dart'; import 'package:test/test.dart'; import '../data/utils/expect_equality.dart'; +import '../data/utils/expect_generated.dart'; typedef _Extractor = Expression Function(Expression d); @@ -37,4 +38,13 @@ void main() { expect(ctx.sql, 'strftime(\'%s\', CURRENT_TIMESTAMP) + 10'); }); + + test('plus and minus durations', () { + final expr = currentDateAndTime + + const Duration(days: 3) - + const Duration(seconds: 5); + + expect(expr, + generates('strftime(\'%s\', CURRENT_TIMESTAMP) + ? - ?', [259200, 5])); + }); } diff --git a/moor/test/expressions/expressions_integration_test.dart b/moor/test/expressions/expressions_integration_test.dart new file mode 100644 index 00000000..27bc084b --- /dev/null +++ b/moor/test/expressions/expressions_integration_test.dart @@ -0,0 +1,35 @@ +import 'package:moor/moor.dart'; +@TestOn('vm') +import 'package:test/test.dart'; +import 'package:moor_ffi/moor_ffi.dart'; + +import '../data/tables/todos.dart'; + +void main() { + TodoDb db; + + setUp(() async { + db = TodoDb(VmDatabase.memory()); + + // we selectOnly from users for the lack of a better option. Insert one + // row so that getSingle works + await db.into(db.users).insert( + UsersCompanion.insert(name: 'User name', profilePicture: Uint8List(0))); + }); + + tearDown(() => db.close()); + + test('plus and minus on DateTimes', () async { + final nowExpr = currentDateAndTime; + final tomorrow = nowExpr + const Duration(days: 1); + final nowStamp = nowExpr.secondsSinceEpoch; + final tomorrowStamp = tomorrow.secondsSinceEpoch; + + final row = await (db.selectOnly(db.users) + ..addColumns([nowStamp, tomorrowStamp])) + .getSingle(); + + expect(row.read(tomorrowStamp) - row.read(nowStamp), + const Duration(days: 1).inSeconds); + }); +} diff --git a/moor/test/update_test.dart b/moor/test/update_test.dart index b97a512e..85fbb96f 100644 --- a/moor/test/update_test.dart +++ b/moor/test/update_test.dart @@ -118,6 +118,20 @@ void main() { }); }); + test('can update with custom companions', () async { + await db.update(db.todosTable).replace(TodosTableCompanion.custom( + id: const Variable(4), + content: db.todosTable.content, + targetDate: db.todosTable.targetDate + const Duration(days: 1), + )); + + verify(executor.runUpdate( + 'UPDATE todos SET content = content, target_date = target_date + ? ' + 'WHERE id = ?;', + argThat(equals([86400, 4])), + )); + }); + group('custom updates', () { test('execute the correct sql', () async { await db.customUpdate('DELETE FROM users');