Add bitwise operators to query builder

This commit is contained in:
Simon Binder 2022-10-05 11:31:45 +02:00
parent a039de0d3a
commit becae40c6e
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
9 changed files with 327 additions and 154 deletions

View File

@ -10,4 +10,12 @@ extension Expressions on MyDatabase {
return (select(categories)..where((row) => hasNoTodo)).get();
}
// #enddocregion emptyCategories
}
// #docregion bitwise
Expression<int> bitwiseMagic(Expression<int> a, Expression<int> b) {
// Generates `~(a | b)` in SQL.
return ~(a.bitwiseAnd(b));
}
// #enddocregion bitwise

View File

@ -50,6 +50,7 @@ select(animals)..where((a) => a.isMammal | a.amountOfLegs.equals(2));
```
## Arithmetic
For `int` and `double` expressions, you can use the `+`, `-`, `*` and `/` operators. To
run calculations between a sql expression and a Dart value, wrap it in a `Variable`:
```dart
@ -62,7 +63,12 @@ Future<List<Product>> canBeBought(int amount, int price) {
```
String expressions define a `+` operator as well. Just like you would expect, it performs
concatenation in sql.
a concatenation in sql.
For integer values, you can use `~`, `bitwiseAnd` and `bitwiseOr` to perform
bitwise operations:
{% include "blocks/snippet" snippets = snippets name = 'bitwise' %}
## Nullability
To check whether an expression evaluates to `NULL` in sql, you can use the `isNull` extension:

View File

@ -2,6 +2,8 @@
- Always escape column names, avoiding the costs of using a regular expression
to check whether they need to be escaped.
- Add extensions for binary methods on integer expressions: `operator ~`,
`bitwiseAnd` and `bitwiseOr`.
## 2.1.0

View File

@ -0,0 +1,71 @@
import '../query_builder.dart';
import 'internal.dart';
/// Extensions providing bitwise operators [~], on integer expressions.
extension BitwiseInt on Expression<int> {
/// Flips all bits in this value (turning `0` to `1` and vice-versa) and
/// returns the result.
Expression<int> operator ~() {
return _BitwiseNegation(this);
}
/// Returns the bitwise-or operation between `this` and [other].
Expression<int> bitwiseOr(Expression<int> other) {
return BaseInfixOperator(this, '|', other, precedence: Precedence.bitwise);
}
/// Returns the bitwise-and operation between `this` and [other].
Expression<int> bitwiseAnd(Expression<int> other) {
return BaseInfixOperator(this, '&', other, precedence: Precedence.bitwise);
}
}
/// Extensions providing bitwise operators [~], on integer expressions that are
/// represented as a Dart [BigInt].
extension BitwiseBigInt on Expression<BigInt> {
/// Flips all bits in this value (turning `0` to `1` and vice-versa) and
/// returns the result.
///
/// Note that, just like [BitwiseInt], this still operates on 64-bit integers
/// in SQL. The [BigInt] type on the expression only tells drift that the
/// result should be integerpreted as a big integer, which is primarily useful
/// on the web where large values cannot be stored in an [int].
Expression<BigInt> operator ~() {
return _BitwiseNegation(this);
}
/// Returns the bitwise-or operation between `this` and [other].
///
/// Note that, just like [BitwiseInt], this still operates on 64-bit integers
/// in SQL. The [BigInt] type on the expression only tells drift that the
/// result should be integerpreted as a big integer, which is primarily useful
/// on the web where large values cannot be stored in an [int].
Expression<BigInt> bitwiseOr(Expression<BigInt> other) {
return BaseInfixOperator(this, '|', other, precedence: Precedence.bitwise);
}
/// Returns the bitwise-and operation between `this` and [other].
///
/// Note that, just like [BitwiseInt], this still operates on 64-bit integers
/// in SQL. The [BigInt] type on the expression only tells drift that the
/// result should be integerpreted as a big integer, which is primarily useful
/// on the web where large values cannot be stored in an [int].
Expression<BigInt> bitwiseAnd(Expression<BigInt> other) {
return BaseInfixOperator(this, '&', other, precedence: Precedence.bitwise);
}
}
class _BitwiseNegation<T extends Object> extends Expression<T> {
final Expression<T> _inner;
@override
Precedence get precedence => Precedence.unary;
_BitwiseNegation(this._inner);
@override
void writeInto(GenerationContext context) {
context.buffer.write('~');
writeInner(context, _inner);
}
}

View File

@ -7,11 +7,15 @@ extension BooleanExpressionOperators on Expression<bool> {
Expression<bool> not() => _NotExpression(this);
/// Returns an expression that is true iff both `this` and [other] are true.
///
/// For a bitwise-and on two numbers, see [BitwiseInt.bitwiseAnd].
Expression<bool> operator &(Expression<bool> other) {
return BaseInfixOperator(this, 'AND', other, precedence: Precedence.and);
}
/// Returns an expression that is true if `this` or [other] are true.
///
/// For a bitwise-or on two numbers, see [BitwiseInt.bitwiseOr].
Expression<bool> operator |(Expression<bool> other) {
return BaseInfixOperator(this, 'OR', other, precedence: Precedence.or);
}

View File

@ -354,7 +354,7 @@ class _UnaryMinus<DT extends Object> extends Expression<DT> {
@override
void writeInto(GenerationContext context) {
context.buffer.write('-');
inner.writeInto(context);
writeInner(context, inner);
}
@override

View File

@ -17,6 +17,7 @@ import 'expressions/case_when.dart';
import 'expressions/internal.dart';
export 'on_table.dart';
export 'expressions/bitwise.dart';
part 'components/group_by.dart';
part 'components/join.dart';

View File

@ -0,0 +1,48 @@
import 'package:drift/drift.dart';
import 'package:test/test.dart';
import '../../test_utils/matchers.dart';
void main() {
group('int', () {
final a = CustomExpression<int>('a', precedence: Precedence.primary);
final b = CustomExpression<int>('b', precedence: Precedence.primary);
test('not', () {
expect(~a, generates('~a'));
expect(~(a + b), generates('~(a + b)'));
});
test('or', () {
expect(a.bitwiseOr(b), generates('a | b'));
expect((~a).bitwiseOr(b), generates('~a | b'));
expect(~(a.bitwiseOr(b)), generates('~(a | b)'));
});
test('and', () {
expect(a.bitwiseAnd(b), generates('a & b'));
expect(-(a.bitwiseAnd(b)), generates('-(a & b)'));
});
});
group('BigInt', () {
final a = CustomExpression<BigInt>('a', precedence: Precedence.primary);
final b = CustomExpression<BigInt>('b', precedence: Precedence.primary);
test('not', () {
expect(~a, generates('~a'));
expect(~(a + b), generates('~(a + b)'));
});
test('or', () {
expect(a.bitwiseOr(b), generates('a | b'));
expect((~a).bitwiseOr(b), generates('~a | b'));
expect(~(a.bitwiseOr(b)), generates('~(a | b)'));
});
test('and', () {
expect(a.bitwiseAnd(b), generates('a & b'));
expect(-(a.bitwiseAnd(b)), generates('-(a & b)'));
});
});
}

View File

@ -6,19 +6,201 @@ import '../../test_utils/test_utils.dart';
void main() {
group('with default options', () {
_testWith(() => TodoDb.connect(testInMemoryDatabase()));
_testDateTimes(() => TodoDb.connect(testInMemoryDatabase()));
});
group('storing date times as text', () {
_testWith(
_testDateTimes(
() => TodoDb.connect(testInMemoryDatabase())
..options = const DriftDatabaseOptions(storeDateTimeAsText: true),
dateTimeAsText: true,
);
});
group('non-datetime expressions', () {
late TodoDb db;
setUp(() async {
db = TodoDb.connect(testInMemoryDatabase());
// 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());
Future<T?> eval<T extends Object>(Expression<T> expr,
{TableInfo? onTable}) {
final query = db.selectOnly(onTable ?? db.users)..addColumns([expr]);
return query.getSingle().then((row) => row.read(expr));
}
test('rowid', () {
expect(eval(db.users.rowId), completion(1));
});
group('aggregate', () {
setUp(() => db.delete(db.users).go());
group('groupConcat', () {
setUp(() async {
for (var i = 0; i < 5; i++) {
await db.into(db.users).insert(UsersCompanion.insert(
name: 'User $i', profilePicture: Uint8List(0)));
}
});
test('simple', () {
expect(eval(db.users.id.groupConcat()), completion('2,3,4,5,6'));
});
test('custom separator', () {
expect(eval(db.users.id.groupConcat(separator: '-')),
completion('2-3-4-5-6'));
});
test('distinct', () async {
for (var i = 0; i < 5; i++) {
await db
.into(db.todosTable)
.insert(TodosTableCompanion.insert(content: 'entry $i'));
await db
.into(db.todosTable)
.insert(TodosTableCompanion.insert(content: 'entry $i'));
}
expect(
eval(db.todosTable.content.groupConcat(distinct: true),
onTable: db.todosTable),
completion('entry 0,entry 1,entry 2,entry 3,entry 4'));
});
test('filter', () {
expect(
eval(db.users.id
.groupConcat(filter: db.users.id.isBiggerThanValue(3))),
completion('4,5,6'));
});
});
test('filters', () async {
await db.into(db.tableWithoutPK).insert(
TableWithoutPKCompanion.insert(notReallyAnId: 3, someFloat: 7));
await db.into(db.tableWithoutPK).insert(
TableWithoutPKCompanion.insert(notReallyAnId: 2, someFloat: 1));
expect(
eval(
db.tableWithoutPK.someFloat.sum(
filter: db.tableWithoutPK.someFloat.isBiggerOrEqualValue(3)),
onTable: db.tableWithoutPK,
),
completion(7),
);
});
});
group('text', () {
test('contains', () {
const stringLiteral = Constant('Some sql string literal');
final containsSql = stringLiteral.contains('sql');
expect(eval(containsSql), completion(isTrue));
});
test('trim()', () {
const literal = Constant(' hello world ');
expect(eval(literal.trim()), completion('hello world'));
});
test('trimLeft()', () {
const literal = Constant(' hello world ');
expect(eval(literal.trimLeft()), completion('hello world '));
});
test('trimRight()', () {
const literal = Constant(' hello world ');
expect(eval(literal.trimRight()), completion(' hello world'));
});
});
test('coalesce', () async {
final expr = coalesce<int>([const Constant(null), const Constant(3)]);
expect(eval(expr), completion(3));
});
test('subquery', () {
final query = db.selectOnly(db.users)..addColumns([db.users.name]);
final expr = subqueryExpression<String>(query);
expect(eval(expr), completion('User name'));
});
test('is in subquery', () {
final query = db.selectOnly(db.users)..addColumns([db.users.name]);
final match = Variable.withString('User name').isInQuery(query);
final noMatch = Variable.withString('Another name').isInQuery(query);
expect(eval(match), completion(isTrue));
expect(eval(noMatch), completion(isFalse));
});
test('groupConcat is nullable', () async {
final ids = db.users.id.groupConcat();
final query = db.selectOnly(db.users)
..where(db.users.id.equals(999))
..addColumns([ids]);
final result = await query.getSingle();
expect(result.read(ids), isNull);
});
test('subqueries cause updates to stream queries', () async {
await db
.into(db.categories)
.insert(CategoriesCompanion.insert(description: 'description'));
final subquery = subqueryExpression<String>(db.selectOnly(db.categories)
..addColumns([db.categories.description]));
final stream = (db.selectOnly(db.users)..addColumns([subquery]))
.map((row) => row.read(subquery))
.watchSingle();
expect(stream, emitsInOrder(['description', 'changed']));
await db
.update(db.categories)
.write(const CategoriesCompanion(description: Value('changed')));
});
test('custom expressions can introduces new tables to watch', () async {
final custom =
CustomExpression<int>('1', watchedTables: [db.sharedTodos]);
final stream = (db.selectOnly(db.users)..addColumns([custom]))
.map((row) => row.read(custom))
.watchSingle();
expect(stream, emitsInOrder([1, 1]));
db.markTablesUpdated({db.sharedTodos});
});
test('bitwise operations - big int', () {
expect(eval(~Variable.withInt(12)), completion(-13));
expect(eval(~Variable.withBigInt(BigInt.from(12))),
completion(BigInt.from(-13)));
expect(eval(Variable.withInt(2).bitwiseOr(Variable(5))), completion(7));
expect(
eval(Variable.withBigInt(BigInt.two)
.bitwiseAnd(Variable(BigInt.from(10)))),
completion(BigInt.two));
});
});
}
void _testWith(TodoDb Function() openDb, {bool dateTimeAsText = false}) {
void _testDateTimes(TodoDb Function() openDb, {bool dateTimeAsText = false}) {
late TodoDb db;
setUp(() async {
@ -203,153 +385,4 @@ void _testWith(TodoDb Function() openDb, {bool dateTimeAsText = false}) {
skip:
sqlite3Version.versionNumber < 3039000 ? 'Requires sqlite 3.39' : null,
);
test('rowid', () {
expect(eval(db.users.rowId), completion(1));
});
group('aggregate', () {
setUp(() => db.delete(db.users).go());
group('groupConcat', () {
setUp(() async {
for (var i = 0; i < 5; i++) {
await db.into(db.users).insert(UsersCompanion.insert(
name: 'User $i', profilePicture: Uint8List(0)));
}
});
test('simple', () {
expect(eval(db.users.id.groupConcat()), completion('2,3,4,5,6'));
});
test('custom separator', () {
expect(eval(db.users.id.groupConcat(separator: '-')),
completion('2-3-4-5-6'));
});
test('distinct', () async {
for (var i = 0; i < 5; i++) {
await db
.into(db.todosTable)
.insert(TodosTableCompanion.insert(content: 'entry $i'));
await db
.into(db.todosTable)
.insert(TodosTableCompanion.insert(content: 'entry $i'));
}
expect(
eval(db.todosTable.content.groupConcat(distinct: true),
onTable: db.todosTable),
completion('entry 0,entry 1,entry 2,entry 3,entry 4'));
});
test('filter', () {
expect(
eval(db.users.id
.groupConcat(filter: db.users.id.isBiggerThanValue(3))),
completion('4,5,6'));
});
});
test('filters', () async {
await db.into(db.tableWithoutPK).insert(
TableWithoutPKCompanion.insert(notReallyAnId: 3, someFloat: 7));
await db.into(db.tableWithoutPK).insert(
TableWithoutPKCompanion.insert(notReallyAnId: 2, someFloat: 1));
expect(
eval(
db.tableWithoutPK.someFloat
.sum(filter: db.tableWithoutPK.someFloat.isBiggerOrEqualValue(3)),
onTable: db.tableWithoutPK,
),
completion(7),
);
});
});
group('text', () {
test('contains', () {
const stringLiteral = Constant('Some sql string literal');
final containsSql = stringLiteral.contains('sql');
expect(eval(containsSql), completion(isTrue));
});
test('trim()', () {
const literal = Constant(' hello world ');
expect(eval(literal.trim()), completion('hello world'));
});
test('trimLeft()', () {
const literal = Constant(' hello world ');
expect(eval(literal.trimLeft()), completion('hello world '));
});
test('trimRight()', () {
const literal = Constant(' hello world ');
expect(eval(literal.trimRight()), completion(' hello world'));
});
});
test('coalesce', () async {
final expr = coalesce<int>([const Constant(null), const Constant(3)]);
expect(eval(expr), completion(3));
});
test('subquery', () {
final query = db.selectOnly(db.users)..addColumns([db.users.name]);
final expr = subqueryExpression<String>(query);
expect(eval(expr), completion('User name'));
});
test('is in subquery', () {
final query = db.selectOnly(db.users)..addColumns([db.users.name]);
final match = Variable.withString('User name').isInQuery(query);
final noMatch = Variable.withString('Another name').isInQuery(query);
expect(eval(match), completion(isTrue));
expect(eval(noMatch), completion(isFalse));
});
test('groupConcat is nullable', () async {
final ids = db.users.id.groupConcat();
final query = db.selectOnly(db.users)
..where(db.users.id.equals(999))
..addColumns([ids]);
final result = await query.getSingle();
expect(result.read(ids), isNull);
});
test('subqueries cause updates to stream queries', () async {
await db
.into(db.categories)
.insert(CategoriesCompanion.insert(description: 'description'));
final subquery = subqueryExpression<String>(
db.selectOnly(db.categories)..addColumns([db.categories.description]));
final stream = (db.selectOnly(db.users)..addColumns([subquery]))
.map((row) => row.read(subquery))
.watchSingle();
expect(stream, emitsInOrder(['description', 'changed']));
await db
.update(db.categories)
.write(const CategoriesCompanion(description: Value('changed')));
});
test('custom expressions can introduces new tables to watch', () async {
final custom = CustomExpression<int>('1', watchedTables: [db.sharedTodos]);
final stream = (db.selectOnly(db.users)..addColumns([custom]))
.map((row) => row.read(custom))
.watchSingle();
expect(stream, emitsInOrder([1, 1]));
db.markTablesUpdated({db.sharedTodos});
});
}