mirror of https://github.com/AMT-Cheif/drift.git
Add bitwise operators to query builder
This commit is contained in:
parent
a039de0d3a
commit
becae40c6e
|
@ -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
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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)'));
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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});
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue