From 58fdda482feceabc03081fd4e4ce1dfa2edd8b97 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Sun, 4 Apr 2021 21:37:32 +0200 Subject: [PATCH] Add caseMatch to build CASE WHEN --- moor/CHANGELOG.md | 4 ++ .../query_builder/expressions/case_when.dart | 44 +++++++++++++++++++ .../query_builder/expressions/expression.dart | 40 ++++++++++++++++- .../runtime/query_builder/query_builder.dart | 6 ++- moor/test/expressions/when_test.dart | 35 +++++++++++++++ 5 files changed, 127 insertions(+), 2 deletions(-) create mode 100644 moor/lib/src/runtime/query_builder/expressions/case_when.dart create mode 100644 moor/test/expressions/when_test.dart diff --git a/moor/CHANGELOG.md b/moor/CHANGELOG.md index 95dc1858..ab3baa59 100644 --- a/moor/CHANGELOG.md +++ b/moor/CHANGELOG.md @@ -1,3 +1,7 @@ +## 4.3.0-dev + +- Add `CASE WHEN` expressions with the `caseMatch` method on `Expression` + ## 4.2.1 - Deprecate `readBool`, `readString`, `readInt`, `readDouble`, `readDateTime` diff --git a/moor/lib/src/runtime/query_builder/expressions/case_when.dart b/moor/lib/src/runtime/query_builder/expressions/case_when.dart new file mode 100644 index 00000000..2668b798 --- /dev/null +++ b/moor/lib/src/runtime/query_builder/expressions/case_when.dart @@ -0,0 +1,44 @@ +import 'package:meta/meta.dart'; + +import '../query_builder.dart'; + +/// A `CASE WHEN` expression in sqlite. +/// +/// This class supports when expressions with or without a base expression. +@internal +class CaseWhenExpression extends Expression { + /// The optional base expression. If it's set, the keys in [whenThen] will be + /// compared to this expression. + final Expression? base; + + /// The when entries for this expression. This expression will evaluate to the + /// value of the entry with a matching key. + final List> whenThen; + + /// The expression to use if no entry in [whenThen] matched. + final Expression? orElse; + + /// Creates a `CASE WHEN` expression from the independent components. + CaseWhenExpression(this.base, this.whenThen, this.orElse); + + @override + void writeInto(GenerationContext context) { + context.buffer.write('CASE '); + base?.writeInto(context); + + for (final entry in whenThen) { + context.buffer.write(' WHEN '); + entry.key.writeInto(context); + context.buffer.write(' THEN '); + entry.value.writeInto(context); + } + + final orElse = this.orElse; + if (orElse != null) { + context.buffer.write(' ELSE '); + orElse.writeInto(context); + } + + context.buffer.write(' END'); + } +} diff --git a/moor/lib/src/runtime/query_builder/expressions/expression.dart b/moor/lib/src/runtime/query_builder/expressions/expression.dart index 02c38ced..c5bfbe3e 100644 --- a/moor/lib/src/runtime/query_builder/expressions/expression.dart +++ b/moor/lib/src/runtime/query_builder/expressions/expression.dart @@ -86,9 +86,47 @@ abstract class Expression implements FunctionParameter { return _InSelectExpression(select, this, true); } + /// A `CASE WHEN` construct using the current expression as a base. + /// + /// The expression on which [caseMatch] is invoked will be used as a base and + /// compared against the keys in [when]. If an equal key is found in the map, + /// the expression returned evaluates to the respective value. + /// If no matching keys are found in [when], the [orElse] expression is + /// evaluated and returned. If no [orElse] expression is provided, `NULL` will + /// be returned instead. + /// + /// For example, consider this expression mapping numerical weekdays to their + /// name: + /// + /// ```dart + /// final weekday = myTable.createdOnWeekDay; + /// weekday.caseMatch( + /// when: { + /// Constant(1): Constant('Monday'), + /// Constant(2): Constant('Tuesday'), + /// Constant(3): Constant('Wednesday'), + /// Constant(4): Constant('Thursday'), + /// Constant(5): Constant('Friday'), + /// Constant(6): Constant('Saturday'), + /// Constant(7): Constant('Sunday'), + /// }, + /// orElse: Constant('(unknown)'), + /// ); + /// ``` + Expression caseMatch({ + required Map, Expression> when, + Expression? orElse, + }) { + if (when.isEmpty) { + throw ArgumentError.value(when, 'when', 'Must not be empty'); + } + + return CaseWhenExpression(this, when.entries.toList(), orElse); + } + /// Writes this expression into the [GenerationContext], assuming that there's /// an outer expression with [precedence]. If the [Expression.precedence] of - /// `this` expression is lower, it will be wrapped in + /// `this` expression is lower, it will be wrap}ped in /// /// See also: /// - [Component.writeInto], which doesn't take any precedence relation into diff --git a/moor/lib/src/runtime/query_builder/query_builder.dart b/moor/lib/src/runtime/query_builder/query_builder.dart index e56f28e3..65d7a3c6 100644 --- a/moor/lib/src/runtime/query_builder/query_builder.dart +++ b/moor/lib/src/runtime/query_builder/query_builder.dart @@ -3,6 +3,7 @@ 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' hide BooleanExpressionOperators, DateTimeExpressions, TableInfoUtils; @@ -11,12 +12,15 @@ import 'package:moor/src/runtime/executor/stream_queries.dart'; import 'package:moor/src/runtime/types/sql_types.dart'; import 'package:moor/src/utils/single_transformer.dart'; +// New files should not be part of this mega library, which we're trying to +// split up. +import 'expressions/case_when.dart'; + part 'components/group_by.dart'; part 'components/join.dart'; part 'components/limit.dart'; part 'components/order_by.dart'; part 'components/where.dart'; - part 'expressions/aggregate.dart'; part 'expressions/algebra.dart'; part 'expressions/bools.dart'; diff --git a/moor/test/expressions/when_test.dart b/moor/test/expressions/when_test.dart new file mode 100644 index 00000000..e765160a --- /dev/null +++ b/moor/test/expressions/when_test.dart @@ -0,0 +1,35 @@ +import 'package:moor/moor.dart'; +import 'package:test/test.dart'; + +import '../data/utils/expect_generated.dart'; + +void main() { + const x = CustomExpression('x'); + const y = CustomExpression('y'); + + test('WHEN without ELSE', () { + expect( + x.caseMatch(when: { + const Constant('a'): const Constant(1), + const Constant('b'): const Constant(2), + }), + generates("CASE x WHEN 'a' THEN 1 WHEN 'b' THEN 2 END"), + ); + }); + + test('WHEN with ELSE', () { + expect( + x.caseMatch( + when: { + const Constant('a'): const Constant(1), + }, + orElse: y, + ), + generates("CASE x WHEN 'a' THEN 1 ELSE y END"), + ); + }); + + test('does not allow empty WHEN map', () { + expect(() => x.caseMatch(when: const {}), throwsA(isA())); + }); +}