mirror of https://github.com/AMT-Cheif/drift.git
Merge branch 'simolus3:develop' into codespace-glowing-space-palm-tree-9vwjv7wx993p57x
This commit is contained in:
commit
581708a328
|
@ -82,14 +82,6 @@ jobs:
|
|||
dart_version: ${{ needs.setup.outputs.dart_version }}
|
||||
- run: melos bootstrap --scope drift
|
||||
working-directory: .
|
||||
- name: Get dependencies for plugin
|
||||
run: |
|
||||
echo "dependency_overrides:" >> pubspec_overrides.yaml
|
||||
echo " drift: {path: ../../}" >> pubspec_overrides.yaml
|
||||
echo " drift_dev: {path: ../../../drift_dev}" >> pubspec_overrides.yaml
|
||||
echo " sqlparser: {path: ../../../sqlparser}" >> pubspec_overrides.yaml
|
||||
dart pub get
|
||||
working-directory: drift/tools/analyzer_plugin
|
||||
# analysis
|
||||
- run: dart format -o none --set-exit-if-changed .
|
||||
name: dartfmt
|
||||
|
@ -188,7 +180,7 @@ jobs:
|
|||
working-directory: extras/drift_postgres
|
||||
run: |
|
||||
dart pub upgrade
|
||||
dart test
|
||||
dart test -j 1
|
||||
- name: MariaDB integration tests
|
||||
working-directory: extras/drift_mariadb
|
||||
continue-on-error: true
|
||||
|
|
|
@ -6,6 +6,7 @@ on:
|
|||
- 'drift-[0-9]+.[0-9]+.[0-9]+*'
|
||||
- 'drift_dev-[0-9]+.[0-9]+.[0-9]+*'
|
||||
- 'sqlparser-[0-9]+.[0-9]+.[0-9]+*'
|
||||
- 'drift_postgres-[0-9]+.[0-9]+.[0-9]+*'
|
||||
|
||||
jobs:
|
||||
setup:
|
||||
|
@ -87,3 +88,22 @@ jobs:
|
|||
- run: dart pub get
|
||||
- run: dart pub lish --dry-run
|
||||
- run: dart pub lish -f
|
||||
|
||||
publish_drift_postgres:
|
||||
if: "${{ startsWith(github.ref_name, 'drift_postgres-') }}"
|
||||
needs: [setup]
|
||||
runs-on: ubuntu-latest
|
||||
environment: pub.dev
|
||||
permissions:
|
||||
id-token: write # Required for authentication using OIDC
|
||||
defaults:
|
||||
run:
|
||||
working-directory: extras/drift_postgres
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/prepare
|
||||
with:
|
||||
dart_version: ${{ needs.setup.outputs.dart_version }}
|
||||
- run: dart pub get
|
||||
- run: dart pub lish --dry-run
|
||||
- run: dart pub lish -f
|
||||
|
|
|
@ -37,7 +37,7 @@ The project is divided into multiple modules:
|
|||
- `moor_flutter/`: Contains a Flutter implementation for the database.
|
||||
- `drift_dev/`: Creates table, database and dao classes from the table structure and
|
||||
compiled queries.
|
||||
- `sqlparser/`: Contains an sql parser and analyzer that is mostly independent of drift,
|
||||
- `sqlparser/`: Contains an SQL parser and analyzer that is mostly independent of drift,
|
||||
but used by the generator for compiled custom queries.
|
||||
|
||||
## Concepts
|
||||
|
@ -46,7 +46,7 @@ we generate three classes:
|
|||
|
||||
1. A class that inherits from `TableInfo` (we call this the "table class"). It contains a structural representation
|
||||
of the table, which includes columns (including name, type, constraints...), the primary key and so on. The idea is
|
||||
that, if we have a `TableInfo` instance, we can create all kinds of sql statements.
|
||||
that, if we have a `TableInfo` instance, we can create all kinds of SQL statements.
|
||||
2. A class to represent a fully loaded row of a table. We call this a "data class" and it inherits from `DataClass`.
|
||||
3. A class to represent partial data (e.g. for inserts or updates, where not all columns are set). This class was
|
||||
introduced in moor 1.5 and is called a "companion".
|
||||
|
@ -55,7 +55,7 @@ This approach lets us write a higher-level api that uses the generated `TableInf
|
|||
write. For instance, the `Migrator` can write `CREATE TABLE` statements from these classes, an `UpdateStatement` will
|
||||
write `UPDATE` statements and so on. To write the query, we construct a `GenerationContext`, which contains a string
|
||||
buffer to write the query, keeps track of the introduced variables and so on. The idea is that everything that can
|
||||
appear anywhere in a sql statement inherits from `Component` (for instance, `Query`, `Expression`, `Variable`, `Where`,
|
||||
appear anywhere in an SQL statement inherits from `Component` (for instance, `Query`, `Expression`, `Variable`, `Where`,
|
||||
`OrderBy`). We can then recursively create the query by calling `Component.writeInto` for all subparts of a component.
|
||||
This query is then sent to a `QueryExecutor`, which is responsible for executing it and returning its result. The
|
||||
`QueryExecutor` is the only part that is platform specific, everything else is pure Dart that doesn't import any
|
||||
|
@ -63,7 +63,7 @@ restricted libraries.
|
|||
|
||||
### Important classes
|
||||
A `DatabaseConnectionUser` is the central piece of a drift database instance. It contains an `SqlTypeSystem` (responsible
|
||||
for mapping simple Dart objects from and to sql), the `QueryExecutor` discussed above and a `StreamQueryStore`
|
||||
for mapping simple Dart objects from and to SQL), the `QueryExecutor` discussed above and a `StreamQueryStore`
|
||||
(responsible for keeping active queries and re-running them when a table updates). It is also the super class of
|
||||
`GeneratedDatabase` and `DatabaseAccessor`, which are the classes a `@UseMoor` and `@UseDao` class inherits from.
|
||||
Finally, the `QueryEngine` is a mixin in `DatabaseConnectionUser` that provides the `select`, `update`, `delete` methods
|
||||
|
@ -119,7 +119,7 @@ updates that span multiple versions, we should follow these steps
|
|||
2. `drift_dev`
|
||||
3. (optional) `moor_flutter`
|
||||
|
||||
The `sqlparser` library can be published independently from drift.
|
||||
The `sqlparser` library can be published independently of drift.
|
||||
|
||||
### Building the documentation
|
||||
|
||||
|
|
16
README.md
16
README.md
|
@ -26,22 +26,22 @@ _Note: Moor has been renamed to drift_
|
|||
| [![Main version](https://img.shields.io/pub/v/drift.svg)](https://pub.dev/packages/drift) | [![Generator version](https://img.shields.io/pub/v/drift_dev.svg)](https://pub.dev/packages/drift_dev) |
|
||||
|
||||
Drift is a reactive persistence library for Flutter and Dart, built on top of
|
||||
sqlite.
|
||||
SQLite.
|
||||
Drift is
|
||||
|
||||
- __Flexible__: Drift lets you write queries in both SQL and Dart,
|
||||
providing fluent apis for both languages. You can filter and order results
|
||||
or use joins to run queries on multiple tables. You can even use complex
|
||||
sql features like `WITH` and `WINDOW` clauses.
|
||||
SQL features like `WITH` and `WINDOW` clauses.
|
||||
- __🔥 Feature rich__: Drift has builtin support for transactions, schema
|
||||
migrations, complex filters and expressions, batched updates and joins. We
|
||||
even have a builtin IDE for SQL!
|
||||
- __📦 Modular__: Thanks to builtin support for daos and `import`s in sql files, drift helps you keep your database code simple.
|
||||
- __🛡️ Safe__: Drift generates typesafe code based on your tables and queries. If you make a mistake in your queries, drift will find it at compile time and
|
||||
- __📦 Modular__: Thanks to builtin support for daos and `import`s in SQL files, drift helps you keep your database code simple.
|
||||
- __🛡️ Safe__: Drift generates type-safe code based on your tables and queries. If you make a mistake in your queries, drift will find it at compile time and
|
||||
provide helpful and descriptive lints.
|
||||
- __⚡ Fast__: Even though drift lets you write powerful queries, it can keep
|
||||
up with the performance of key-value stores like shared preferences and Hive. Drift is the only major persistence library with builtin threading support, allowing you to run database code across isolates with zero additional effort.
|
||||
- __Reactive__: Turn any sql query into an auto-updating stream! This includes complex queries across many tables
|
||||
- __Reactive__: Turn any SQL query into an auto-updating stream! This includes complex queries across many tables
|
||||
- __⚙️ Cross-Platform support__: Drift works on Android, iOS, macOS, Windows, Linux and the web. [This template](https://github.com/simolus3/drift/tree/develop/examples/app) is a Flutter todo app that works on all platforms.
|
||||
- __🗡️ Battle tested and production ready__: Drift is stable and well tested with a wide range of unit and integration tests. It powers production Flutter apps.
|
||||
|
||||
|
@ -58,10 +58,10 @@ project, I'd appreciate your [🌟 on GitHub](https://github.com/simolus3/drift/
|
|||
This repository contains a number of packages making up the drift project, most
|
||||
notably:
|
||||
|
||||
- `drift`: The main runtime for drift, which provides most apis
|
||||
- `drift`: The main runtime for drift, which provides most APIs.
|
||||
- `drift_dev`: The compiler for drift tables, databases and daos. It
|
||||
also contains a fully-featured sql ide for the Dart analyzer.
|
||||
- `sqlparser`: A sql parser and static analyzer, written in pure Dart. This package can be used without drift to perform analysis on sql statements.
|
||||
also contains a fully-featured SQL IDE for the Dart analyzer.
|
||||
- `sqlparser`: A SQL parser and static analyzer, written in pure Dart. This package can be used without drift to perform analysis on SQL statements.
|
||||
It's on pub at
|
||||
[![sqlparser](https://img.shields.io/pub/v/sqlparser.svg)](https://pub.dev/packages/sqlparser)
|
||||
|
||||
|
|
|
@ -3,3 +3,7 @@ import 'dart:io';
|
|||
Future<Directory> getApplicationDocumentsDirectory() {
|
||||
throw UnsupportedError('stub!');
|
||||
}
|
||||
|
||||
Future<Directory> getTemporaryDirectory() {
|
||||
throw UnsupportedError('stub!');
|
||||
}
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
Future<void> applyWorkaroundToOpenSqlite3OnOldAndroidVersions() async {
|
||||
throw 'stub!';
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
name: sqlite3_flutter_libs
|
||||
publish_to: none
|
||||
description: Fake "sqlite3_flutter_libs" package so that we can import it in snippets without depending on Flutter.
|
||||
|
||||
environment:
|
||||
sdk: ^2.16.0
|
|
@ -29,7 +29,17 @@ class MyDatabase extends $MyDatabase {
|
|||
@override
|
||||
int get schemaVersion => 1;
|
||||
|
||||
MyDatabase(QueryExecutor e) : super(e);
|
||||
MyDatabase(super.e);
|
||||
|
||||
// #docregion amountOfTodosInCategory
|
||||
Stream<int> amountOfTodosInCategory(int id) {
|
||||
return customSelect(
|
||||
'SELECT COUNT(*) AS c FROM todo_items WHERE category = ?',
|
||||
variables: [Variable.withInt(id)],
|
||||
readsFrom: {todoItems},
|
||||
).map((row) => row.read<int>('c')).watchSingle();
|
||||
}
|
||||
// #enddocregion amountOfTodosInCategory
|
||||
|
||||
// #docregion run
|
||||
Future<void> useGeneratedQuery() async {
|
||||
|
|
|
@ -13,7 +13,7 @@ CREATE TABLE categories (
|
|||
-- You can also create an index or triggers with drift files
|
||||
CREATE INDEX categories_description ON categories(description);
|
||||
|
||||
-- we can put named sql queries in here as well:
|
||||
-- we can put named SQL queries in here as well:
|
||||
createEntry: INSERT INTO todos (title, content) VALUES (:title, :content);
|
||||
deleteById: DELETE FROM todos WHERE id = :id;
|
||||
allTodos: SELECT * FROM todos;
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift/native.dart';
|
||||
|
||||
// #docregion class
|
||||
class LogInterceptor extends QueryInterceptor {
|
||||
Future<T> _run<T>(
|
||||
String description, FutureOr<T> Function() operation) async {
|
||||
final stopwatch = Stopwatch()..start();
|
||||
print('Running $description');
|
||||
|
||||
try {
|
||||
final result = await operation();
|
||||
print(' => succeeded after ${stopwatch.elapsedMilliseconds}ms');
|
||||
return result;
|
||||
} on Object catch (e) {
|
||||
print(' => failed after ${stopwatch.elapsedMilliseconds}ms ($e)');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
TransactionExecutor beginTransaction(QueryExecutor parent) {
|
||||
print('begin');
|
||||
return super.beginTransaction(parent);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> commitTransaction(TransactionExecutor inner) {
|
||||
return _run('commit', () => inner.send());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> rollbackTransaction(TransactionExecutor inner) {
|
||||
return _run('rollback', () => inner.rollback());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> runBatched(
|
||||
QueryExecutor executor, BatchedStatements statements) {
|
||||
return _run(
|
||||
'batch with $statements', () => executor.runBatched(statements));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> runInsert(
|
||||
QueryExecutor executor, String statement, List<Object?> args) {
|
||||
return _run(
|
||||
'$statement with $args', () => executor.runInsert(statement, args));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> runUpdate(
|
||||
QueryExecutor executor, String statement, List<Object?> args) {
|
||||
return _run(
|
||||
'$statement with $args', () => executor.runUpdate(statement, args));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> runDelete(
|
||||
QueryExecutor executor, String statement, List<Object?> args) {
|
||||
return _run(
|
||||
'$statement with $args', () => executor.runDelete(statement, args));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> runCustom(
|
||||
QueryExecutor executor, String statement, List<Object?> args) {
|
||||
return _run(
|
||||
'$statement with $args', () => executor.runCustom(statement, args));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Map<String, Object?>>> runSelect(
|
||||
QueryExecutor executor, String statement, List<Object?> args) {
|
||||
return _run(
|
||||
'$statement with $args', () => executor.runSelect(statement, args));
|
||||
}
|
||||
}
|
||||
// #enddocregion class
|
||||
|
||||
void use() {
|
||||
final myDatabaseFile = File('/dev/null');
|
||||
|
||||
// #docregion use
|
||||
NativeDatabase.createInBackground(
|
||||
myDatabaseFile,
|
||||
).interceptWith(LogInterceptor());
|
||||
// #enddocregion use
|
||||
}
|
|
@ -18,7 +18,7 @@ class Todos extends Table {
|
|||
|
||||
@DriftDatabase(tables: [Todos])
|
||||
class MyDatabase extends _$MyDatabase {
|
||||
MyDatabase(QueryExecutor e) : super(e);
|
||||
MyDatabase(super.e);
|
||||
|
||||
// #docregion start
|
||||
@override
|
||||
|
|
|
@ -8,14 +8,14 @@ import 'package:drift_dev/api/migrations.dart';
|
|||
const kDebugMode = true;
|
||||
|
||||
abstract class _$MyDatabase extends GeneratedDatabase {
|
||||
_$MyDatabase(QueryExecutor executor) : super(executor);
|
||||
_$MyDatabase(super.executor);
|
||||
}
|
||||
|
||||
// #docregion
|
||||
|
||||
class MyDatabase extends _$MyDatabase {
|
||||
// #enddocregion
|
||||
MyDatabase(QueryExecutor executor) : super(executor);
|
||||
MyDatabase(super.executor);
|
||||
|
||||
@override
|
||||
Iterable<TableInfo<Table, dynamic>> get allTables =>
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
import 'package:drift/drift.dart';
|
||||
|
||||
import 'example.drift.dart';
|
||||
|
||||
class DartExample extends ExampleDrift {
|
||||
DartExample(GeneratedDatabase attachedDatabase) : super(attachedDatabase);
|
||||
DartExample(super.attachedDatabase);
|
||||
|
||||
// #docregion watchInCategory
|
||||
Stream<List<Todo>> watchInCategory(int category) {
|
||||
|
|
|
@ -48,7 +48,7 @@ class ShoppingCartEntries {
|
|||
|
||||
@DriftDatabase(tables: [BuyableItems, ShoppingCarts])
|
||||
class JsonBasedDatabase extends $JsonBasedDatabase {
|
||||
JsonBasedDatabase(QueryExecutor e) : super(e);
|
||||
JsonBasedDatabase(super.e);
|
||||
|
||||
@override
|
||||
int get schemaVersion => 1;
|
||||
|
|
|
@ -31,7 +31,7 @@ class ShoppingCartEntries extends Table {
|
|||
|
||||
@DriftDatabase(tables: [BuyableItems, ShoppingCarts, ShoppingCartEntries])
|
||||
class RelationalDatabase extends $RelationalDatabase {
|
||||
RelationalDatabase(QueryExecutor e) : super(e);
|
||||
RelationalDatabase(super.e);
|
||||
|
||||
@override
|
||||
int get schemaVersion => 1;
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:drift/internal/modular.dart';
|
||||
|
||||
import 'upserts.drift.dart';
|
||||
|
||||
// #docregion words-table
|
||||
class Words extends Table {
|
||||
TextColumn get word => text()();
|
||||
IntColumn get usages => integer().withDefault(const Constant(1))();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {word};
|
||||
}
|
||||
// #enddocregion words-table
|
||||
|
||||
// #docregion upsert-target
|
||||
class MatchResults extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
TextColumn get teamA => text()();
|
||||
TextColumn get teamB => text()();
|
||||
BoolColumn get teamAWon => boolean()();
|
||||
|
||||
@override
|
||||
List<Set<Column<Object>>>? get uniqueKeys => [
|
||||
{teamA, teamB}
|
||||
];
|
||||
}
|
||||
// #enddocregion upsert-target
|
||||
|
||||
extension DocumentationSnippets on ModularAccessor {
|
||||
$WordsTable get words => throw 'stub';
|
||||
$MatchResultsTable get matches => throw 'stub';
|
||||
|
||||
// #docregion track-word
|
||||
Future<void> trackWord(String word) {
|
||||
return into(words).insert(
|
||||
WordsCompanion.insert(word: word),
|
||||
onConflict: DoUpdate(
|
||||
(old) => WordsCompanion.custom(usages: old.usages + Constant(1))),
|
||||
);
|
||||
}
|
||||
// #enddocregion track-word
|
||||
|
||||
// #docregion upsert-target
|
||||
Future<void> insertMatch(String teamA, String teamB, bool teamAWon) {
|
||||
final data = MatchResultsCompanion.insert(
|
||||
teamA: teamA, teamB: teamB, teamAWon: teamAWon);
|
||||
|
||||
return into(matches).insert(data,
|
||||
onConflict:
|
||||
DoUpdate((old) => data, target: [matches.teamA, matches.teamB]));
|
||||
}
|
||||
// #enddocregion upsert-target
|
||||
}
|
|
@ -0,0 +1,446 @@
|
|||
// ignore_for_file: type=lint
|
||||
import 'package:drift/drift.dart' as i0;
|
||||
import 'package:drift_docs/snippets/modular/upserts.drift.dart' as i1;
|
||||
import 'package:drift_docs/snippets/modular/upserts.dart' as i2;
|
||||
import 'package:drift/src/runtime/query_builder/query_builder.dart' as i3;
|
||||
|
||||
class $WordsTable extends i2.Words with i0.TableInfo<$WordsTable, i1.Word> {
|
||||
@override
|
||||
final i0.GeneratedDatabase attachedDatabase;
|
||||
final String? _alias;
|
||||
$WordsTable(this.attachedDatabase, [this._alias]);
|
||||
static const i0.VerificationMeta _wordMeta =
|
||||
const i0.VerificationMeta('word');
|
||||
@override
|
||||
late final i0.GeneratedColumn<String> word = i0.GeneratedColumn<String>(
|
||||
'word', aliasedName, false,
|
||||
type: i0.DriftSqlType.string, requiredDuringInsert: true);
|
||||
static const i0.VerificationMeta _usagesMeta =
|
||||
const i0.VerificationMeta('usages');
|
||||
@override
|
||||
late final i0.GeneratedColumn<int> usages = i0.GeneratedColumn<int>(
|
||||
'usages', aliasedName, false,
|
||||
type: i0.DriftSqlType.int,
|
||||
requiredDuringInsert: false,
|
||||
defaultValue: const i3.Constant(1));
|
||||
@override
|
||||
List<i0.GeneratedColumn> get $columns => [word, usages];
|
||||
@override
|
||||
String get aliasedName => _alias ?? actualTableName;
|
||||
@override
|
||||
String get actualTableName => $name;
|
||||
static const String $name = 'words';
|
||||
@override
|
||||
i0.VerificationContext validateIntegrity(i0.Insertable<i1.Word> instance,
|
||||
{bool isInserting = false}) {
|
||||
final context = i0.VerificationContext();
|
||||
final data = instance.toColumns(true);
|
||||
if (data.containsKey('word')) {
|
||||
context.handle(
|
||||
_wordMeta, word.isAcceptableOrUnknown(data['word']!, _wordMeta));
|
||||
} else if (isInserting) {
|
||||
context.missing(_wordMeta);
|
||||
}
|
||||
if (data.containsKey('usages')) {
|
||||
context.handle(_usagesMeta,
|
||||
usages.isAcceptableOrUnknown(data['usages']!, _usagesMeta));
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
@override
|
||||
Set<i0.GeneratedColumn> get $primaryKey => {word};
|
||||
@override
|
||||
i1.Word map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
|
||||
return i1.Word(
|
||||
word: attachedDatabase.typeMapping
|
||||
.read(i0.DriftSqlType.string, data['${effectivePrefix}word'])!,
|
||||
usages: attachedDatabase.typeMapping
|
||||
.read(i0.DriftSqlType.int, data['${effectivePrefix}usages'])!,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
$WordsTable createAlias(String alias) {
|
||||
return $WordsTable(attachedDatabase, alias);
|
||||
}
|
||||
}
|
||||
|
||||
class Word extends i0.DataClass implements i0.Insertable<i1.Word> {
|
||||
final String word;
|
||||
final int usages;
|
||||
const Word({required this.word, required this.usages});
|
||||
@override
|
||||
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, i0.Expression>{};
|
||||
map['word'] = i0.Variable<String>(word);
|
||||
map['usages'] = i0.Variable<int>(usages);
|
||||
return map;
|
||||
}
|
||||
|
||||
i1.WordsCompanion toCompanion(bool nullToAbsent) {
|
||||
return i1.WordsCompanion(
|
||||
word: i0.Value(word),
|
||||
usages: i0.Value(usages),
|
||||
);
|
||||
}
|
||||
|
||||
factory Word.fromJson(Map<String, dynamic> json,
|
||||
{i0.ValueSerializer? serializer}) {
|
||||
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
|
||||
return Word(
|
||||
word: serializer.fromJson<String>(json['word']),
|
||||
usages: serializer.fromJson<int>(json['usages']),
|
||||
);
|
||||
}
|
||||
@override
|
||||
Map<String, dynamic> toJson({i0.ValueSerializer? serializer}) {
|
||||
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
|
||||
return <String, dynamic>{
|
||||
'word': serializer.toJson<String>(word),
|
||||
'usages': serializer.toJson<int>(usages),
|
||||
};
|
||||
}
|
||||
|
||||
i1.Word copyWith({String? word, int? usages}) => i1.Word(
|
||||
word: word ?? this.word,
|
||||
usages: usages ?? this.usages,
|
||||
);
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('Word(')
|
||||
..write('word: $word, ')
|
||||
..write('usages: $usages')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(word, usages);
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
(other is i1.Word &&
|
||||
other.word == this.word &&
|
||||
other.usages == this.usages);
|
||||
}
|
||||
|
||||
class WordsCompanion extends i0.UpdateCompanion<i1.Word> {
|
||||
final i0.Value<String> word;
|
||||
final i0.Value<int> usages;
|
||||
final i0.Value<int> rowid;
|
||||
const WordsCompanion({
|
||||
this.word = const i0.Value.absent(),
|
||||
this.usages = const i0.Value.absent(),
|
||||
this.rowid = const i0.Value.absent(),
|
||||
});
|
||||
WordsCompanion.insert({
|
||||
required String word,
|
||||
this.usages = const i0.Value.absent(),
|
||||
this.rowid = const i0.Value.absent(),
|
||||
}) : word = i0.Value(word);
|
||||
static i0.Insertable<i1.Word> custom({
|
||||
i0.Expression<String>? word,
|
||||
i0.Expression<int>? usages,
|
||||
i0.Expression<int>? rowid,
|
||||
}) {
|
||||
return i0.RawValuesInsertable({
|
||||
if (word != null) 'word': word,
|
||||
if (usages != null) 'usages': usages,
|
||||
if (rowid != null) 'rowid': rowid,
|
||||
});
|
||||
}
|
||||
|
||||
i1.WordsCompanion copyWith(
|
||||
{i0.Value<String>? word, i0.Value<int>? usages, i0.Value<int>? rowid}) {
|
||||
return i1.WordsCompanion(
|
||||
word: word ?? this.word,
|
||||
usages: usages ?? this.usages,
|
||||
rowid: rowid ?? this.rowid,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, i0.Expression>{};
|
||||
if (word.present) {
|
||||
map['word'] = i0.Variable<String>(word.value);
|
||||
}
|
||||
if (usages.present) {
|
||||
map['usages'] = i0.Variable<int>(usages.value);
|
||||
}
|
||||
if (rowid.present) {
|
||||
map['rowid'] = i0.Variable<int>(rowid.value);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('WordsCompanion(')
|
||||
..write('word: $word, ')
|
||||
..write('usages: $usages, ')
|
||||
..write('rowid: $rowid')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
|
||||
class $MatchResultsTable extends i2.MatchResults
|
||||
with i0.TableInfo<$MatchResultsTable, i1.MatchResult> {
|
||||
@override
|
||||
final i0.GeneratedDatabase attachedDatabase;
|
||||
final String? _alias;
|
||||
$MatchResultsTable(this.attachedDatabase, [this._alias]);
|
||||
static const i0.VerificationMeta _idMeta = const i0.VerificationMeta('id');
|
||||
@override
|
||||
late final i0.GeneratedColumn<int> id = i0.GeneratedColumn<int>(
|
||||
'id', aliasedName, false,
|
||||
hasAutoIncrement: true,
|
||||
type: i0.DriftSqlType.int,
|
||||
requiredDuringInsert: false,
|
||||
defaultConstraints:
|
||||
i0.GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT'));
|
||||
static const i0.VerificationMeta _teamAMeta =
|
||||
const i0.VerificationMeta('teamA');
|
||||
@override
|
||||
late final i0.GeneratedColumn<String> teamA = i0.GeneratedColumn<String>(
|
||||
'team_a', aliasedName, false,
|
||||
type: i0.DriftSqlType.string, requiredDuringInsert: true);
|
||||
static const i0.VerificationMeta _teamBMeta =
|
||||
const i0.VerificationMeta('teamB');
|
||||
@override
|
||||
late final i0.GeneratedColumn<String> teamB = i0.GeneratedColumn<String>(
|
||||
'team_b', aliasedName, false,
|
||||
type: i0.DriftSqlType.string, requiredDuringInsert: true);
|
||||
static const i0.VerificationMeta _teamAWonMeta =
|
||||
const i0.VerificationMeta('teamAWon');
|
||||
@override
|
||||
late final i0.GeneratedColumn<bool> teamAWon = i0.GeneratedColumn<bool>(
|
||||
'team_a_won', aliasedName, false,
|
||||
type: i0.DriftSqlType.bool,
|
||||
requiredDuringInsert: true,
|
||||
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
|
||||
'CHECK ("team_a_won" IN (0, 1))'));
|
||||
@override
|
||||
List<i0.GeneratedColumn> get $columns => [id, teamA, teamB, teamAWon];
|
||||
@override
|
||||
String get aliasedName => _alias ?? actualTableName;
|
||||
@override
|
||||
String get actualTableName => $name;
|
||||
static const String $name = 'match_results';
|
||||
@override
|
||||
i0.VerificationContext validateIntegrity(
|
||||
i0.Insertable<i1.MatchResult> instance,
|
||||
{bool isInserting = false}) {
|
||||
final context = i0.VerificationContext();
|
||||
final data = instance.toColumns(true);
|
||||
if (data.containsKey('id')) {
|
||||
context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta));
|
||||
}
|
||||
if (data.containsKey('team_a')) {
|
||||
context.handle(
|
||||
_teamAMeta, teamA.isAcceptableOrUnknown(data['team_a']!, _teamAMeta));
|
||||
} else if (isInserting) {
|
||||
context.missing(_teamAMeta);
|
||||
}
|
||||
if (data.containsKey('team_b')) {
|
||||
context.handle(
|
||||
_teamBMeta, teamB.isAcceptableOrUnknown(data['team_b']!, _teamBMeta));
|
||||
} else if (isInserting) {
|
||||
context.missing(_teamBMeta);
|
||||
}
|
||||
if (data.containsKey('team_a_won')) {
|
||||
context.handle(_teamAWonMeta,
|
||||
teamAWon.isAcceptableOrUnknown(data['team_a_won']!, _teamAWonMeta));
|
||||
} else if (isInserting) {
|
||||
context.missing(_teamAWonMeta);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
@override
|
||||
Set<i0.GeneratedColumn> get $primaryKey => {id};
|
||||
@override
|
||||
List<Set<i0.GeneratedColumn>> get uniqueKeys => [
|
||||
{teamA, teamB},
|
||||
];
|
||||
@override
|
||||
i1.MatchResult map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
|
||||
return i1.MatchResult(
|
||||
id: attachedDatabase.typeMapping
|
||||
.read(i0.DriftSqlType.int, data['${effectivePrefix}id'])!,
|
||||
teamA: attachedDatabase.typeMapping
|
||||
.read(i0.DriftSqlType.string, data['${effectivePrefix}team_a'])!,
|
||||
teamB: attachedDatabase.typeMapping
|
||||
.read(i0.DriftSqlType.string, data['${effectivePrefix}team_b'])!,
|
||||
teamAWon: attachedDatabase.typeMapping
|
||||
.read(i0.DriftSqlType.bool, data['${effectivePrefix}team_a_won'])!,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
$MatchResultsTable createAlias(String alias) {
|
||||
return $MatchResultsTable(attachedDatabase, alias);
|
||||
}
|
||||
}
|
||||
|
||||
class MatchResult extends i0.DataClass
|
||||
implements i0.Insertable<i1.MatchResult> {
|
||||
final int id;
|
||||
final String teamA;
|
||||
final String teamB;
|
||||
final bool teamAWon;
|
||||
const MatchResult(
|
||||
{required this.id,
|
||||
required this.teamA,
|
||||
required this.teamB,
|
||||
required this.teamAWon});
|
||||
@override
|
||||
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, i0.Expression>{};
|
||||
map['id'] = i0.Variable<int>(id);
|
||||
map['team_a'] = i0.Variable<String>(teamA);
|
||||
map['team_b'] = i0.Variable<String>(teamB);
|
||||
map['team_a_won'] = i0.Variable<bool>(teamAWon);
|
||||
return map;
|
||||
}
|
||||
|
||||
i1.MatchResultsCompanion toCompanion(bool nullToAbsent) {
|
||||
return i1.MatchResultsCompanion(
|
||||
id: i0.Value(id),
|
||||
teamA: i0.Value(teamA),
|
||||
teamB: i0.Value(teamB),
|
||||
teamAWon: i0.Value(teamAWon),
|
||||
);
|
||||
}
|
||||
|
||||
factory MatchResult.fromJson(Map<String, dynamic> json,
|
||||
{i0.ValueSerializer? serializer}) {
|
||||
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
|
||||
return MatchResult(
|
||||
id: serializer.fromJson<int>(json['id']),
|
||||
teamA: serializer.fromJson<String>(json['teamA']),
|
||||
teamB: serializer.fromJson<String>(json['teamB']),
|
||||
teamAWon: serializer.fromJson<bool>(json['teamAWon']),
|
||||
);
|
||||
}
|
||||
@override
|
||||
Map<String, dynamic> toJson({i0.ValueSerializer? serializer}) {
|
||||
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
|
||||
return <String, dynamic>{
|
||||
'id': serializer.toJson<int>(id),
|
||||
'teamA': serializer.toJson<String>(teamA),
|
||||
'teamB': serializer.toJson<String>(teamB),
|
||||
'teamAWon': serializer.toJson<bool>(teamAWon),
|
||||
};
|
||||
}
|
||||
|
||||
i1.MatchResult copyWith(
|
||||
{int? id, String? teamA, String? teamB, bool? teamAWon}) =>
|
||||
i1.MatchResult(
|
||||
id: id ?? this.id,
|
||||
teamA: teamA ?? this.teamA,
|
||||
teamB: teamB ?? this.teamB,
|
||||
teamAWon: teamAWon ?? this.teamAWon,
|
||||
);
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('MatchResult(')
|
||||
..write('id: $id, ')
|
||||
..write('teamA: $teamA, ')
|
||||
..write('teamB: $teamB, ')
|
||||
..write('teamAWon: $teamAWon')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(id, teamA, teamB, teamAWon);
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
(other is i1.MatchResult &&
|
||||
other.id == this.id &&
|
||||
other.teamA == this.teamA &&
|
||||
other.teamB == this.teamB &&
|
||||
other.teamAWon == this.teamAWon);
|
||||
}
|
||||
|
||||
class MatchResultsCompanion extends i0.UpdateCompanion<i1.MatchResult> {
|
||||
final i0.Value<int> id;
|
||||
final i0.Value<String> teamA;
|
||||
final i0.Value<String> teamB;
|
||||
final i0.Value<bool> teamAWon;
|
||||
const MatchResultsCompanion({
|
||||
this.id = const i0.Value.absent(),
|
||||
this.teamA = const i0.Value.absent(),
|
||||
this.teamB = const i0.Value.absent(),
|
||||
this.teamAWon = const i0.Value.absent(),
|
||||
});
|
||||
MatchResultsCompanion.insert({
|
||||
this.id = const i0.Value.absent(),
|
||||
required String teamA,
|
||||
required String teamB,
|
||||
required bool teamAWon,
|
||||
}) : teamA = i0.Value(teamA),
|
||||
teamB = i0.Value(teamB),
|
||||
teamAWon = i0.Value(teamAWon);
|
||||
static i0.Insertable<i1.MatchResult> custom({
|
||||
i0.Expression<int>? id,
|
||||
i0.Expression<String>? teamA,
|
||||
i0.Expression<String>? teamB,
|
||||
i0.Expression<bool>? teamAWon,
|
||||
}) {
|
||||
return i0.RawValuesInsertable({
|
||||
if (id != null) 'id': id,
|
||||
if (teamA != null) 'team_a': teamA,
|
||||
if (teamB != null) 'team_b': teamB,
|
||||
if (teamAWon != null) 'team_a_won': teamAWon,
|
||||
});
|
||||
}
|
||||
|
||||
i1.MatchResultsCompanion copyWith(
|
||||
{i0.Value<int>? id,
|
||||
i0.Value<String>? teamA,
|
||||
i0.Value<String>? teamB,
|
||||
i0.Value<bool>? teamAWon}) {
|
||||
return i1.MatchResultsCompanion(
|
||||
id: id ?? this.id,
|
||||
teamA: teamA ?? this.teamA,
|
||||
teamB: teamB ?? this.teamB,
|
||||
teamAWon: teamAWon ?? this.teamAWon,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, i0.Expression>{};
|
||||
if (id.present) {
|
||||
map['id'] = i0.Variable<int>(id.value);
|
||||
}
|
||||
if (teamA.present) {
|
||||
map['team_a'] = i0.Variable<String>(teamA.value);
|
||||
}
|
||||
if (teamB.present) {
|
||||
map['team_b'] = i0.Variable<String>(teamB.value);
|
||||
}
|
||||
if (teamAWon.present) {
|
||||
map['team_a_won'] = i0.Variable<bool>(teamAWon.value);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('MatchResultsCompanion(')
|
||||
..write('id: $id, ')
|
||||
..write('teamA: $teamA, ')
|
||||
..write('teamB: $teamB, ')
|
||||
..write('teamAWon: $teamAWon')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:drift_postgres/drift_postgres.dart';
|
||||
import 'package:postgres/postgres_v3_experimental.dart';
|
||||
import 'package:postgres/postgres.dart';
|
||||
|
||||
part 'postgres.g.dart';
|
||||
|
||||
|
@ -20,12 +20,17 @@ class MyDatabase extends _$MyDatabase {
|
|||
|
||||
void main() async {
|
||||
final pgDatabase = PgDatabase(
|
||||
endpoint: PgEndpoint(
|
||||
endpoint: Endpoint(
|
||||
host: 'localhost',
|
||||
database: 'postgres',
|
||||
username: 'postgres',
|
||||
password: 'postgres',
|
||||
),
|
||||
settings: ConnectionSettings(
|
||||
// If you expect to talk to a Postgres database over a public connection,
|
||||
// please use SslMode.verifyFull instead.
|
||||
sslMode: SslMode.disable,
|
||||
),
|
||||
);
|
||||
|
||||
final driftDatabase = MyDatabase(pgDatabase);
|
||||
|
|
|
@ -28,7 +28,7 @@ DatabaseConnection connectOnWeb() {
|
|||
|
||||
// You can then use this method to open your database:
|
||||
class MyWebDatabase extends _$MyWebDatabase {
|
||||
MyWebDatabase._(QueryExecutor e) : super(e);
|
||||
MyWebDatabase._(super.e);
|
||||
|
||||
factory MyWebDatabase() => MyWebDatabase._(connectOnWeb());
|
||||
// ...
|
||||
|
|
|
@ -10,6 +10,8 @@ import 'dart:io';
|
|||
import 'package:drift/native.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:sqlite3/sqlite3.dart';
|
||||
import 'package:sqlite3_flutter_libs/sqlite3_flutter_libs.dart';
|
||||
|
||||
// ... the TodoItems table definition stays the same
|
||||
// #enddocregion open
|
||||
|
@ -47,6 +49,19 @@ LazyDatabase _openConnection() {
|
|||
// for your app.
|
||||
final dbFolder = await getApplicationDocumentsDirectory();
|
||||
final file = File(p.join(dbFolder.path, 'db.sqlite'));
|
||||
|
||||
// Also work around limitations on old Android versions
|
||||
if (Platform.isAndroid) {
|
||||
await applyWorkaroundToOpenSqlite3OnOldAndroidVersions();
|
||||
}
|
||||
|
||||
// Make sqlite3 pick a more suitable location for temporary files - the
|
||||
// one from the system may be inaccessible due to sandboxing.
|
||||
final cachebase = (await getTemporaryDirectory()).path;
|
||||
// We can't access /tmp on Android, which sqlite3 would try by default.
|
||||
// Explicitly tell it about the correct temporary directory.
|
||||
sqlite3.tempDirectory = cachebase;
|
||||
|
||||
return NativeDatabase.createInBackground(file);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -9,9 +9,9 @@ path: docs/getting-started/expressions/
|
|||
template: layouts/docs/single
|
||||
---
|
||||
|
||||
Expressions are pieces of sql that return a value when the database interprets them.
|
||||
Expressions are pieces of SQL that return a value when the database interprets them.
|
||||
The Dart API from drift allows you to write most expressions in Dart and then convert
|
||||
them to sql. Expressions are used in all kinds of situations. For instance, `where`
|
||||
them to SQL. Expressions are used in all kinds of situations. For instance, `where`
|
||||
expects an expression that returns a boolean.
|
||||
|
||||
In most cases, you're writing an expression that combines other expressions. Any
|
||||
|
@ -62,7 +62,7 @@ Expression.and([
|
|||
## 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`:
|
||||
run calculations between an SQL expression and a Dart value, wrap it in a `Variable`:
|
||||
```dart
|
||||
Future<List<Product>> canBeBought(int amount, int price) {
|
||||
return (select(products)..where((p) {
|
||||
|
@ -73,7 +73,7 @@ Future<List<Product>> canBeBought(int amount, int price) {
|
|||
```
|
||||
|
||||
String expressions define a `+` operator as well. Just like you would expect, it performs
|
||||
a concatenation in sql.
|
||||
a concatenation in SQL.
|
||||
|
||||
For integer values, you can use `~`, `bitwiseAnd` and `bitwiseOr` to perform
|
||||
bitwise operations:
|
||||
|
@ -81,7 +81,7 @@ 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:
|
||||
To check whether an expression evaluates to `NULL` in SQL, you can use the `isNull` extension:
|
||||
|
||||
```dart
|
||||
final withoutCategories = select(todos)..where((row) => row.category.isNull());
|
||||
|
@ -213,7 +213,7 @@ with the `separator` argument on `groupConcat`.
|
|||
## Mathematical functions and regexp
|
||||
|
||||
When using a `NativeDatabase`, a basic set of trigonometric functions will be available.
|
||||
It also defines the `REGEXP` function, which allows you to use `a REGEXP b` in sql queries.
|
||||
It also defines the `REGEXP` function, which allows you to use `a REGEXP b` in SQL queries.
|
||||
For more information, see the [list of functions]({{ "../Platforms/vm.md#moor-only-functions" | pageUrl }}) here.
|
||||
|
||||
## Subqueries
|
||||
|
@ -261,8 +261,8 @@ Drift also supports subqueries that appear in `JOIN`s, which are described in th
|
|||
[documentation for joins]({{ 'select.md#subqueries' | pageUrl }}).
|
||||
|
||||
## Custom expressions
|
||||
If you want to inline custom sql into Dart queries, you can use a `CustomExpression` class.
|
||||
It takes a `sql` parameter that lets you write custom expressions:
|
||||
If you want to inline custom SQL into Dart queries, you can use a `CustomExpression` class.
|
||||
It takes an `sql` parameter that lets you write custom expressions:
|
||||
```dart
|
||||
const inactive = CustomExpression<bool, BoolType>("julianday('now') - julianday(last_login) > 60");
|
||||
select(users)..where((u) => inactive);
|
||||
|
@ -270,5 +270,5 @@ select(users)..where((u) => inactive);
|
|||
|
||||
_Note_: It's easy to write invalid queries by using `CustomExpressions` too much. If you feel like
|
||||
you need to use them because a feature you use is not available in drift, consider creating an issue
|
||||
to let us know. If you just prefer sql, you could also take a look at
|
||||
[compiled sql]({{ "../SQL API/custom_queries.md" | pageUrl }}) which is typesafe to use.
|
||||
to let us know. If you just prefer SQL, you could also take a look at
|
||||
[compiled SQL]({{ "../SQL API/custom_queries.md" | pageUrl }}) which is type-safe to use.
|
||||
|
|
|
@ -10,7 +10,7 @@ aliases:
|
|||
|
||||
{% assign snippets = 'package:drift_docs/snippets/modular/schema_inspection.dart.excerpt.json' | readString | json_decode %}
|
||||
|
||||
Thanks to the typesafe table classes generated by drift, [writing SQL queries]({{ '../Dart API/select.md' | pageUrl }}) in Dart
|
||||
Thanks to the type-safe table classes generated by drift, [writing SQL queries]({{ '../Dart API/select.md' | pageUrl }}) in Dart
|
||||
is simple and safe.
|
||||
However, these queries are usually written against a specific table. And while drift supports inheritance for tables, sometimes it is easier
|
||||
to access tables reflectively. Luckily, code generated by drift implements interfaces which can be used to do just that.
|
||||
|
|
|
@ -11,7 +11,7 @@ path: /docs/getting-started/advanced_dart_tables/
|
|||
{% assign setup = 'package:drift_docs/snippets/setup/database.dart.excerpt.json' | readString | json_decode %}
|
||||
|
||||
In relational databases, tables are used to describe the structure of rows. By
|
||||
adhering to a predefined schema, drift can generate typesafe code for your
|
||||
adhering to a predefined schema, drift can generate type-safe code for your
|
||||
database.
|
||||
As already shown in the [setup]({{ '../setup.md#database-class' | pageUrl }})
|
||||
page, drift provides APIs to declare tables in Dart:
|
||||
|
|
|
@ -109,7 +109,9 @@ This makes them suitable for bulk insert or update operations.
|
|||
|
||||
### Upserts
|
||||
|
||||
Upserts are a feature from newer sqlite3 versions that allows an insert to
|
||||
{% assign upserts = "package:drift_docs/snippets/modular/upserts.dart.excerpt.json" | readString | json_decode %}
|
||||
|
||||
Upserts are a feature from newer sqlite3 versions that allows an insert to
|
||||
behave like an update if a conflicting row already exists.
|
||||
|
||||
This allows us to create or override an existing row when its primary key is
|
||||
|
@ -129,35 +131,20 @@ Future<int> createOrUpdateUser(User user) {
|
|||
}
|
||||
```
|
||||
|
||||
When calling `createOrUpdateUser()` with an email address that already exists,
|
||||
When calling `createOrUpdateUser()` with an email address that already exists,
|
||||
that user's name will be updated. Otherwise, a new user will be inserted into
|
||||
the database.
|
||||
|
||||
Inserts can also be used with more advanced queries. For instance, let's say
|
||||
we're building a dictionary and want to keep track of how many times we
|
||||
Inserts can also be used with more advanced queries. For instance, let's say
|
||||
we're building a dictionary and want to keep track of how many times we
|
||||
encountered a word. A table for that might look like
|
||||
|
||||
```dart
|
||||
class Words extends Table {
|
||||
TextColumn get word => text()();
|
||||
IntColumn get usages => integer().withDefault(const Constant(1))();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {word};
|
||||
}
|
||||
```
|
||||
{% include "blocks/snippet" snippets = upserts name = "words-table" %}
|
||||
|
||||
By using a custom upserts, we can insert a new word or increment its `usages`
|
||||
counter if it already exists:
|
||||
|
||||
```dart
|
||||
Future<void> trackWord(String word) {
|
||||
return into(words).insert(
|
||||
WordsCompanion.insert(word: word),
|
||||
onConflict: DoUpdate((old) => WordsCompanion.custom(usages: old.usages + Constant(1))),
|
||||
);
|
||||
}
|
||||
```
|
||||
{% include "blocks/snippet" snippets = upserts name = "track-word" %}
|
||||
|
||||
{% block "blocks/alert" title="Unique constraints and conflict targets" %}
|
||||
Both `insertOnConflictUpdate` and `onConflict: DoUpdate` use an `DO UPDATE`
|
||||
|
@ -165,7 +152,10 @@ upsert in sql. This requires us to provide a so-called "conflict target", a
|
|||
set of columns to check for uniqueness violations. By default, drift will use
|
||||
the table's primary key as conflict target. That works in most cases, but if
|
||||
you have custom `UNIQUE` constraints on some columns, you'll need to use
|
||||
the `target` parameter on `DoUpdate` in Dart to include those columns.
|
||||
the `target` parameter on `DoUpdate` in Dart to include those columns:
|
||||
|
||||
{% include "blocks/snippet" snippets = upserts name = "upsert-target" %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
Note that this requires a fairly recent sqlite3 version (3.24.0) that might not
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
data:
|
||||
title: "Many to many relationships"
|
||||
title: "Many-to-many relationships"
|
||||
description: An example that models a shopping cart system with drift.
|
||||
template: layouts/docs/single
|
||||
---
|
||||
|
@ -19,16 +19,16 @@ A product can be in many shopping carts at the same time, and carts can of cours
|
|||
contain more than one product too.
|
||||
|
||||
In sqlite3, there are two good ways to model many-to-many relationships
|
||||
betweeen tables:
|
||||
between tables:
|
||||
|
||||
1. The traditional way of using a third table storing every combination of
|
||||
products and carts.
|
||||
2. A more modern way might be to store product ids in a shopping cart as a JSON
|
||||
2. A more modern way might be to store product IDs in a shopping cart as a JSON
|
||||
array.
|
||||
|
||||
The two approaches have different upsides and downsides. With the traditional
|
||||
relational way, it's easier to ensure data integrity (by, for instance, deleting
|
||||
product references out of shopping carts when they a product is deleted).
|
||||
product references out of shopping carts when a product is deleted).
|
||||
On the other hand, queries are easier to write with JSON structures. Especially
|
||||
when the order of products in the shopping cart is important as well, a JSON
|
||||
list is very helpful since rows in a table are unordered.
|
||||
|
@ -109,11 +109,11 @@ in a single table:
|
|||
|
||||
To select a single cart, we can use the [`json_each`](https://sqlite.org/json1.html#jeach)
|
||||
function from sqlite3 to "join" each item stored in the JSON array as if it were a separate
|
||||
row. That way, we can efficiently lookup all items in a cart:
|
||||
row. That way, we can efficiently look up all items in a cart:
|
||||
|
||||
{% include "blocks/snippet" snippets=json name="watchCart" %}
|
||||
|
||||
Watching all carts isn't that much harder, we just remove the `where` clause and
|
||||
combine all rows into a map from carts to their items:
|
||||
|
||||
{% include "blocks/snippet" snippets=json name="watchAllCarts" %}
|
||||
{% include "blocks/snippet" snippets=json name="watchAllCarts" %}
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
data:
|
||||
title: "Tracing database operations"
|
||||
description: Using the `QueryInterceptor` API to log details about database operations.
|
||||
template: layouts/docs/single
|
||||
---
|
||||
|
||||
{% assign snippets = 'package:drift_docs/snippets/log_interceptor.dart.excerpt.json' | readString | json_decode %}
|
||||
|
||||
Drift provides the relatively simple `logStatements` option to print the statements it
|
||||
executes.
|
||||
The `QueryInterceptor` API can be used to extend this logging to provide more information,
|
||||
which this example will show.
|
||||
|
||||
{% include "blocks/snippet" snippets=snippets name="class" %}
|
||||
|
||||
Interceptors can be applied with the `interceptWith` extension on `QueryExecutor` and
|
||||
`DatabaseConnection`:
|
||||
|
||||
{% include "blocks/snippet" snippets=snippets name="use" %}
|
||||
|
||||
The `QueryInterceptor` class is pretty powerful, as it allows you to fully control the underlying
|
||||
database connection. You could also use it to retry some failing statements or to aggregate
|
||||
statistics about query times to an external monitoring service.
|
|
@ -15,8 +15,21 @@ targets:
|
|||
drift:
|
||||
auto_apply_builders: false
|
||||
builders:
|
||||
drift_dev:analyzer:
|
||||
enabled: true
|
||||
options: &options
|
||||
# Drift build options, as per https://drift.simonbinder.eu/docs/advanced-features/builder_options/
|
||||
store_date_time_values_as_text: true
|
||||
named_parameters: true
|
||||
sql:
|
||||
dialect: sqlite
|
||||
options:
|
||||
version: "3.39"
|
||||
modules: [fts5]
|
||||
drift_dev:modular:
|
||||
enabled: true
|
||||
# We use yaml anchors to give the two builders the same options
|
||||
options: *options
|
||||
|
||||
$default:
|
||||
dependencies:
|
||||
|
@ -27,7 +40,6 @@ targets:
|
|||
# its own target instead.
|
||||
drift_dev:
|
||||
enabled: false
|
||||
|
||||
```
|
||||
|
||||
With modular generation, you'll have to replace the `part` statement in the database file with an
|
||||
|
@ -59,3 +71,41 @@ and use the non-shared generator instead.
|
|||
Finally, we need to the build system to run drift first, and all the other builders otherwise. This is
|
||||
why we split the builders up into multiple targets. The first target will only run drift, the second
|
||||
target has a dependency on the first one and will run all the other builders.
|
||||
|
||||
## Using `drift_dev:not_shared`
|
||||
|
||||
For complex build setups like those requiring other builders to see drift code, the `drift_dev:modular`
|
||||
builder is recommended.
|
||||
However, enabling the modular builder requires other code modifications like replacing `part` statements
|
||||
with imports. A simpler change may be the `not_shared` builder offered by `drift_dev`. It works like the
|
||||
default setup, except that it emits a `.drift.dart` part file instead of a shared `.g.dart` file - so you
|
||||
only have to change a single `part` statement to migrate.
|
||||
|
||||
To enable this builder, also enable the `drift_dev:analyzer` builder and the `has_separate_analyzer`
|
||||
option:
|
||||
|
||||
```yaml
|
||||
targets:
|
||||
drift:
|
||||
auto_apply_builders: false
|
||||
builders:
|
||||
drift_dev:analyzer:
|
||||
enabled: true
|
||||
options: &options
|
||||
has_separate_analyzer: true # always enable this option when using `not_shared`
|
||||
# remaining options...
|
||||
drift_dev:not_shared:
|
||||
enabled: true
|
||||
# We use yaml anchors to give the two builders the same options
|
||||
options: *options
|
||||
|
||||
$default:
|
||||
dependencies:
|
||||
# run drift's builder first
|
||||
- ":drift"
|
||||
builders:
|
||||
# This builder is enabled by default, but we're using the modular builder in
|
||||
# its own target instead.
|
||||
drift_dev:
|
||||
enabled: false
|
||||
```
|
||||
|
|
|
@ -77,6 +77,9 @@ At the moment, drift supports these options:
|
|||
The possible values are `preserve`, `camelCase`, `CONSTANT_CASE`, `snake_case`, `PascalCase`, `lowercase` and `UPPERCASE` (default: `snake_case`).
|
||||
* `write_to_columns_mixins`: Whether the `toColumns` method should be written as a mixin instead of being added directly to the data class.
|
||||
This is useful when using [existing row classes]({{ '../custom_row_classes.md' | pageUrl }}), as the mixin is generated for those as well.
|
||||
* `has_separate_analyzer`: This option is only relevant when using the `drift_dev:not_shared` builder, which needs to use a less efficient
|
||||
analysis implementation than the other builders by default. After also applying `drift_dev:analyzer` to the same build target, this option
|
||||
can be enabled to speed up builds. This option has no effect with the default or the modular builder.
|
||||
* `fatal_warnings`: When enabled (defaults to `false`), warnings found by `drift_dev` in the build process (like syntax errors in SQL queries or
|
||||
unresolved references in your Dart tables) will cause the build to fail.
|
||||
* `preamble`: This option is useful when using drift [as a standalone part builder](#using-drift-classes-in-other-builders) or when running a
|
||||
|
|
|
@ -1,19 +1,16 @@
|
|||
---
|
||||
data:
|
||||
title: PostgreSQL support (Alpha)
|
||||
title: PostgreSQL support
|
||||
description: Use drift with PostgreSQL database servers.
|
||||
weight: 10
|
||||
template: layouts/docs/single
|
||||
---
|
||||
|
||||
{% block "blocks/pageinfo" %}
|
||||
Postgres support is still in development. In particular, drift is waiting for [version 3](https://github.com/isoos/postgresql-dart/issues/105)
|
||||
of the postgres package to stabilize. Minor breaking changes or remaining issues are not unlikely.
|
||||
{% endblock %}
|
||||
|
||||
Thanks to contributions from the community, drift currently has alpha support for postgres with the `drift_postgres` package.
|
||||
Without having to change your query code, drift can generate Postgres-compatible SQL for most queries,
|
||||
allowing you to use your drift databases with a Postgres database server.
|
||||
While drift has originally been designed as a client-side database wrapper for SQLite databases, it can also be used
|
||||
with PostgreSQL database servers.
|
||||
Without having to change your query code, drift can generate Postgres-compatible SQL for most queries.
|
||||
Please keep in mind that some drift APIs, like those for date time modification, are only supported with SQLite.
|
||||
Most queries will work without any modification though.
|
||||
|
||||
## Setup
|
||||
|
||||
|
@ -68,11 +65,12 @@ disable migrations in postgres by passing `enableMigrations: false` to the `PgDa
|
|||
|
||||
## Current state
|
||||
|
||||
Drift's support for Postgres is still in development, and the integration tests we have for Postgres are
|
||||
less extensive than the tests for sqlite3 databases.
|
||||
Also, some parts of the core APIs (like the datetime expressions API) are direct wrappers around SQL
|
||||
functions only available in sqlite3 and won't work in Postgres.
|
||||
However, you can already create tables (or use an existing schema) and most queries should work already.
|
||||
Drift's support for PostgreSQL is stable in the sense that the current API is unlikely to break.
|
||||
Still, it is a newer implementation and integration tests for PostgreSQL are less extensive than
|
||||
the tests for SQLite databases. And while drift offers typed wrappers around most functions supported
|
||||
by SQLite, only a tiny subset of PostgreSQL's advanced operators and functions are exposed by
|
||||
`drift_postgres`.
|
||||
|
||||
If you're running into problems or bugs with the postgres database, please let us know by creating an issue
|
||||
or a discussion.
|
||||
or a discussion.
|
||||
Contributions expanding wrappers around PosgreSQL functions are also much appreciated.
|
||||
|
|
|
@ -25,7 +25,7 @@ the second section gives an example for a custom query defined at runtime.
|
|||
|
||||
## Statements with a generated api
|
||||
|
||||
You can instruct drift to automatically generate a typesafe
|
||||
You can instruct drift to automatically generate a type-safe
|
||||
API for your select, update and delete statements. Of course, you can still write custom
|
||||
sql manually. See the sections below for details.
|
||||
|
||||
|
@ -84,21 +84,14 @@ items.
|
|||
|
||||
You can also bind SQL variables by using question-mark placeholders and the `variables` parameter:
|
||||
|
||||
```dart
|
||||
Stream<int> amountOfTodosInCategory(int id) {
|
||||
return customSelect(
|
||||
'SELECT COUNT(*) AS c FROM todos WHERE category = ?',
|
||||
variables: [Variable.withInt(id)],
|
||||
readsFrom: {todos},
|
||||
).map((row) => row.read<int>('c')).watch();
|
||||
}
|
||||
```
|
||||
{% include "blocks/snippet" snippets = snippets name = "amountOfTodosInCategory" %}
|
||||
|
||||
|
||||
Of course, you can also use indexed variables (like `?12`) - for more information on them, see
|
||||
[the sqlite3 documentation](https://sqlite.org/lang_expr.html#varparam).
|
||||
|
||||
## Custom update statements
|
||||
For update and delete statements, you can use `customUpdate`. Just like `customSelect`, that method
|
||||
also takes a sql statement and optional variables. You can also tell drift which tables will be
|
||||
also takes an SQL statement and optional variables. You can also tell drift which tables will be
|
||||
affected by your query using the optional `updates` parameter. That will help with other select
|
||||
streams, which will then update automatically.
|
||||
|
|
|
@ -22,7 +22,7 @@ template: layouts/docs/single
|
|||
Drift files are a new feature that lets you write all your database code in SQL.
|
||||
But unlike raw SQL strings you might pass to simple database clients, everything in a drift file is verified
|
||||
by drift's powerful SQL analyzer.
|
||||
This allows you to write SQL queries safer: Drift will find mistakes in them during builds, and it will generate typesafe
|
||||
This allows you to write SQL queries safer: Drift will find mistakes in them during builds, and it will generate type-safe
|
||||
Dart APIs for them so that you don't have to read back results manually.
|
||||
|
||||
## Getting started
|
||||
|
@ -63,7 +63,7 @@ Inside of named queries, you can use variables just like you would expect with
|
|||
sql. We support regular variables (`?`), explicitly indexed variables (`?123`)
|
||||
and colon-named variables (`:id`). We don't support variables declared
|
||||
with @ or $. The compiler will attempt to infer the variable's type by
|
||||
looking at its context. This lets drift generate typesafe apis for your
|
||||
looking at its context. This lets drift generate type-safe APIs for your
|
||||
queries, the variables will be written as parameters to your method.
|
||||
|
||||
When it's ambiguous, the analyzer might be unable to resolve the type of
|
||||
|
@ -476,4 +476,4 @@ At the moment, the following statements can appear in a `.drift` file.
|
|||
All imports must come before DDL statements, and those must come before named queries.
|
||||
|
||||
If you need support for another statement, or if drift rejects a query you think is valid, please
|
||||
create an issue!
|
||||
create an issue!
|
||||
|
|
|
@ -11,6 +11,16 @@ Do you have a drift-related package you want to share? Awesome, please let me kn
|
|||
[Twitter](https://twitter.com/dersimolus) or via email to oss <at>simonbinder<dot>eu.
|
||||
{% endblock %}
|
||||
|
||||
## Conflict-free replicated datatypes
|
||||
|
||||
Conflict-free replicated datatypes (CRDTs) enable synchronization and replication of data
|
||||
even when offline.
|
||||
The [sql\_crdt](https://pub.dev/packages/sql_crdt) package by Daniel Cachapa uses the
|
||||
`sqlparser` package from the drift project transforms SQL queries at runtime to implement
|
||||
CRDTs for databases.
|
||||
The [drift\_crdt](https://pub.dev/packages/drift_crdt) package by Janez Štupar provides a
|
||||
wrapper around this for drift.
|
||||
|
||||
## Storage inspector
|
||||
|
||||
[Nicola Verbeeck](https://github.com/NicolaVerbeeck) wrote the `storage_inspector` packages, which
|
||||
|
|
|
@ -50,7 +50,7 @@ If you want to use another constructor, set the `constructor` parameter on the
|
|||
{% assign snippets = "package:drift_docs/snippets/custom_row_classes/named.dart.excerpt.json" | readString | json_decode %}
|
||||
{% include "blocks/snippet" snippets = snippets name = "named" %}
|
||||
|
||||
### Static and aynchronous factories
|
||||
### Static and asynchronous factories
|
||||
|
||||
Starting with drift 2.0, the custom constructor set with the `constructor`
|
||||
parameter on the `@UseRowClass` annotation may also refer to a static method
|
||||
|
|
|
@ -124,7 +124,7 @@ Sqflite is a Flutter package that provides bindings to the sqlite api for both i
|
|||
and has stable api. In fact, the `moor_flutter` or `drift_sqflite` variants are built on top of sqflite. But even though sqflite
|
||||
has an api to construct some simple queries in Dart, drift goes a bit further by
|
||||
|
||||
* Generating typesafe mapping code for your queries
|
||||
* Generating type-safe mapping code for your queries
|
||||
* Providing auto-updating streams for queries
|
||||
* Managing `CREATE TABLE` statements and most schema migrations
|
||||
* A more fluent api to compose queries
|
||||
|
|
|
@ -11,20 +11,20 @@ Drift is a reactive persistence library for Dart and Flutter applications. It's
|
|||
of database libraries like [the sqlite3 package](https://pub.dev/packages/sqlite3), [sqflite](https://pub.dev/packages/sqflite) or [sql.js](https://github.com/sql-js/sql.js/)
|
||||
and provides additional features, like:
|
||||
|
||||
- __Type safety__: Instead of writing sql queries manually and parsing the `List<Map<String, dynamic>>` that they
|
||||
- __Type safety__: Instead of writing SQL queries manually and parsing the `List<Map<String, dynamic>>` that they
|
||||
return, drift turns rows into objects of your choice.
|
||||
- __Stream queries__: Drift lets you "watch" your queries with zero additional effort. Any query can be turned into
|
||||
an auto-updating stream that emits new items when the underlying data changes.
|
||||
- __Fluent queries__: Drift generates a Dart api that you can use to write queries and automatically get their results.
|
||||
Keep an updated list of all users with `select(users).watch()`. That's it! No sql to write, no rows to parse.
|
||||
- __Typesafe sql__: If you prefer to write sql, that's fine! Drift has an sql parser and analyzer built in. It can parse
|
||||
Keep an updated list of all users with `select(users).watch()`. That's it! No SQL to write, no rows to parse.
|
||||
- __Type-safe SQL__: If you prefer to write SQL, that's fine! Drift has an SQL parser and analyzer built in. It can parse
|
||||
your queries at compile time, figure out what columns they're going to return and generate Dart code to represent your
|
||||
rows.
|
||||
- __Migration utils__: Drift makes writing migrations easier thanks to utility functions like `.createAllTables()`.
|
||||
You don't need to manually write your `CREATE TABLE` statements and keep them updated.
|
||||
|
||||
And much more! Drift validates data before inserting it, so you can get helpful error messages instead of just an
|
||||
sql error code. Of course, it supports transactions. And DAOs. And efficient batched insert statements. The list goes on.
|
||||
SQL error code. Of course, it supports transactions. And DAOs. And efficient batched insert statements. The list goes on.
|
||||
|
||||
## Getting started
|
||||
|
||||
|
|
|
@ -26,7 +26,7 @@ If you want to look at an example app for inspiration, a cross-platform Flutter
|
|||
|
||||
## The dependencies {#adding-dependencies}
|
||||
|
||||
First, lets add drift to your project's `pubspec.yaml`.
|
||||
First, let's add drift to your project's `pubspec.yaml`.
|
||||
In addition to the core drift dependencies, we're also adding packages to find a suitable database
|
||||
location on the device and to include a recent version of `sqlite3`, the database most commonly
|
||||
used with drift.
|
||||
|
@ -66,7 +66,7 @@ If you're wondering why so many packages are necessary, here's a quick overview
|
|||
## Database class
|
||||
|
||||
Every project using drift needs at least one class to access a database. This class references all the
|
||||
tables you want to use and is the central entrypoint for drift's code generator.
|
||||
tables you want to use and is the central entry point for drift's code generator.
|
||||
In this example, we'll assume that this database class is defined in a file called `database.dart` and
|
||||
somewhere under `lib/`. Of course, you can put this class in any Dart file you like.
|
||||
|
||||
|
@ -96,6 +96,11 @@ now looks like this:
|
|||
|
||||
{% include "blocks/snippet" snippets = snippets name = 'open' %}
|
||||
|
||||
The Android-specific workarounds are necessary because sqlite3 attempts to use `/tmp` to store
|
||||
private data on unix-like systems, which is forbidden on Android. We also use this opportunity
|
||||
to work around a problem some older Android devices have with loading custom libraries through
|
||||
`dart:ffi`.
|
||||
|
||||
## Next steps
|
||||
|
||||
Congratulations! With this setup complete, your project is ready to use drift.
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
page:
|
||||
title: "Drift - Reactive, typesafe persistence library for Dart"
|
||||
title: "Drift - Reactive, type-safe persistence library for Dart"
|
||||
template: layouts/home.html
|
||||
path: ""
|
||||
data:
|
||||
|
@ -57,7 +57,7 @@ between all your revisions.
|
|||
|
||||
{% block "blocks/feature.html" icon="fas fa-database" title="Prefer SQL? Drift's got you covered!" %}
|
||||
{% block "blocks/markdown.html" %}
|
||||
Drift ships a powerful sql parser and analyzer, allowing it to create typesafe methods for all your sql queries. All sql queries are
|
||||
Drift ships a powerful SQL parser and analyzer, allowing it to create type-safe methods for all your SQL queries. All SQL queries are
|
||||
validated and analyzed during build-time, so drift can provide hints about potential errors quickly and generate efficient mapping
|
||||
code.
|
||||
Of course, you can mix SQL and Dart to your liking.
|
||||
|
|
|
@ -32,7 +32,7 @@ more flexibility when writing database code.
|
|||
{% block "blocks/section" color="light" %}
|
||||
{% block "blocks/feature" icon="fas fa-database" title="Pure SQL API" %} {% block "blocks/markdown" %}
|
||||
The new `.moor` files have been updated and can now hold both `CREATE TABLE` statements
|
||||
and queries you define. Moor will then generate typesafe Dart APIs based on your tables
|
||||
and queries you define. Moor will then generate type-safe Dart APIs based on your tables
|
||||
and statements.
|
||||
|
||||
[Get started with SQL and moor]({{ "docs/SQL API/index.md" | pageUrl }})
|
||||
|
@ -55,7 +55,7 @@ worlds.
|
|||
{% block "blocks/lead" color="green" %} {% block "blocks/markdown" %}
|
||||
## Builtin SQL IDE
|
||||
|
||||
Moor 2.0 expands the previous sql parser and analyzer, providing real-time feedback on your
|
||||
Moor 2.0 expands the previous SQL parser and analyzer, providing real-time feedback on your
|
||||
SQL queries as you type. Moor plugs right into the Dart analysis server, so you don't have
|
||||
to install any additional extensions.
|
||||
|
||||
|
@ -113,4 +113,4 @@ _Please not that the package is still in preview_
|
|||
- To get started with SQL in moor, or to migrate an existing project to moor, follow our
|
||||
[migration guide]({{ "docs/SQL API/index.md" | pageUrl }})
|
||||
|
||||
{% endblock %} {% endblock %}
|
||||
{% endblock %} {% endblock %}
|
||||
|
|
|
@ -19,9 +19,11 @@ dependencies:
|
|||
# used in snippets
|
||||
http: ^1.1.0
|
||||
sqlite3: ^2.0.0
|
||||
# Fake path_provider for snippets
|
||||
# Fake flutter packages for snippets
|
||||
path_provider:
|
||||
path: assets/path_provider
|
||||
sqlite3_flutter_libs:
|
||||
path: assets/sqlite3_flutter_libs
|
||||
# Used in examples
|
||||
rxdart: ^0.27.3
|
||||
yaml: ^3.1.1
|
||||
|
@ -31,10 +33,10 @@ dependencies:
|
|||
hosted: https://simonbinder.eu
|
||||
version: ^1.5.10
|
||||
test: ^1.18.0
|
||||
postgres: ^2.6.3
|
||||
postgres: ^3.0.0-0
|
||||
|
||||
dev_dependencies:
|
||||
lints: ^2.0.0
|
||||
lints: ^3.0.0
|
||||
build: ^2.1.0
|
||||
build_runner: ^2.0.5
|
||||
build_runner_core: ^7.2.7
|
||||
|
|
|
@ -11,7 +11,7 @@ class Users extends Table {
|
|||
|
||||
@DriftDatabase(tables: [Users])
|
||||
class Database extends _$Database {
|
||||
Database(QueryExecutor c) : super(c);
|
||||
Database(super.c);
|
||||
|
||||
@override
|
||||
int get schemaVersion => 1;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:drift_docs/snippets/dart_api/datetime_conversion.dart';
|
||||
import 'package:drift_docs/snippets/log_interceptor.dart';
|
||||
import 'package:drift_docs/snippets/modular/schema_inspection.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
|
@ -117,4 +118,34 @@ void main() {
|
|||
expect(row.name, 'bar');
|
||||
});
|
||||
});
|
||||
|
||||
test('interceptor', () {
|
||||
expect(
|
||||
() async {
|
||||
final db =
|
||||
Database(NativeDatabase.memory().interceptWith(LogInterceptor()));
|
||||
|
||||
await db.batch((batch) {
|
||||
batch.insert(db.users, UsersCompanion.insert(name: 'foo'));
|
||||
});
|
||||
|
||||
await db.users.all().get();
|
||||
},
|
||||
prints(
|
||||
allOf(
|
||||
stringContainsInOrder(
|
||||
[
|
||||
'begin',
|
||||
'Running batch with BatchedStatements',
|
||||
' => succeeded after ',
|
||||
'Running commit',
|
||||
' => succeeded after ',
|
||||
'Running SELECT * FROM "users"; with []',
|
||||
' => succeeded after'
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -35,7 +35,7 @@ class SnippetsBuilder extends CodeExcerptBuilder {
|
|||
}
|
||||
|
||||
class _DriftHighlighter extends Highlighter {
|
||||
_DriftHighlighter(SourceFile file) : super(file);
|
||||
_DriftHighlighter(super.file);
|
||||
|
||||
@override
|
||||
void highlight() {
|
||||
|
|
|
@ -1,3 +1,12 @@
|
|||
## 2.14.0-dev
|
||||
|
||||
- Add the `QueryInterceptor` to easily monitor all database calls made by drift.
|
||||
- Add the `count()` extension on tables to easily count rows in tables or views.
|
||||
|
||||
## 2.13.1
|
||||
|
||||
- Fix a bug when running batches over serialized communication channels.
|
||||
|
||||
## 2.13.0
|
||||
|
||||
- Add APIs to setup Wasm databases with custom drift workers.
|
||||
|
|
|
@ -15,22 +15,22 @@
|
|||
</p>
|
||||
|
||||
Drift is a reactive persistence library for Flutter and Dart, built on top of
|
||||
sqlite.
|
||||
SQLite.
|
||||
Drift is
|
||||
|
||||
- __Flexible__: Drift lets you write queries in both SQL and Dart,
|
||||
providing fluent apis for both languages. You can filter and order results
|
||||
or use joins to run queries on multiple tables. You can even use complex
|
||||
sql features like `WITH` and `WINDOW` clauses.
|
||||
SQL features like `WITH` and `WINDOW` clauses.
|
||||
- __🔥 Feature rich__: Drift has builtin support for transactions, schema
|
||||
migrations, complex filters and expressions, batched updates and joins. We
|
||||
even have a builtin IDE for SQL!
|
||||
- __📦 Modular__: Thanks to builtin support for daos and `import`s in sql files, drift helps you keep your database code simple.
|
||||
- __🛡️ Safe__: Drift generates typesafe code based on your tables and queries. If you make a mistake in your queries, drift will find it at compile time and
|
||||
- __📦 Modular__: Thanks to builtin support for daos and `import`s in SQL files, drift helps you keep your database code simple.
|
||||
- __🛡️ Safe__: Drift generates type-safe code based on your tables and queries. If you make a mistake in your queries, drift will find it at compile time and
|
||||
provide helpful and descriptive lints.
|
||||
- __⚡ Fast__: Even though drift lets you write powerful queries, it can keep
|
||||
up with the performance of key-value stores. Drift is the only major persistence library with builtin threading support, allowing you to run database code across isolates with zero additional effort.
|
||||
- __Reactive__: Turn any sql query into an auto-updating stream! This includes complex queries across many tables
|
||||
- __Reactive__: Turn any SQL query into an auto-updating stream! This includes complex queries across many tables
|
||||
- __⚙️ Cross-Platform support__: Drift works on Android, iOS, macOS, Windows, Linux and [the web](https://drift.simonbinder.eu/web). [This template](https://github.com/simolus3/drift/tree/develop/examples/app) is a Flutter todo app that works on all platforms
|
||||
- __🗡️ Battle tested and production ready__: Drift is stable and well tested with a wide range of unit and integration tests. It powers production Flutter apps.
|
||||
|
||||
|
|
|
@ -64,7 +64,7 @@ abstract class TodoItemWithCategoryNameView extends View {
|
|||
TodoItemWithCategoryNameView,
|
||||
])
|
||||
class Database extends _$Database {
|
||||
Database(QueryExecutor e) : super(e);
|
||||
Database(super.e);
|
||||
|
||||
@override
|
||||
int get schemaVersion => 2;
|
||||
|
|
|
@ -8,13 +8,14 @@ export 'dart:typed_data' show Uint8List;
|
|||
|
||||
export 'src/dsl/dsl.dart';
|
||||
export 'src/runtime/api/options.dart';
|
||||
export 'src/runtime/api/runtime_api.dart';
|
||||
export 'src/runtime/api/runtime_api.dart' hide RunWithEngine;
|
||||
export 'src/runtime/custom_result_set.dart';
|
||||
export 'src/runtime/data_class.dart';
|
||||
export 'src/runtime/data_verification.dart';
|
||||
export 'src/runtime/exceptions.dart';
|
||||
export 'src/runtime/executor/connection_pool.dart';
|
||||
export 'src/runtime/executor/executor.dart';
|
||||
export 'src/runtime/executor/interceptor.dart';
|
||||
export 'src/runtime/query_builder/query_builder.dart'
|
||||
hide CaseWhenExpressionWithBase, BaseCaseWhenExpression;
|
||||
export 'src/runtime/types/converters.dart';
|
||||
|
|
|
@ -223,9 +223,9 @@ class VersionedVirtualTable extends VersionedTable
|
|||
/// Create a virtual table by copying fields from [source] and applying a
|
||||
/// [alias] to columns.
|
||||
VersionedVirtualTable.aliased(
|
||||
{required VersionedVirtualTable source, required String? alias})
|
||||
{required VersionedVirtualTable super.source, required super.alias})
|
||||
: moduleAndArgs = source.moduleAndArgs,
|
||||
super.aliased(source: source, alias: alias);
|
||||
super.aliased();
|
||||
|
||||
@override
|
||||
VersionedVirtualTable createAlias(String alias) {
|
||||
|
|
|
@ -48,8 +48,8 @@ class NativeDatabase extends DelegatedDatabase {
|
|||
// when changing this, also update the documentation in `drift_vm_database_factory`.
|
||||
static const _cacheStatementsByDefault = false;
|
||||
|
||||
NativeDatabase._(DatabaseDelegate delegate, bool logStatements)
|
||||
: super(delegate, isSequential: false, logStatements: logStatements);
|
||||
NativeDatabase._(super.delegate, bool logStatements)
|
||||
: super(isSequential: false, logStatements: logStatements);
|
||||
|
||||
/// Creates a database that will store its result in the [file], creating it
|
||||
/// if it doesn't exist.
|
||||
|
@ -253,15 +253,12 @@ class _NativeDelegate extends Sqlite3Delegate<Database> {
|
|||
);
|
||||
|
||||
_NativeDelegate.opened(
|
||||
Database db,
|
||||
DatabaseSetup? setup,
|
||||
bool closeUnderlyingWhenClosed,
|
||||
Database super.db,
|
||||
super.setup,
|
||||
super.closeUnderlyingWhenClosed,
|
||||
bool cachePreparedStatements,
|
||||
) : file = null,
|
||||
super.opened(
|
||||
db,
|
||||
setup,
|
||||
closeUnderlyingWhenClosed,
|
||||
cachePreparedStatements: cachePreparedStatements,
|
||||
);
|
||||
|
||||
|
|
|
@ -395,7 +395,7 @@ class DriftView {
|
|||
/// By default, drift will attempt to use the view name followed by "Data"
|
||||
/// when naming data classes (e.g. a view named "UserView" will generate a
|
||||
/// data class called "UserViewData").
|
||||
/// {@macro drift_custom_data_class}
|
||||
/// {@endtemplate}
|
||||
final String? dataClassName;
|
||||
|
||||
/// The parent class of generated data class. Class must extends [DataClass]!
|
||||
|
|
|
@ -135,8 +135,7 @@ abstract class _BaseExecutor extends QueryExecutor {
|
|||
}
|
||||
|
||||
class _RemoteQueryExecutor extends _BaseExecutor {
|
||||
_RemoteQueryExecutor(DriftClient client, [int? executorId])
|
||||
: super(client, executorId);
|
||||
_RemoteQueryExecutor(super.client, [super.executorId]);
|
||||
|
||||
Completer<void>? _setSchemaVersion;
|
||||
Future<bool>? _serverIsOpen;
|
||||
|
@ -181,8 +180,7 @@ class _RemoteTransactionExecutor extends _BaseExecutor
|
|||
implements TransactionExecutor {
|
||||
final int? _outerExecutorId;
|
||||
|
||||
_RemoteTransactionExecutor(DriftClient client, this._outerExecutorId)
|
||||
: super(client);
|
||||
_RemoteTransactionExecutor(super.client, this._outerExecutorId);
|
||||
|
||||
Completer<bool>? _pendingOpen;
|
||||
bool _done = false;
|
||||
|
|
|
@ -191,7 +191,7 @@ class DriftProtocol {
|
|||
list[0] as int, list.skip(1).toList()));
|
||||
}
|
||||
|
||||
final executorId = fullMessage.last as int;
|
||||
final executorId = fullMessage.last as int?;
|
||||
return ExecuteBatchedStatement(
|
||||
BatchedStatements(sql, args), executorId);
|
||||
case _tag_RunTransactionAction:
|
||||
|
|
|
@ -606,3 +606,15 @@ extension on TransactionExecutor {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Exposes the private `_runConnectionZoned` method for other parts of drift.
|
||||
///
|
||||
/// This is only used by the DevTools extension.
|
||||
@internal
|
||||
extension RunWithEngine on DatabaseConnectionUser {
|
||||
/// Call the private [_runConnectionZoned] method.
|
||||
Future<T> runConnectionZoned<T>(
|
||||
DatabaseConnectionUser user, Future<T> Function() calculation) {
|
||||
return _runConnectionZoned(user, calculation);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -59,14 +59,13 @@ abstract class GeneratedDatabase extends DatabaseConnectionUser
|
|||
final Type _$dontSendThisOverIsolates = Null;
|
||||
|
||||
/// Used by generated code
|
||||
GeneratedDatabase(QueryExecutor executor, {StreamQueryStore? streamStore})
|
||||
: super(executor, streamQueries: streamStore) {
|
||||
GeneratedDatabase(super.executor, {StreamQueryStore? streamStore})
|
||||
: super(streamQueries: streamStore) {
|
||||
_whenConstructed();
|
||||
}
|
||||
|
||||
/// Used by generated code to connect to a database that is already open.
|
||||
GeneratedDatabase.connect(DatabaseConnection connection)
|
||||
: super.fromConnection(connection) {
|
||||
GeneratedDatabase.connect(super.connection) : super.fromConnection() {
|
||||
_whenConstructed();
|
||||
}
|
||||
|
||||
|
|
|
@ -2,9 +2,11 @@ import 'dart:async';
|
|||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift/src/remote/protocol.dart';
|
||||
import 'package:drift/src/runtime/executor/transactions.dart';
|
||||
|
||||
import '../query_builder/query_builder.dart';
|
||||
import '../api/runtime_api.dart';
|
||||
import 'devtools.dart';
|
||||
|
||||
/// A service extension making asynchronous requests on drift databases
|
||||
|
@ -26,7 +28,7 @@ class DriftServiceExtension {
|
|||
final stream = tracked.database.tableUpdates();
|
||||
final id = _subscriptionId++;
|
||||
|
||||
stream.listen((event) {
|
||||
_activeSubscriptions[id] = stream.listen((event) {
|
||||
postEvent('event', {
|
||||
'subscription': id,
|
||||
'payload':
|
||||
|
@ -60,6 +62,16 @@ class DriftServiceExtension {
|
|||
};
|
||||
|
||||
return _protocol.encodePayload(result);
|
||||
case 'collect-expected-schema':
|
||||
final executor = _CollectCreateStatements();
|
||||
await tracked.database.runConnectionZoned(
|
||||
BeforeOpenRunner(tracked.database, executor), () async {
|
||||
// ignore: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member
|
||||
final migrator = tracked.database.createMigrator();
|
||||
await migrator.createAll();
|
||||
});
|
||||
|
||||
return executor.statements;
|
||||
default:
|
||||
throw UnsupportedError('Method $action');
|
||||
}
|
||||
|
@ -96,3 +108,52 @@ class DriftServiceExtension {
|
|||
|
||||
static const _protocol = DriftProtocol();
|
||||
}
|
||||
|
||||
class _CollectCreateStatements extends QueryExecutor {
|
||||
final List<String> statements = [];
|
||||
|
||||
@override
|
||||
TransactionExecutor beginTransaction() {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
SqlDialect get dialect => SqlDialect.sqlite;
|
||||
|
||||
@override
|
||||
Future<bool> ensureOpen(QueryExecutorUser user) {
|
||||
return Future.value(true);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> runBatched(BatchedStatements statements) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> runCustom(String statement, [List<Object?>? args]) {
|
||||
statements.add(statement);
|
||||
return Future.value();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> runDelete(String statement, List<Object?> args) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> runInsert(String statement, List<Object?> args) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Map<String, Object?>>> runSelect(
|
||||
String statement, List<Object?> args) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> runUpdate(String statement, List<Object?> args) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -93,6 +93,7 @@ class EntityDescription {
|
|||
return EntityDescription(
|
||||
name: entity.entityName,
|
||||
type: switch (entity) {
|
||||
VirtualTableInfo() => 'virtual_table',
|
||||
TableInfo() => 'table',
|
||||
ViewInfo() => 'view',
|
||||
Index() => 'index',
|
||||
|
|
|
@ -1,77 +0,0 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'shared.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
TypeDescription _$TypeDescriptionFromJson(Map<String, dynamic> json) =>
|
||||
TypeDescription(
|
||||
type: $enumDecodeNullable(_$DriftSqlTypeEnumMap, json['type']),
|
||||
customTypeName: json['customTypeName'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$TypeDescriptionToJson(TypeDescription instance) =>
|
||||
<String, dynamic>{
|
||||
'type': _$DriftSqlTypeEnumMap[instance.type],
|
||||
'customTypeName': instance.customTypeName,
|
||||
};
|
||||
|
||||
const _$DriftSqlTypeEnumMap = {
|
||||
DriftSqlType.bool: 'bool',
|
||||
DriftSqlType.string: 'string',
|
||||
DriftSqlType.bigInt: 'bigInt',
|
||||
DriftSqlType.int: 'int',
|
||||
DriftSqlType.dateTime: 'dateTime',
|
||||
DriftSqlType.blob: 'blob',
|
||||
DriftSqlType.double: 'double',
|
||||
DriftSqlType.any: 'any',
|
||||
};
|
||||
|
||||
ColumnDescription _$ColumnDescriptionFromJson(Map<String, dynamic> json) =>
|
||||
ColumnDescription(
|
||||
name: json['name'] as String,
|
||||
type: json['type'] == null
|
||||
? null
|
||||
: TypeDescription.fromJson(json['type'] as Map<String, dynamic>),
|
||||
isNullable: json['isNullable'] as bool,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ColumnDescriptionToJson(ColumnDescription instance) =>
|
||||
<String, dynamic>{
|
||||
'name': instance.name,
|
||||
'type': instance.type,
|
||||
'isNullable': instance.isNullable,
|
||||
};
|
||||
|
||||
EntityDescription _$EntityDescriptionFromJson(Map<String, dynamic> json) =>
|
||||
EntityDescription(
|
||||
name: json['name'] as String,
|
||||
type: json['type'] as String,
|
||||
columns: (json['columns'] as List<dynamic>?)
|
||||
?.map((e) => ColumnDescription.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$EntityDescriptionToJson(EntityDescription instance) =>
|
||||
<String, dynamic>{
|
||||
'name': instance.name,
|
||||
'type': instance.type,
|
||||
'columns': instance.columns,
|
||||
};
|
||||
|
||||
DatabaseDescription _$DatabaseDescriptionFromJson(Map<String, dynamic> json) =>
|
||||
DatabaseDescription(
|
||||
dateTimeAsText: json['dateTimeAsText'] as bool,
|
||||
entities: (json['entities'] as List<dynamic>)
|
||||
.map((e) => EntityDescription.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$DatabaseDescriptionToJson(
|
||||
DatabaseDescription instance) =>
|
||||
<String, dynamic>{
|
||||
'dateTimeAsText': instance.dateTimeAsText,
|
||||
'entities': instance.entities,
|
||||
};
|
|
@ -0,0 +1,178 @@
|
|||
import '../api/runtime_api.dart';
|
||||
import '../query_builder/query_builder.dart';
|
||||
import 'executor.dart';
|
||||
|
||||
/// Extension to wrap a [QueryExecutor] with a [QueryInterceptor].
|
||||
extension ApplyInterceptor on QueryExecutor {
|
||||
/// Returns a [QueryExecutor] that will use `this` executor internally, but
|
||||
/// with calls intercepted by the given [interceptor].
|
||||
///
|
||||
/// This can be used to, for instance, write a custom statement logger or to
|
||||
/// retry failing statements automatically.
|
||||
QueryExecutor interceptWith(QueryInterceptor interceptor) {
|
||||
final $this = this;
|
||||
|
||||
if ($this is TransactionExecutor) {
|
||||
return _InterceptedTransactionExecutor($this, interceptor);
|
||||
} else {
|
||||
return _InterceptedExecutor($this, interceptor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Extension to wrap a [DatabaseConnection] with a [QueryInterceptor].
|
||||
extension ApplyInterceptorConnection on DatabaseConnection {
|
||||
/// Returns a [DatabaseConnection] that will use the same stream queries as
|
||||
/// `this`, but replaces its executor by wrapping it with the [interceptor].
|
||||
///
|
||||
/// See also: [ApplyInterceptor.interceptWith].
|
||||
DatabaseConnection interceptWith(QueryInterceptor interceptor) {
|
||||
return withExecutor(executor.interceptWith(interceptor));
|
||||
}
|
||||
}
|
||||
|
||||
/// An interceptor for SQL queries.
|
||||
///
|
||||
/// This wraps an existing [QueryExecutor] implemented by drift, and by default
|
||||
/// does nothing. However, specific methods can be overridden to customize the
|
||||
/// behavior of an existing query executor.
|
||||
abstract class QueryInterceptor {
|
||||
/// Intercept [QueryExecutor.dialect] calls.
|
||||
SqlDialect dialect(QueryExecutor executor) => executor.dialect;
|
||||
|
||||
/// Intercept [QueryExecutor.beginTransaction] calls.
|
||||
TransactionExecutor beginTransaction(QueryExecutor parent) =>
|
||||
parent.beginTransaction();
|
||||
|
||||
/// Intercept [TransactionExecutor.supportsNestedTransactions] calls.
|
||||
bool transactionCanBeNested(TransactionExecutor inner) {
|
||||
return inner.supportsNestedTransactions;
|
||||
}
|
||||
|
||||
/// Intercept [QueryExecutor.close] calls.
|
||||
Future<void> close(QueryExecutor inner) => inner.close();
|
||||
|
||||
/// Intercept [TransactionExecutor.send] calls.
|
||||
Future<void> commitTransaction(TransactionExecutor inner) {
|
||||
return inner.send();
|
||||
}
|
||||
|
||||
/// Intercept [TransactionExecutor.rollback] calls.
|
||||
Future<void> rollbackTransaction(TransactionExecutor inner) {
|
||||
return inner.rollback();
|
||||
}
|
||||
|
||||
/// Intercept [QueryExecutor.ensureOpen] calls.
|
||||
Future<bool> ensureOpen(QueryExecutor executor, QueryExecutorUser user) =>
|
||||
executor.ensureOpen(user);
|
||||
|
||||
/// Intercept [QueryExecutor.runBatched] calls.
|
||||
Future<void> runBatched(
|
||||
QueryExecutor executor, BatchedStatements statements) {
|
||||
return executor.runBatched(statements);
|
||||
}
|
||||
|
||||
/// Intercept [QueryExecutor.runCustom] calls.
|
||||
Future<void> runCustom(
|
||||
QueryExecutor executor, String statement, List<Object?> args) {
|
||||
return executor.runCustom(statement, args);
|
||||
}
|
||||
|
||||
/// Intercept [QueryExecutor.runInsert] calls.
|
||||
Future<int> runInsert(
|
||||
QueryExecutor executor, String statement, List<Object?> args) {
|
||||
return executor.runInsert(statement, args);
|
||||
}
|
||||
|
||||
/// Intercept [QueryExecutor.runDelete] calls.
|
||||
Future<int> runDelete(
|
||||
QueryExecutor executor, String statement, List<Object?> args) {
|
||||
return executor.runDelete(statement, args);
|
||||
}
|
||||
|
||||
/// Intercept [QueryExecutor.runUpdate] calls.
|
||||
Future<int> runUpdate(
|
||||
QueryExecutor executor, String statement, List<Object?> args) {
|
||||
return executor.runUpdate(statement, args);
|
||||
}
|
||||
|
||||
/// Intercept [QueryExecutor.runSelect] calls.
|
||||
Future<List<Map<String, Object?>>> runSelect(
|
||||
QueryExecutor executor, String statement, List<Object?> args) {
|
||||
return executor.runSelect(statement, args);
|
||||
}
|
||||
}
|
||||
|
||||
class _InterceptedExecutor extends QueryExecutor {
|
||||
final QueryExecutor _inner;
|
||||
final QueryInterceptor _interceptor;
|
||||
|
||||
_InterceptedExecutor(this._inner, this._interceptor);
|
||||
|
||||
@override
|
||||
TransactionExecutor beginTransaction() => _InterceptedTransactionExecutor(
|
||||
_interceptor.beginTransaction(_inner), _interceptor);
|
||||
|
||||
@override
|
||||
SqlDialect get dialect => _interceptor.dialect(_inner);
|
||||
|
||||
@override
|
||||
Future<bool> ensureOpen(QueryExecutorUser user) {
|
||||
return _interceptor.ensureOpen(_inner, user);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> runBatched(BatchedStatements statements) {
|
||||
return _interceptor.runBatched(_inner, statements);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> runCustom(String statement, [List<Object?>? args]) {
|
||||
return _interceptor.runCustom(_inner, statement, args ?? const []);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> runDelete(String statement, List<Object?> args) {
|
||||
return _interceptor.runDelete(_inner, statement, args);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> runInsert(String statement, List<Object?> args) {
|
||||
return _interceptor.runInsert(_inner, statement, args);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Map<String, Object?>>> runSelect(
|
||||
String statement, List<Object?> args) {
|
||||
return _interceptor.runSelect(_inner, statement, args);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> runUpdate(String statement, List<Object?> args) {
|
||||
return _interceptor.runUpdate(_inner, statement, args);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
return _interceptor.close(_inner);
|
||||
}
|
||||
}
|
||||
|
||||
class _InterceptedTransactionExecutor extends _InterceptedExecutor
|
||||
implements TransactionExecutor {
|
||||
_InterceptedTransactionExecutor(super.inner, super.interceptor);
|
||||
|
||||
@override
|
||||
Future<void> rollback() {
|
||||
return _interceptor.rollbackTransaction(_inner as TransactionExecutor);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> send() {
|
||||
return _interceptor.commitTransaction(_inner as TransactionExecutor);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get supportsNestedTransactions =>
|
||||
_interceptor.transactionCanBeNested(_inner as TransactionExecutor);
|
||||
}
|
|
@ -71,7 +71,7 @@ extension DateTimeExpressions on Expression<DateTime> {
|
|||
/// ```dart
|
||||
/// Variable(DateTime.now()).modify(DateTimeModifier.localTime()).hour
|
||||
/// ```
|
||||
/// {@template}
|
||||
/// {@endtemplate}
|
||||
Expression<int> get year => _StrftimeSingleFieldExpression('%Y', this);
|
||||
|
||||
/// Extracts the month from `this` datetime expression.
|
||||
|
|
|
@ -12,6 +12,23 @@ extension TableOrViewStatements<Tbl extends HasResultSet, Row>
|
|||
return select();
|
||||
}
|
||||
|
||||
/// Counts the rows in this table.
|
||||
///
|
||||
/// The optional [where] clause can be used to only count rows matching the
|
||||
/// condition, similar to [SimpleSelectStatement.where].
|
||||
///
|
||||
/// The returned [Selectable] can be run once with [Selectable.getSingle] to
|
||||
/// get the count once, or be watched as a stream with [Selectable.watchSingle].
|
||||
Selectable<int> count({Expression<bool> Function(Tbl row)? where}) {
|
||||
final count = countAll();
|
||||
final stmt = selectOnly()..addColumns([count]);
|
||||
if (where != null) {
|
||||
stmt.where(where(asDslTable));
|
||||
}
|
||||
|
||||
return stmt.map((row) => row.read(count)!);
|
||||
}
|
||||
|
||||
/// Composes a `SELECT` statement on the captured table or view.
|
||||
///
|
||||
/// This is equivalent to calling [DatabaseConnectionUser.select].
|
||||
|
|
|
@ -5,8 +5,7 @@ class DeleteStatement<T extends Table, D> extends Query<T, D>
|
|||
with SingleTableQueryMixin<T, D> {
|
||||
/// This constructor should be called by [DatabaseConnectionUser.delete] for
|
||||
/// you.
|
||||
DeleteStatement(DatabaseConnectionUser database, TableInfo<T, D> table)
|
||||
: super(database, table);
|
||||
DeleteStatement(super.database, TableInfo<T, D> super.table);
|
||||
|
||||
@override
|
||||
void writeStartPart(GenerationContext ctx) {
|
||||
|
|
|
@ -30,10 +30,7 @@ class SimpleSelectStatement<T extends HasResultSet, D> extends Query<T, D>
|
|||
|
||||
/// Used internally by drift, users will want to call
|
||||
/// [DatabaseConnectionUser.select] instead.
|
||||
SimpleSelectStatement(
|
||||
DatabaseConnectionUser database, ResultSetImplementation<T, D> table,
|
||||
{this.distinct = false})
|
||||
: super(database, table);
|
||||
SimpleSelectStatement(super.database, super.table, {this.distinct = false});
|
||||
|
||||
/// The tables this select statement reads from.
|
||||
@visibleForOverriding
|
||||
|
|
|
@ -10,12 +10,10 @@ class JoinedSelectStatement<FirstT extends HasResultSet, FirstD>
|
|||
implements BaseSelectStatement {
|
||||
/// Used internally by drift, users should use [SimpleSelectStatement.join]
|
||||
/// instead.
|
||||
JoinedSelectStatement(DatabaseConnectionUser database,
|
||||
ResultSetImplementation<FirstT, FirstD> table, this._joins,
|
||||
JoinedSelectStatement(super.database, super.table, this._joins,
|
||||
[this.distinct = false,
|
||||
this._includeMainTableInResult = true,
|
||||
this._includeJoinedTablesInResult = true])
|
||||
: super(database, table);
|
||||
this._includeJoinedTablesInResult = true]);
|
||||
|
||||
/// Whether to generate a `SELECT DISTINCT` query that will remove duplicate
|
||||
/// rows from the result set.
|
||||
|
|
|
@ -4,8 +4,7 @@ part of '../query_builder.dart';
|
|||
class UpdateStatement<T extends Table, D> extends Query<T, D>
|
||||
with SingleTableQueryMixin<T, D> {
|
||||
/// Used internally by drift, construct an update statement
|
||||
UpdateStatement(DatabaseConnectionUser database, TableInfo<T, D> table)
|
||||
: super(database, table);
|
||||
UpdateStatement(super.database, TableInfo<T, D> super.table);
|
||||
|
||||
late Map<String, Expression> _updatedFields;
|
||||
|
||||
|
|
|
@ -155,7 +155,7 @@ final class SqlTypes {
|
|||
// thing.
|
||||
result = DateTime.parse(rawValue);
|
||||
} else {
|
||||
// Result from complex date tmie transformation. Interpret as UTC,
|
||||
// Result from complex date time transformation. Interpret as UTC,
|
||||
// which is what sqlite3 does by default.
|
||||
result = DateTime.parse('${rawValue}Z');
|
||||
}
|
||||
|
|
|
@ -31,8 +31,8 @@ export 'src/web/wasm_setup/types.dart';
|
|||
/// how to obtain this file. A [working example](https://github.com/simolus3/drift/blob/04539882330d80519128fec1ceb120fb1623a831/examples/app/lib/database/connection/web.dart#L27-L36)
|
||||
/// is also available in the drift repository.
|
||||
class WasmDatabase extends DelegatedDatabase {
|
||||
WasmDatabase._(DatabaseDelegate delegate, bool logStatements)
|
||||
: super(delegate, isSequential: true, logStatements: logStatements);
|
||||
WasmDatabase._(super.delegate, bool logStatements)
|
||||
: super(isSequential: true, logStatements: logStatements);
|
||||
|
||||
/// Creates a wasm database at [path] in the virtual file system of the
|
||||
/// [sqlite3] module.
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
name: drift
|
||||
description: Drift is a reactive library to store relational data in Dart and Flutter applications.
|
||||
version: 2.13.0
|
||||
version: 2.14.0-dev
|
||||
repository: https://github.com/simolus3/drift
|
||||
homepage: https://drift.simonbinder.eu/
|
||||
issue_tracker: https://github.com/simolus3/drift/issues
|
||||
|
@ -28,7 +28,7 @@ dev_dependencies:
|
|||
drift_testcases:
|
||||
path: ../extras/integration_tests/drift_testcases
|
||||
http: ^0.13.4
|
||||
lints: ^2.0.0
|
||||
lints: ^3.0.0
|
||||
uuid: ^4.0.0
|
||||
build_runner: ^2.0.0
|
||||
test: ^1.17.0
|
||||
|
@ -37,3 +37,4 @@ dev_dependencies:
|
|||
shelf: ^1.3.0
|
||||
stack_trace: ^1.10.0
|
||||
test_descriptor: ^2.0.1
|
||||
vm_service: ^13.0.0
|
||||
|
|
|
@ -6,7 +6,7 @@ import '../generated/todos.dart';
|
|||
import '../test_utils/test_utils.dart';
|
||||
|
||||
class _FakeDb extends GeneratedDatabase {
|
||||
_FakeDb(QueryExecutor executor) : super(executor);
|
||||
_FakeDb(super.executor);
|
||||
|
||||
@override
|
||||
MigrationStrategy get migration {
|
||||
|
|
|
@ -321,7 +321,7 @@ final class _FakeSchemaVersion extends VersionedSchema {
|
|||
}
|
||||
|
||||
class _DefaultDb extends GeneratedDatabase {
|
||||
_DefaultDb(QueryExecutor executor) : super(executor);
|
||||
_DefaultDb(super.executor);
|
||||
|
||||
@override
|
||||
List<TableInfo<Table, DataClass>> get allTables => [];
|
||||
|
|
|
@ -256,4 +256,33 @@ void main() {
|
|||
[r'$.foo'],
|
||||
));
|
||||
});
|
||||
|
||||
group('count', () {
|
||||
test('all', () async {
|
||||
when(executor.runSelect(any, any)).thenAnswer((_) async => [
|
||||
{'c0': 3}
|
||||
]);
|
||||
|
||||
final result = await db.todosTable.count().getSingle();
|
||||
expect(result, 3);
|
||||
|
||||
verify(executor.runSelect(
|
||||
'SELECT COUNT(*) AS "c0" FROM "todos";', argThat(isEmpty)));
|
||||
});
|
||||
|
||||
test('with filter', () async {
|
||||
when(executor.runSelect(any, any)).thenAnswer((_) async => [
|
||||
{'c0': 2}
|
||||
]);
|
||||
|
||||
final result = await db.todosTable
|
||||
.count(where: (row) => row.id.isBiggerThanValue(12))
|
||||
.getSingle();
|
||||
expect(result, 2);
|
||||
|
||||
verify(executor.runSelect(
|
||||
'SELECT COUNT(*) AS "c0" FROM "todos" WHERE "todos"."id" > ?;',
|
||||
[12]));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,96 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import '../generated/todos.dart';
|
||||
import '../test_utils/test_utils.dart';
|
||||
|
||||
void main() {
|
||||
test('calls interceptor methods', () async {
|
||||
final interceptor = EmittingInterceptor();
|
||||
final events = <String>[];
|
||||
interceptor.events.stream.listen(events.add);
|
||||
|
||||
final database = TodoDb(testInMemoryDatabase().interceptWith(interceptor));
|
||||
expect(await database.categories.select().get(), isEmpty);
|
||||
expect(events, ['select']);
|
||||
|
||||
await database.batch((batch) {
|
||||
batch.insert(database.categories,
|
||||
CategoriesCompanion.insert(description: 'from batch'));
|
||||
});
|
||||
expect(events, ['select', 'begin', 'batched', 'commit']);
|
||||
events.clear();
|
||||
|
||||
await database.users.insertOne(
|
||||
UsersCompanion.insert(name: 'Simon B', profilePicture: Uint8List(0)));
|
||||
await database.users.update().write(UsersCompanion(isAwesome: Value(true)));
|
||||
await database.users.delete().go();
|
||||
expect(events, ['insert', 'update', 'delete']);
|
||||
});
|
||||
}
|
||||
|
||||
class EmittingInterceptor extends QueryInterceptor {
|
||||
final events = StreamController<String>();
|
||||
|
||||
@override
|
||||
TransactionExecutor beginTransaction(QueryExecutor parent) {
|
||||
events.add('begin');
|
||||
return super.beginTransaction(parent);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> commitTransaction(TransactionExecutor inner) {
|
||||
events.add('commit');
|
||||
return super.commitTransaction(inner);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> rollbackTransaction(TransactionExecutor inner) {
|
||||
events.add('rollback');
|
||||
return super.rollbackTransaction(inner);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> runBatched(
|
||||
QueryExecutor executor, BatchedStatements statements) {
|
||||
events.add('batched');
|
||||
return super.runBatched(executor, statements);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> runCustom(
|
||||
QueryExecutor executor, String statement, List<Object?> args) {
|
||||
events.add('custom');
|
||||
return super.runCustom(executor, statement, args);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> runInsert(
|
||||
QueryExecutor executor, String statement, List<Object?> args) {
|
||||
events.add('insert');
|
||||
return super.runInsert(executor, statement, args);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> runDelete(
|
||||
QueryExecutor executor, String statement, List<Object?> args) {
|
||||
events.add('delete');
|
||||
return super.runDelete(executor, statement, args);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> runUpdate(
|
||||
QueryExecutor executor, String statement, List<Object?> args) {
|
||||
events.add('update');
|
||||
return super.runUpdate(executor, statement, args);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Map<String, Object?>>> runSelect(
|
||||
QueryExecutor executor, String statement, List<Object?> args) {
|
||||
events.add('select');
|
||||
return super.runSelect(executor, statement, args);
|
||||
}
|
||||
}
|
|
@ -1,5 +1,32 @@
|
|||
import 'package:drift/drift.dart';
|
||||
|
||||
class CustomTextType implements CustomSqlType<String> {
|
||||
const CustomTextType();
|
||||
|
||||
@override
|
||||
String mapToSqlLiteral(String dartValue) {
|
||||
final escapedChars = dartValue.replaceAll('\'', '\'\'');
|
||||
return "'$escapedChars'";
|
||||
}
|
||||
|
||||
@override
|
||||
Object mapToSqlParameter(String dartValue) {
|
||||
return dartValue;
|
||||
}
|
||||
|
||||
@override
|
||||
String read(Object fromSql) {
|
||||
return fromSql.toString();
|
||||
}
|
||||
|
||||
@override
|
||||
String sqlTypeName(GenerationContext context) {
|
||||
// Still has text column affinity, but can be used to verify that the type
|
||||
// really is used.
|
||||
return 'MY_TEXT';
|
||||
}
|
||||
}
|
||||
|
||||
enum SyncType {
|
||||
locallyCreated,
|
||||
locallyUpdated,
|
||||
|
|
|
@ -15,7 +15,7 @@ part 'custom_tables.g.dart';
|
|||
},
|
||||
)
|
||||
class CustomTablesDb extends _$CustomTablesDb {
|
||||
CustomTablesDb(QueryExecutor e) : super(e) {
|
||||
CustomTablesDb(super.e) {
|
||||
driftRuntimeOptions.dontWarnAboutMultipleDatabases = true;
|
||||
}
|
||||
|
||||
|
|
|
@ -106,7 +106,7 @@ class WithDefaults extends Table with TableInfo<WithDefaults, WithDefault> {
|
|||
static const VerificationMeta _aMeta = const VerificationMeta('a');
|
||||
late final GeneratedColumn<String> a = GeneratedColumn<String>(
|
||||
'a', aliasedName, true,
|
||||
type: DriftSqlType.string,
|
||||
type: const CustomTextType(),
|
||||
requiredDuringInsert: false,
|
||||
$customConstraints: 'DEFAULT \'something\'',
|
||||
defaultValue: const CustomExpression('\'something\''));
|
||||
|
@ -144,7 +144,7 @@ class WithDefaults extends Table with TableInfo<WithDefaults, WithDefault> {
|
|||
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
|
||||
return WithDefault(
|
||||
a: attachedDatabase.typeMapping
|
||||
.read(DriftSqlType.string, data['${effectivePrefix}a']),
|
||||
.read(const CustomTextType(), data['${effectivePrefix}a']),
|
||||
b: attachedDatabase.typeMapping
|
||||
.read(DriftSqlType.int, data['${effectivePrefix}b']),
|
||||
);
|
||||
|
@ -267,7 +267,7 @@ class WithDefaultsCompanion extends UpdateCompanion<WithDefault> {
|
|||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, Expression>{};
|
||||
if (a.present) {
|
||||
map['a'] = Variable<String>(a.value);
|
||||
map['a'] = Variable<String>(a.value, const CustomTextType());
|
||||
}
|
||||
if (b.present) {
|
||||
map['b'] = Variable<int>(b.value);
|
||||
|
@ -1801,7 +1801,7 @@ abstract class _$CustomTablesDb extends GeneratedDatabase {
|
|||
...generatedpredicate.watchedTables,
|
||||
}).asyncMap((QueryRow row) async => MultipleResult(
|
||||
row: row,
|
||||
a: row.readNullable<String>('a'),
|
||||
a: row.readNullableWithType<String>(const CustomTextType(), 'a'),
|
||||
b: row.readNullable<int>('b'),
|
||||
c: await withConstraints.mapFromRowOrNull(row,
|
||||
tablePrefix: 'nested_0'),
|
||||
|
|
|
@ -6,7 +6,7 @@ CREATE TABLE no_ids (
|
|||
) WITHOUT ROWID WITH NoIdRow;
|
||||
|
||||
CREATE TABLE with_defaults (
|
||||
a TEXT JSON KEY customJsonName DEFAULT 'something',
|
||||
a `const CustomTextType()` JSON KEY customJsonName DEFAULT 'something',
|
||||
b INT UNIQUE
|
||||
);
|
||||
|
||||
|
|
|
@ -257,7 +257,7 @@ class TodoDb extends _$TodoDb {
|
|||
},
|
||||
)
|
||||
class SomeDao extends DatabaseAccessor<TodoDb> with _$SomeDaoMixin {
|
||||
SomeDao(TodoDb db) : super(db);
|
||||
SomeDao(super.db);
|
||||
}
|
||||
|
||||
QueryExecutor get _nullExecutor =>
|
||||
|
|
|
@ -35,7 +35,7 @@ DatabaseConnection createConnection() {
|
|||
}
|
||||
|
||||
class EmptyDb extends GeneratedDatabase {
|
||||
EmptyDb(DatabaseConnection c) : super(c);
|
||||
EmptyDb(DatabaseConnection super.c);
|
||||
@override
|
||||
final List<TableInfo> allTables = const [];
|
||||
@override
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
import 'package:drift/native.dart';
|
||||
|
||||
import '../../generated/todos.dart';
|
||||
|
||||
void main() {
|
||||
TodoDb(NativeDatabase.memory());
|
||||
print('database created');
|
||||
|
||||
// Keep the process alive
|
||||
Stream<void>.periodic(const Duration(seconds: 10)).listen(null);
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:test/test.dart';
|
||||
import 'package:vm_service/vm_service.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:vm_service/vm_service_io.dart';
|
||||
|
||||
void main() {
|
||||
late Process child;
|
||||
late VmService vm;
|
||||
late String isolateId;
|
||||
|
||||
setUpAll(() async {
|
||||
final socket = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0);
|
||||
final port = socket.port;
|
||||
await socket.close();
|
||||
|
||||
String sdk = p.dirname(p.dirname(Platform.resolvedExecutable));
|
||||
child = await Process.start(p.join(sdk, 'bin', 'dart'), [
|
||||
'run',
|
||||
'--enable-vm-service=$port',
|
||||
'--disable-service-auth-codes',
|
||||
'--enable-asserts',
|
||||
'test/integration_tests/devtools/app.dart',
|
||||
]);
|
||||
|
||||
final vmServiceListening = Completer<void>();
|
||||
final databaseOpened = Completer<void>();
|
||||
|
||||
child.stdout
|
||||
.map(utf8.decode)
|
||||
.transform(const LineSplitter())
|
||||
.listen((line) {
|
||||
if (line.startsWith('The Dart VM service is listening')) {
|
||||
vmServiceListening.complete();
|
||||
} else if (line == 'database created') {
|
||||
databaseOpened.complete();
|
||||
} else if (!line.startsWith('The Dart DevTools')) {
|
||||
print('[child]: $line');
|
||||
}
|
||||
});
|
||||
|
||||
await vmServiceListening.future;
|
||||
vm = await vmServiceConnectUri('ws://localhost:$port/ws');
|
||||
|
||||
final state = await vm.getVM();
|
||||
isolateId = state.isolates!.single.id!;
|
||||
|
||||
await databaseOpened.future;
|
||||
});
|
||||
|
||||
tearDownAll(() async {
|
||||
child.kill();
|
||||
});
|
||||
|
||||
test('can list create statements', () async {
|
||||
final response = await vm.callServiceExtension(
|
||||
'ext.drift.database',
|
||||
args: {'action': 'collect-expected-schema', 'db': '0'},
|
||||
isolateId: isolateId,
|
||||
);
|
||||
|
||||
expect(
|
||||
response.json!['r'],
|
||||
containsAll([
|
||||
'CREATE TABLE IF NOT EXISTS "categories" ("id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "desc" TEXT NOT NULL UNIQUE, "priority" INTEGER NOT NULL DEFAULT 0, "description_in_upper_case" TEXT NOT NULL GENERATED ALWAYS AS (UPPER("desc")) VIRTUAL);',
|
||||
'CREATE TABLE IF NOT EXISTS "todos" ("id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "title" TEXT NULL, "content" TEXT NOT NULL, "target_date" INTEGER NULL UNIQUE, "category" INTEGER NULL REFERENCES categories (id), "status" TEXT NULL, UNIQUE ("title", "category"), UNIQUE ("title", "target_date"));',
|
||||
'CREATE TABLE IF NOT EXISTS "shared_todos" ("todo" INTEGER NOT NULL, "user" INTEGER NOT NULL, PRIMARY KEY ("todo", "user"), FOREIGN KEY (todo) REFERENCES todos(id), FOREIGN KEY (user) REFERENCES users(id));'
|
||||
]));
|
||||
});
|
||||
}
|
|
@ -11,7 +11,7 @@ const _createNoIds =
|
|||
'WITHOUT ROWID;';
|
||||
|
||||
const _createWithDefaults = 'CREATE TABLE IF NOT EXISTS "with_defaults" ('
|
||||
"\"a\" TEXT DEFAULT 'something', \"b\" INTEGER UNIQUE);";
|
||||
"\"a\" MY_TEXT DEFAULT 'something', \"b\" INTEGER UNIQUE);";
|
||||
|
||||
const _createWithConstraints = 'CREATE TABLE IF NOT EXISTS "with_constraints" ('
|
||||
'"a" TEXT, "b" INTEGER NOT NULL, "c" REAL, '
|
||||
|
|
|
@ -495,8 +495,7 @@ void main() {
|
|||
}
|
||||
|
||||
class _TestDatabase extends GeneratedDatabase {
|
||||
_TestDatabase(QueryExecutor executor, this.schemaVersion, this.migration)
|
||||
: super(executor);
|
||||
_TestDatabase(super.executor, this.schemaVersion, this.migration);
|
||||
|
||||
@override
|
||||
Iterable<TableInfo<Table, dynamic>> get allTables => const Iterable.empty();
|
||||
|
|
|
@ -61,7 +61,7 @@ CREATE TABLE IF NOT EXISTS todo_categories (
|
|||
}
|
||||
|
||||
class _Database extends GeneratedDatabase {
|
||||
_Database(QueryExecutor executor) : super(executor);
|
||||
_Database(super.executor);
|
||||
|
||||
@override
|
||||
Iterable<TableInfo<Table, dynamic>> get allTables => const Iterable.empty();
|
||||
|
|
|
@ -149,6 +149,15 @@ void main() {
|
|||
(e) => e.remoteCause, 'remoteCause', 'UnimplementedError: error!')),
|
||||
);
|
||||
|
||||
final statements =
|
||||
BatchedStatements(['SELECT 1'], [ArgumentsForBatchedStatement(0, [])]);
|
||||
when(executor.runBatched(any)).thenAnswer((i) => Future.value());
|
||||
// Not using db.batch because that starts a transaction, we want to test
|
||||
// this working with the default executor.
|
||||
// Regression test for: https://github.com/simolus3/drift/pull/2707
|
||||
await db.executor.runBatched(statements);
|
||||
verify(executor.runBatched(statements));
|
||||
|
||||
await db.close();
|
||||
});
|
||||
|
||||
|
|
|
@ -1,38 +0,0 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:isolate';
|
||||
|
||||
import 'package:drift_dev/integrations/plugin.dart' as plugin;
|
||||
import 'package:web_socket_channel/io.dart';
|
||||
|
||||
const useDebuggingVariant = false;
|
||||
|
||||
void main(List<String> args, SendPort sendPort) {
|
||||
if (useDebuggingVariant) {
|
||||
_PluginProxy(sendPort).start();
|
||||
} else {
|
||||
plugin.start(args, sendPort);
|
||||
}
|
||||
}
|
||||
|
||||
// Used during development. See CONTRIBUTING.md in the drift repo on how to
|
||||
// debug the plugin.
|
||||
class _PluginProxy {
|
||||
final SendPort sendToAnalysisServer;
|
||||
|
||||
_PluginProxy(this.sendToAnalysisServer);
|
||||
|
||||
Future<void> start() async {
|
||||
final channel = IOWebSocketChannel.connect('ws://localhost:9999');
|
||||
final receive = ReceivePort();
|
||||
sendToAnalysisServer.send(receive.sendPort);
|
||||
|
||||
receive.listen((data) {
|
||||
// the server will send messages as maps, convert to json
|
||||
channel.sink.add(json.encode(data));
|
||||
});
|
||||
|
||||
channel.stream.listen((data) {
|
||||
sendToAnalysisServer.send(json.decode(data as String));
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
name: analyzer_load_drift_plugin
|
||||
version: 1.0.0
|
||||
description: This pubspec is a part of Drift and determines the version of the analyzer plugin to load
|
||||
|
||||
environment:
|
||||
sdk: '>=2.17.0 <3.0.0'
|
||||
|
||||
dependencies:
|
||||
drift_dev: ^2.0.0
|
||||
web_socket_channel: ^2.2.0
|
||||
|
||||
# These overrides are only needed when working on the plugin with useDebuggingVariant = false (not recommended)
|
||||
|
||||
#dependency_overrides:
|
||||
# drift_dev:
|
||||
# path: /path/to/drift/drift_dev
|
||||
# sqlparser:
|
||||
# path: /path/to/drift/sqlparser
|
|
@ -1,3 +1,15 @@
|
|||
## 2.14.0-dev
|
||||
|
||||
## 2.13.2
|
||||
|
||||
- Fix generated queries relying on custom types.
|
||||
|
||||
## 2.13.1
|
||||
|
||||
- Add `has_separate_analyzer` option to optimize builds using the `not_shared` builder.
|
||||
- Avoid illegal references to implicitly nullable variant of type converter when no such
|
||||
field exists.
|
||||
|
||||
## 2.13.0
|
||||
|
||||
- Fix indices not being created for Dart tables from different files.
|
||||
|
|
|
@ -62,6 +62,7 @@ builders:
|
|||
auto_apply: none
|
||||
required_inputs: [".drift_prep.json"]
|
||||
applies_builders: [":preparing_builder"]
|
||||
runs_before: [":not_shared"]
|
||||
|
||||
post_process_builders:
|
||||
cleanup:
|
||||
|
|
|
@ -2,10 +2,13 @@ import 'package:drift/drift.dart';
|
|||
import 'package:drift/internal/migrations.dart';
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:drift_dev/src/services/schema/verifier_impl.dart';
|
||||
import 'package:drift_dev/src/services/schema/verifier_common.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:sqlite3/sqlite3.dart';
|
||||
|
||||
export 'package:drift/internal/migrations.dart';
|
||||
export 'package:drift_dev/src/services/schema/verifier_common.dart'
|
||||
show SchemaMismatch;
|
||||
|
||||
abstract class SchemaVerifier {
|
||||
factory SchemaVerifier(SchemaInstantiationHelper helper) =
|
||||
|
@ -130,18 +133,6 @@ class _GenerateFromScratch extends GeneratedDatabase {
|
|||
int get schemaVersion => 1;
|
||||
}
|
||||
|
||||
/// Thrown when the actual schema differs from the expected schema.
|
||||
class SchemaMismatch implements Exception {
|
||||
final String explanation;
|
||||
|
||||
SchemaMismatch(this.explanation);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Schema does not match\n$explanation';
|
||||
}
|
||||
}
|
||||
|
||||
/// Contains an initialized schema with all tables, views, triggers and indices.
|
||||
///
|
||||
/// You can use the [newConnection] for your database class and the
|
||||
|
|
|
@ -174,19 +174,40 @@ class DriftAnalysisDriver {
|
|||
}
|
||||
|
||||
Future<void> _warnAboutUnresolvedImportsInDriftFile(FileState known) async {
|
||||
await discoverIfNecessary(known);
|
||||
|
||||
final state = known.discovery;
|
||||
if (state is DiscoveredDriftFile) {
|
||||
for (final import in state.imports) {
|
||||
final file = await findLocalElements(import.importedUri);
|
||||
|
||||
if (file.isValidImport != true) {
|
||||
known.errorsDuringDiscovery.add(
|
||||
DriftAnalysisError.inDriftFile(
|
||||
import.ast,
|
||||
'The imported file, `${import.importedUri}`, does not exist or '
|
||||
"can't be imported.",
|
||||
),
|
||||
var crossesPackageBoundaries = false;
|
||||
|
||||
if (import.importedUri.scheme == 'package' &&
|
||||
known.ownUri.scheme == 'package') {
|
||||
final ownPackage = known.ownUri.pathSegments.first;
|
||||
final importedPackage = import.importedUri.pathSegments.first;
|
||||
crossesPackageBoundaries = ownPackage != importedPackage;
|
||||
}
|
||||
|
||||
final message = StringBuffer(
|
||||
'The imported file, `${import.importedUri}`, does not exist or '
|
||||
"can't be imported.",
|
||||
);
|
||||
if (crossesPackageBoundaries) {
|
||||
message
|
||||
..writeln()
|
||||
..writeln(
|
||||
'Note: When importing drift files across packages, the '
|
||||
'imported package needs to apply a drift builder. '
|
||||
'See https://github.com/simolus3/drift/issues/2719 for '
|
||||
'details.',
|
||||
);
|
||||
}
|
||||
|
||||
known.errorsDuringDiscovery.add(
|
||||
DriftAnalysisError.inDriftFile(import.ast, message.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -241,7 +262,7 @@ class DriftAnalysisDriver {
|
|||
await _warnAboutUnresolvedImportsInDriftFile(known);
|
||||
|
||||
// Also make sure elements in transitive imports have been resolved.
|
||||
final seen = cache.knownFiles.keys.toSet();
|
||||
final seen = <Uri>{};
|
||||
final pending = <Uri>[known.ownUri];
|
||||
|
||||
while (pending.isNotEmpty) {
|
||||
|
|
|
@ -102,6 +102,9 @@ class DriftOptions {
|
|||
@JsonKey(name: 'write_to_columns_mixins', defaultValue: false)
|
||||
final bool writeToColumnsMixins;
|
||||
|
||||
@JsonKey(name: 'has_separate_analyzer', defaultValue: false)
|
||||
final bool hasDriftAnalyzer;
|
||||
|
||||
final String? preamble;
|
||||
|
||||
@JsonKey(name: 'fatal_warnings', defaultValue: false)
|
||||
|
@ -131,6 +134,7 @@ class DriftOptions {
|
|||
this.preamble,
|
||||
this.writeToColumnsMixins = false,
|
||||
this.fatalWarnings = false,
|
||||
this.hasDriftAnalyzer = false,
|
||||
});
|
||||
|
||||
DriftOptions({
|
||||
|
@ -155,6 +159,7 @@ class DriftOptions {
|
|||
required this.writeToColumnsMixins,
|
||||
required this.fatalWarnings,
|
||||
required this.preamble,
|
||||
required this.hasDriftAnalyzer,
|
||||
this.dialect,
|
||||
}) {
|
||||
// ignore: deprecated_member_use_from_same_package
|
||||
|
|
|
@ -192,7 +192,11 @@ class AppliedTypeConverter {
|
|||
/// column, drift generates a new wrapped type converter which will deal with
|
||||
/// `null` values.
|
||||
/// That converter is stored in this field.
|
||||
String get nullableFieldName => '${fieldName}n';
|
||||
String get nullableFieldName {
|
||||
assert(canBeSkippedForNulls && owningColumn?.nullable == true);
|
||||
|
||||
return '${fieldName}n';
|
||||
}
|
||||
|
||||
AppliedTypeConverter({
|
||||
required this.expression,
|
||||
|
|
|
@ -140,8 +140,10 @@ class _DriftBuildRun {
|
|||
// The discovery and analyzer builders will have emitted IR for
|
||||
// every relevant file in a previous build step that this builder
|
||||
// has a dependency on.
|
||||
findsResolvedElementsReliably: !mode.embeddedAnalyzer,
|
||||
findsLocalElementsReliably: !mode.embeddedAnalyzer,
|
||||
findsResolvedElementsReliably:
|
||||
!mode.embeddedAnalyzer || options.hasDriftAnalyzer,
|
||||
findsLocalElementsReliably:
|
||||
!mode.embeddedAnalyzer || options.hasDriftAnalyzer,
|
||||
);
|
||||
|
||||
Future<void> run() async {
|
||||
|
|
|
@ -3,7 +3,7 @@ import 'dart:io';
|
|||
import '../cli.dart';
|
||||
|
||||
class AnalyzeCommand extends MoorCommand {
|
||||
AnalyzeCommand(DriftDevCli cli) : super(cli);
|
||||
AnalyzeCommand(super.cli);
|
||||
|
||||
@override
|
||||
String get description => 'Analyze and lint drift database code';
|
||||
|
|
|
@ -7,7 +7,7 @@ import '../../analysis/results/results.dart';
|
|||
import '../cli.dart';
|
||||
|
||||
class IdentifyDatabases extends MoorCommand {
|
||||
IdentifyDatabases(DriftDevCli cli) : super(cli);
|
||||
IdentifyDatabases(super.cli);
|
||||
|
||||
@override
|
||||
String get description =>
|
||||
|
|
|
@ -26,7 +26,7 @@ class MigrateCommand extends MoorCommand {
|
|||
|
||||
late final AnalysisContext context;
|
||||
|
||||
MigrateCommand(DriftDevCli cli) : super(cli);
|
||||
MigrateCommand(super.cli);
|
||||
|
||||
@override
|
||||
String get description => 'Migrate a project from moor to drift';
|
||||
|
|
|
@ -86,7 +86,8 @@ class DumpSchemaCommand extends Command {
|
|||
|
||||
try {
|
||||
final elements = await extractDriftElementsFromDatabase(opened);
|
||||
final userVersion = opened.select('pragma user_version').single[0] as int;
|
||||
final userVersion =
|
||||
opened.select('pragma user_version').single.columnAt(0) as int;
|
||||
|
||||
return _AnalyzedDatabase(elements, userVersion);
|
||||
} finally {
|
||||
|
|
|
@ -33,6 +33,7 @@ DriftOptions _$DriftOptionsFromJson(Map json) => $checkedCreate(
|
|||
'store_date_time_values_as_text',
|
||||
'case_from_dart_to_sql',
|
||||
'write_to_columns_mixins',
|
||||
'has_separate_analyzer',
|
||||
'preamble',
|
||||
'fatal_warnings'
|
||||
],
|
||||
|
@ -91,6 +92,8 @@ DriftOptions _$DriftOptionsFromJson(Map json) => $checkedCreate(
|
|||
fatalWarnings:
|
||||
$checkedConvert('fatal_warnings', (v) => v as bool? ?? false),
|
||||
preamble: $checkedConvert('preamble', (v) => v as String?),
|
||||
hasDriftAnalyzer: $checkedConvert(
|
||||
'has_separate_analyzer', (v) => v as bool? ?? false),
|
||||
dialect: $checkedConvert('sql',
|
||||
(v) => v == null ? null : DialectOptions.fromJson(v as Map)),
|
||||
);
|
||||
|
@ -120,6 +123,7 @@ DriftOptions _$DriftOptionsFromJson(Map json) => $checkedCreate(
|
|||
'caseFromDartToSql': 'case_from_dart_to_sql',
|
||||
'writeToColumnsMixins': 'write_to_columns_mixins',
|
||||
'fatalWarnings': 'fatal_warnings',
|
||||
'hasDriftAnalyzer': 'has_separate_analyzer',
|
||||
'dialect': 'sql'
|
||||
},
|
||||
);
|
||||
|
@ -153,6 +157,7 @@ Map<String, dynamic> _$DriftOptionsToJson(DriftOptions instance) =>
|
|||
'case_from_dart_to_sql':
|
||||
_$CaseFromDartToSqlEnumMap[instance.caseFromDartToSql]!,
|
||||
'write_to_columns_mixins': instance.writeToColumnsMixins,
|
||||
'has_separate_analyzer': instance.hasDriftAnalyzer,
|
||||
'preamble': instance.preamble,
|
||||
'fatal_warnings': instance.fatalWarnings,
|
||||
};
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import 'package:analyzer/dart/element/element.dart';
|
||||
import 'package:drift_dev/src/analysis/options.dart';
|
||||
import 'package:drift_dev/src/services/schema/verifier_impl.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:sqlite3/common.dart';
|
||||
import 'package:sqlparser/sqlparser.dart';
|
||||
|
@ -8,6 +7,7 @@ import 'package:sqlparser/sqlparser.dart';
|
|||
import '../../analysis/backend.dart';
|
||||
import '../../analysis/driver/driver.dart';
|
||||
import '../../analysis/results/results.dart';
|
||||
import 'verifier_common.dart';
|
||||
|
||||
/// Extracts drift elements from the schema of an existing database.
|
||||
///
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
import 'find_differences.dart';
|
||||
|
||||
/// Attempts to recognize whether [name] is likely the name of an internal
|
||||
/// sqlite3 table (like `sqlite3_sequence`) that we should not consider when
|
||||
/// comparing schemas.
|
||||
bool isInternalElement(String name, List<String> virtualTables) {
|
||||
// Skip sqlite-internal tables, https://www.sqlite.org/fileformat2.html#intschema
|
||||
if (name.startsWith('sqlite_')) return true;
|
||||
if (virtualTables.any((v) => name.startsWith('${v}_'))) return true;
|
||||
|
||||
// This file is added on some Android versions when using the native Android
|
||||
// database APIs, https://github.com/simolus3/drift/discussions/2042
|
||||
if (name == 'android_metadata') return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void verify(List<Input> referenceSchema, List<Input> actualSchema,
|
||||
bool validateDropped) {
|
||||
final result =
|
||||
FindSchemaDifferences(referenceSchema, actualSchema, validateDropped)
|
||||
.compare();
|
||||
|
||||
if (!result.noChanges) {
|
||||
throw SchemaMismatch(result.describe());
|
||||
}
|
||||
}
|
||||
|
||||
/// Thrown when the actual schema differs from the expected schema.
|
||||
class SchemaMismatch implements Exception {
|
||||
final String explanation;
|
||||
|
||||
SchemaMismatch(this.explanation);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Schema does not match\n$explanation';
|
||||
}
|
||||
}
|
|
@ -6,6 +6,7 @@ import 'package:drift_dev/api/migrations.dart';
|
|||
import 'package:sqlite3/sqlite3.dart';
|
||||
|
||||
import 'find_differences.dart';
|
||||
import 'verifier_common.dart';
|
||||
|
||||
Expando<List<Input>> expectedSchema = Expando();
|
||||
|
||||
|
@ -94,21 +95,6 @@ Input? _parseInputFromSchemaRow(
|
|||
return Input(name, row['sql'] as String);
|
||||
}
|
||||
|
||||
/// Attempts to recognize whether [name] is likely the name of an internal
|
||||
/// sqlite3 table (like `sqlite3_sequence`) that we should not consider when
|
||||
/// comparing schemas.
|
||||
bool isInternalElement(String name, List<String> virtualTables) {
|
||||
// Skip sqlite-internal tables, https://www.sqlite.org/fileformat2.html#intschema
|
||||
if (name.startsWith('sqlite_')) return true;
|
||||
if (virtualTables.any((v) => name.startsWith('${v}_'))) return true;
|
||||
|
||||
// This file is added on some Android versions when using the native Android
|
||||
// database APIs, https://github.com/simolus3/drift/discussions/2042
|
||||
if (name == 'android_metadata') return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
extension CollectSchemaDb on DatabaseConnectionUser {
|
||||
Future<List<Input>> collectSchemaInput(List<String> virtualTables) async {
|
||||
final result = await customSelect('SELECT * FROM sqlite_master;').get();
|
||||
|
@ -141,17 +127,6 @@ extension CollectSchema on QueryExecutor {
|
|||
}
|
||||
}
|
||||
|
||||
void verify(List<Input> referenceSchema, List<Input> actualSchema,
|
||||
bool validateDropped) {
|
||||
final result =
|
||||
FindSchemaDifferences(referenceSchema, actualSchema, validateDropped)
|
||||
.compare();
|
||||
|
||||
if (!result.noChanges) {
|
||||
throw SchemaMismatch(result.describe());
|
||||
}
|
||||
}
|
||||
|
||||
class _DelegatingUser extends QueryExecutorUser {
|
||||
@override
|
||||
final int schemaVersion;
|
||||
|
|
|
@ -85,7 +85,7 @@ class ModularAccessorWriter {
|
|||
// Also make imports available
|
||||
final imports = file.discovery?.importDependencies ?? const [];
|
||||
for (final import in imports) {
|
||||
final file = driver.cache.knownFiles[import];
|
||||
final file = driver.cache.knownFiles[import.uri];
|
||||
|
||||
if (file != null && file.needsModularAccessor(driver)) {
|
||||
final moduleClass = restOfClass.modularAccessor(import.uri);
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue