diff --git a/docs/lib/snippets/modular/upserts.dart b/docs/lib/snippets/modular/upserts.dart new file mode 100644 index 00000000..3a407a57 --- /dev/null +++ b/docs/lib/snippets/modular/upserts.dart @@ -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 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>>? 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 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 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 +} diff --git a/docs/lib/snippets/modular/upserts.drift.dart b/docs/lib/snippets/modular/upserts.drift.dart new file mode 100644 index 00000000..977b6e89 --- /dev/null +++ b/docs/lib/snippets/modular/upserts.drift.dart @@ -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 word = i0.GeneratedColumn( + 'word', aliasedName, false, + type: i0.DriftSqlType.string, requiredDuringInsert: true); + static const i0.VerificationMeta _usagesMeta = + const i0.VerificationMeta('usages'); + @override + late final i0.GeneratedColumn usages = i0.GeneratedColumn( + 'usages', aliasedName, false, + type: i0.DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const i3.Constant(1)); + @override + List 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 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 get $primaryKey => {word}; + @override + i1.Word map(Map 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 { + final String word; + final int usages; + const Word({required this.word, required this.usages}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['word'] = i0.Variable(word); + map['usages'] = i0.Variable(usages); + return map; + } + + i1.WordsCompanion toCompanion(bool nullToAbsent) { + return i1.WordsCompanion( + word: i0.Value(word), + usages: i0.Value(usages), + ); + } + + factory Word.fromJson(Map json, + {i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return Word( + word: serializer.fromJson(json['word']), + usages: serializer.fromJson(json['usages']), + ); + } + @override + Map toJson({i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return { + 'word': serializer.toJson(word), + 'usages': serializer.toJson(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 { + final i0.Value word; + final i0.Value usages; + final i0.Value 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 custom({ + i0.Expression? word, + i0.Expression? usages, + i0.Expression? rowid, + }) { + return i0.RawValuesInsertable({ + if (word != null) 'word': word, + if (usages != null) 'usages': usages, + if (rowid != null) 'rowid': rowid, + }); + } + + i1.WordsCompanion copyWith( + {i0.Value? word, i0.Value? usages, i0.Value? rowid}) { + return i1.WordsCompanion( + word: word ?? this.word, + usages: usages ?? this.usages, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (word.present) { + map['word'] = i0.Variable(word.value); + } + if (usages.present) { + map['usages'] = i0.Variable(usages.value); + } + if (rowid.present) { + map['rowid'] = i0.Variable(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 id = i0.GeneratedColumn( + '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 teamA = i0.GeneratedColumn( + 'team_a', aliasedName, false, + type: i0.DriftSqlType.string, requiredDuringInsert: true); + static const i0.VerificationMeta _teamBMeta = + const i0.VerificationMeta('teamB'); + @override + late final i0.GeneratedColumn teamB = i0.GeneratedColumn( + 'team_b', aliasedName, false, + type: i0.DriftSqlType.string, requiredDuringInsert: true); + static const i0.VerificationMeta _teamAWonMeta = + const i0.VerificationMeta('teamAWon'); + @override + late final i0.GeneratedColumn teamAWon = i0.GeneratedColumn( + 'team_a_won', aliasedName, false, + type: i0.DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: i0.GeneratedColumn.constraintIsAlways( + 'CHECK ("team_a_won" IN (0, 1))')); + @override + List 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 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 get $primaryKey => {id}; + @override + List> get uniqueKeys => [ + {teamA, teamB}, + ]; + @override + i1.MatchResult map(Map 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 { + 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 toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = i0.Variable(id); + map['team_a'] = i0.Variable(teamA); + map['team_b'] = i0.Variable(teamB); + map['team_a_won'] = i0.Variable(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 json, + {i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return MatchResult( + id: serializer.fromJson(json['id']), + teamA: serializer.fromJson(json['teamA']), + teamB: serializer.fromJson(json['teamB']), + teamAWon: serializer.fromJson(json['teamAWon']), + ); + } + @override + Map toJson({i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'teamA': serializer.toJson(teamA), + 'teamB': serializer.toJson(teamB), + 'teamAWon': serializer.toJson(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 { + final i0.Value id; + final i0.Value teamA; + final i0.Value teamB; + final i0.Value 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 custom({ + i0.Expression? id, + i0.Expression? teamA, + i0.Expression? teamB, + i0.Expression? 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? id, + i0.Value? teamA, + i0.Value? teamB, + i0.Value? teamAWon}) { + return i1.MatchResultsCompanion( + id: id ?? this.id, + teamA: teamA ?? this.teamA, + teamB: teamB ?? this.teamB, + teamAWon: teamAWon ?? this.teamAWon, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = i0.Variable(id.value); + } + if (teamA.present) { + map['team_a'] = i0.Variable(teamA.value); + } + if (teamB.present) { + map['team_b'] = i0.Variable(teamB.value); + } + if (teamAWon.present) { + map['team_a_won'] = i0.Variable(teamAWon.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MatchResultsCompanion(') + ..write('id: $id, ') + ..write('teamA: $teamA, ') + ..write('teamB: $teamB, ') + ..write('teamAWon: $teamAWon') + ..write(')')) + .toString(); + } +} diff --git a/docs/pages/docs/Dart API/writes.md b/docs/pages/docs/Dart API/writes.md index 51127b0a..69a79c44 100644 --- a/docs/pages/docs/Dart API/writes.md +++ b/docs/pages/docs/Dart API/writes.md @@ -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 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 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 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