Merge branch 'simolus3:develop' into codespace-glowing-space-palm-tree-9vwjv7wx993p57x

This commit is contained in:
Abass Sesay 2023-11-16 21:18:35 -05:00 committed by GitHub
commit 581708a328
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
168 changed files with 2217 additions and 579 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -3,3 +3,7 @@ import 'dart:io';
Future<Directory> getApplicationDocumentsDirectory() {
throw UnsupportedError('stub!');
}
Future<Directory> getTemporaryDirectory() {
throw UnsupportedError('stub!');
}

View File

@ -0,0 +1,3 @@
Future<void> applyWorkaroundToOpenSqlite3OnOldAndroidVersions() async {
throw 'stub!';
}

View File

@ -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

View File

@ -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 {

View File

@ -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;

View File

@ -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
}

View File

@ -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

View File

@ -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 =>

View File

@ -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) {

View File

@ -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;

View File

@ -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;

View File

@ -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
}

View File

@ -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();
}
}

View File

@ -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);

View File

@ -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());
// ...

View File

@ -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);
});
}

View 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.

View File

@ -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.

View File

@ -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:

View File

@ -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

View File

@ -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" %}

View File

@ -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.

View File

@ -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
```

View File

@ -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

View File

@ -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.

View File

@ -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.

View File

@ -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!

View File

@ -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 &lt;at&gt;simonbinder&lt;dot&gt;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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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.

View File

@ -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 %}

View File

@ -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

View File

@ -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;

View File

@ -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'
],
),
),
),
);
});
}

View File

@ -35,7 +35,7 @@ class SnippetsBuilder extends CodeExcerptBuilder {
}
class _DriftHighlighter extends Highlighter {
_DriftHighlighter(SourceFile file) : super(file);
_DriftHighlighter(super.file);
@override
void highlight() {

View File

@ -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.

View File

@ -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.

View File

@ -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;

View File

@ -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';

View File

@ -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) {

View File

@ -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,
);

View File

@ -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]!

View File

@ -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;

View File

@ -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:

View File

@ -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);
}
}

View File

@ -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();
}

View File

@ -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();
}
}

View File

@ -93,6 +93,7 @@ class EntityDescription {
return EntityDescription(
name: entity.entityName,
type: switch (entity) {
VirtualTableInfo() => 'virtual_table',
TableInfo() => 'table',
ViewInfo() => 'view',
Index() => 'index',

View File

@ -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,
};

View File

@ -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);
}

View File

@ -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.

View File

@ -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].

View File

@ -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) {

View File

@ -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

View File

@ -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.

View File

@ -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;

View File

@ -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');
}

View File

@ -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.

View File

@ -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

View File

@ -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 {

View File

@ -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 => [];

View File

@ -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]));
});
});
}

View File

@ -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);
}
}

View File

@ -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,

View File

@ -15,7 +15,7 @@ part 'custom_tables.g.dart';
},
)
class CustomTablesDb extends _$CustomTablesDb {
CustomTablesDb(QueryExecutor e) : super(e) {
CustomTablesDb(super.e) {
driftRuntimeOptions.dontWarnAboutMultipleDatabases = true;
}

View File

@ -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'),

View File

@ -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
);

View File

@ -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 =>

View File

@ -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

View File

@ -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);
}

View File

@ -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));'
]));
});
}

View File

@ -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, '

View File

@ -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();

View File

@ -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();

View File

@ -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();
});

View File

@ -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));
});
}
}

View File

@ -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

View File

@ -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.

View File

@ -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:

View File

@ -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

View File

@ -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) {

View File

@ -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

View File

@ -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,

View File

@ -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 {

View File

@ -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';

View File

@ -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 =>

View File

@ -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';

View File

@ -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 {

View File

@ -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,
};

View File

@ -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.
///

View File

@ -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';
}
}

View File

@ -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;

View File

@ -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