diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index c20d8770..658271a4 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -18,6 +18,12 @@ jobs: run: dart run tool/ci_build.dart working-directory: docs + - name: Analyze Dart sources + working-directory: docs + run: | + dart analyze --fatal-infos --fatal-warnings + dart run drift_dev analyze + - name: Deploy to netlify (Branch) if: ${{ github.event_name == 'push' }} uses: nwtgck/actions-netlify@v1.2 diff --git a/.gitignore b/.gitignore index 78b82a71..94d463b8 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,5 @@ flutter_export_environment.sh # Local Netlify folder .netlify .DS_Store + +docs/**/*.g.dart diff --git a/docs/assets/path_provider/lib/path_provider.dart b/docs/assets/path_provider/lib/path_provider.dart new file mode 100644 index 00000000..73d86f30 --- /dev/null +++ b/docs/assets/path_provider/lib/path_provider.dart @@ -0,0 +1,5 @@ +import 'dart:io'; + +Future getApplicationDocumentsDirectory() { + throw UnsupportedError('stub!'); +} diff --git a/docs/assets/path_provider/pubspec.yaml b/docs/assets/path_provider/pubspec.yaml new file mode 100644 index 00000000..a25aaaa8 --- /dev/null +++ b/docs/assets/path_provider/pubspec.yaml @@ -0,0 +1,6 @@ +name: path_provider +publish_to: none +description: Fake "path_provider" package so that we can import it in snippets without depending on Flutter. + +environment: + sdk: ^2.16.0 diff --git a/docs/build.deploy.yaml b/docs/build.deploy.yaml index 3c24ea0f..75e47ee4 100644 --- a/docs/build.deploy.yaml +++ b/docs/build.deploy.yaml @@ -4,17 +4,31 @@ builders: build_to: cache builder_factories: ["writeVersions"] build_extensions: {"$package$": ["lib/versions.json"]} + code_snippets: + import: 'tool/snippets.dart' + build_to: cache + builder_factories: ["SnippetsBuilder.new"] + build_extensions: {"": [".excerpt.json"]} + auto_apply: none targets: prepare: builders: "|versions": enabled: true + "|code_snippets": + enabled: true + generate_for: + - "lib/snippets/**/*.dart" + - "lib/snippets/*/*.drift" + - "lib/snippets/*.dart" auto_apply_builders: false sources: - "$package$" - "lib/versions.json" + - "lib/snippets/**" - "tool/write_versions.dart" + - "tool/snippets.dart" $default: dependencies: [":prepare"] diff --git a/docs/build.yaml b/docs/build.yaml index 243221fa..c3ec60ad 100644 --- a/docs/build.yaml +++ b/docs/build.yaml @@ -4,17 +4,31 @@ builders: build_to: cache builder_factories: ["writeVersions"] build_extensions: {"$package$": ["lib/versions.json"]} + code_snippets: + import: 'tool/snippets.dart' + build_to: cache + builder_factories: ["SnippetsBuilder.new"] + build_extensions: {"": [".excerpt.json"]} + auto_apply: none targets: prepare: builders: "|versions": enabled: true + "|code_snippets": + enabled: true + generate_for: + - "lib/snippets/**/*.dart" + - "lib/snippets/*/*.drift" + - "lib/snippets/*.dart" auto_apply_builders: false sources: - "$package$" - "lib/versions.json" + - "lib/snippets/**" - "tool/write_versions.dart" + - "tool/snippets.dart" $default: dependencies: [":prepare"] diff --git a/docs/lib/_highlight.scss b/docs/lib/_highlight.scss index 76fb8865..878e7f7f 100644 --- a/docs/lib/_highlight.scss +++ b/docs/lib/_highlight.scss @@ -22,6 +22,7 @@ .hljs-number, .hljs-literal, .hljs-variable, +.hljs-title.function_.invoked__, .hljs-template-variable, .hljs-tag .hljs-attr { color: #008081; @@ -44,7 +45,7 @@ } .hljs-type, -.hljs-class .hljs-title { +.hljs-title.class_ { color: #458; font-weight: bold; } diff --git a/docs/lib/snippets/drift_files/database.dart b/docs/lib/snippets/drift_files/database.dart new file mode 100644 index 00000000..4de57e08 --- /dev/null +++ b/docs/lib/snippets/drift_files/database.dart @@ -0,0 +1,27 @@ +// #docregion overview +import 'package:drift/drift.dart'; +import 'package:drift/native.dart'; + +part 'database.g.dart'; + +@DriftDatabase( + include: {'tables.drift'}, +) +class MyDb extends _$MyDb { + // This example creates a simple in-memory database (without actual + // persistence). + // To store data, see the database setups from other "Getting started" guides. + MyDb() : super(NativeDatabase.memory()); + + @override + int get schemaVersion => 1; +} +// #enddocregion overview + +extension MoreSnippets on MyDb { + // #docregion dart_interop_insert + Future insert(TodosCompanion companion) async { + await into(todos).insert(companion); + } + // #enddocregion dart_interop_insert +} diff --git a/docs/lib/snippets/drift_files/nested.drift b/docs/lib/snippets/drift_files/nested.drift new file mode 100644 index 00000000..559126d6 --- /dev/null +++ b/docs/lib/snippets/drift_files/nested.drift @@ -0,0 +1,44 @@ +/* #docregion overview */ +CREATE TABLE coordinates ( + id INTEGER NOT NULL PRIMARY KEY, + lat REAL NOT NULL, + long REAL NOT NULL +); + +CREATE TABLE saved_routes ( + id INTEGER NOT NULL PRIMARY KEY, + name TEXT NOT NULL, + "from" INTEGER NOT NULL REFERENCES coordinates (id), + "to" INTEGER NOT NULL REFERENCES coordinates (id) +); + +/* #enddocregion overview */ +/* #docregion route_points*/ +CREATE TABLE route_points ( + route INTEGER NOT NULL REFERENCES saved_routes (id), + point INTEGER NOT NULL REFERENCES coordinates (id), + index_on_route INTEGER, + PRIMARY KEY (route, point) +); +/* #enddocregion route_points */ + +/* #docregion overview */ +routesWithPoints: SELECT r.id, r.name, f.*, t.* FROM routes r + INNER JOIN coordinates f ON f.id = r."from" + INNER JOIN coordinates t ON t.id = r."to"; +/* #enddocregion overview */ +/* #docregion nested */ +routesWithNestedPoints: SELECT r.id, r.name, f.** AS "from", t.** AS "to" FROM routes r + INNER JOIN coordinates f ON f.id = r."from" + INNER JOIN coordinates t ON t.id = r."to"; +/* #enddocregion nested */ +/* #docregion list */ +routeWithPoints: SELECT + route.**, + LIST(SELECT coordinates.* FROM route_points + INNER JOIN coordinates ON id = point + WHERE route = route.id + ORDER BY index_on_route + ) AS points + FROM saved_routes route; +/* #enddocregion list */ \ No newline at end of file diff --git a/docs/lib/snippets/drift_files/small_snippets.drift b/docs/lib/snippets/drift_files/small_snippets.drift new file mode 100644 index 00000000..c7de7c0e --- /dev/null +++ b/docs/lib/snippets/drift_files/small_snippets.drift @@ -0,0 +1,19 @@ +/* #docregion import */ +import 'tables.drift'; -- single quotes are required for imports +/* #enddocregion import */ + +/* #docregion q1 */ +myQuery(:variable AS TEXT): SELECT :variable; +/* #enddocregion q1 */ +/* #docregion q2 */ +myNullableQuery(:variable AS TEXT OR NULL): SELECT :variable; +/* #enddocregion q2 */ +/* #docregion q3 */ +myRequiredQuery(REQUIRED :variable AS TEXT OR NULL): SELECT :variable; +/* #enddocregion q3 */ +/* #docregion entries */ +entriesWithId: SELECT * FROM todos WHERE id IN ?; +/* #enddocregion entries */ +/* #docregion filter */ +_filterTodos: SELECT * FROM todos WHERE $predicate; +/* #enddocregion filter */ diff --git a/docs/lib/snippets/drift_files/tables.drift b/docs/lib/snippets/drift_files/tables.drift new file mode 100644 index 00000000..7b654d2e --- /dev/null +++ b/docs/lib/snippets/drift_files/tables.drift @@ -0,0 +1,19 @@ +CREATE TABLE todos ( + id INT NOT NULL PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL, + content TEXT NOT NULL, + category INTEGER REFERENCES categories(id) +); + +CREATE TABLE categories ( + id INT NOT NULL PRIMARY KEY AUTOINCREMENT, + description TEXT NOT NULL +) AS Category; -- the AS xyz after the table defines the data class name + +-- 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: +createEntry: INSERT INTO todos (title, content) VALUES (:title, :content); +deleteById: DELETE FROM todos WHERE id = :id; +allTodos: SELECT * FROM todos; diff --git a/docs/lib/snippets/many_to_many_relationships.dart b/docs/lib/snippets/many_to_many_relationships.dart new file mode 100644 index 00000000..b284086a --- /dev/null +++ b/docs/lib/snippets/many_to_many_relationships.dart @@ -0,0 +1,159 @@ +import 'package:drift/drift.dart'; +import 'package:rxdart/rxdart.dart'; + +part 'many_to_many_relationships.g.dart'; + +// #docregion buyable_items +class BuyableItems extends Table { + IntColumn get id => integer().autoIncrement()(); + TextColumn get description => text()(); + IntColumn get price => integer()(); + // we could add more columns as we wish. +} +// #enddocregion buyable_items + +// #docregion cart_tables +class ShoppingCarts extends Table { + IntColumn get id => integer().autoIncrement()(); + // we could also store some further information about the user creating + // this cart etc. +} + +@DataClassName('ShoppingCartEntry') +class ShoppingCartEntries extends Table { + // id of the cart that should contain this item. + IntColumn get shoppingCart => integer()(); + // id of the item in this cart + IntColumn get item => integer()(); + // again, we could store additional information like when the item was + // added, an amount, etc. +} +// #enddocregion cart_tables + +// #docregion cart +/// Represents a full shopping cart with all its items. +class CartWithItems { + final ShoppingCart cart; + final List items; + + CartWithItems(this.cart, this.items); +} +// #enddocregion cart + +@DriftDatabase(tables: [BuyableItems, ShoppingCarts, ShoppingCartEntries]) +class Db extends _$Db { + Db(QueryExecutor e) : super(e); + + @override + int get schemaVersion => 1; + + // #docregion writeShoppingCart + Future writeShoppingCart(CartWithItems entry) { + return transaction(() async { + final cart = entry.cart; + + // first, we write the shopping cart + await into(shoppingCarts).insert(cart, mode: InsertMode.replace); + + // we replace the entries of the cart, so first delete the old ones + await (delete(shoppingCartEntries) + ..where((entry) => entry.shoppingCart.equals(cart.id))) + .go(); + + // And write the new ones + for (final item in entry.items) { + await into(shoppingCartEntries) + .insert(ShoppingCartEntry(shoppingCart: cart.id, item: item.id)); + } + }); + } + // #enddocregion writeShoppingCart + + // #docregion createEmptyCart + Future createEmptyCart() async { + final id = await into(shoppingCarts).insert(const ShoppingCartsCompanion()); + final cart = ShoppingCart(id: id); + // we set the items property to [] because we've just created the cart - it + // will be empty + return CartWithItems(cart, []); + } + // #enddocregion createEmptyCart + + // #docregion watchCart + Stream watchCart(int id) { + // load information about the cart + final cartQuery = select(shoppingCarts) + ..where((cart) => cart.id.equals(id)); + + // and also load information about the entries in this cart + final contentQuery = select(shoppingCartEntries).join( + [ + innerJoin( + buyableItems, + buyableItems.id.equalsExp(shoppingCartEntries.item), + ), + ], + )..where(shoppingCartEntries.shoppingCart.equals(id)); + + final cartStream = cartQuery.watchSingle(); + + final contentStream = contentQuery.watch().map((rows) { + // we join the shoppingCartEntries with the buyableItems, but we + // only care about the item here. + return rows.map((row) => row.readTable(buyableItems)).toList(); + }); + + // now, we can merge the two queries together in one stream + return Rx.combineLatest2(cartStream, contentStream, + (ShoppingCart cart, List items) { + return CartWithItems(cart, items); + }); + } + // #enddocregion watchCart + + // #docregion watchAllCarts + Stream> watchAllCarts() { + // start by watching all carts + final cartStream = select(shoppingCarts).watch(); + + return cartStream.switchMap((carts) { + // this method is called whenever the list of carts changes. For each + // cart, now we want to load all the items in it. + // (we create a map from id to cart here just for performance reasons) + final idToCart = {for (var cart in carts) cart.id: cart}; + final ids = idToCart.keys; + + // select all entries that are included in any cart that we found + final entryQuery = select(shoppingCartEntries).join( + [ + innerJoin( + buyableItems, + buyableItems.id.equalsExp(shoppingCartEntries.item), + ) + ], + )..where(shoppingCartEntries.shoppingCart.isIn(ids)); + + return entryQuery.watch().map((rows) { + // Store the list of entries for each cart, again using maps for faster + // lookups. + final idToItems = >{}; + + // for each entry (row) that is included in a cart, put it in the map + // of items. + for (final row in rows) { + final item = row.readTable(buyableItems); + final id = row.readTable(shoppingCartEntries).shoppingCart; + + idToItems.putIfAbsent(id, () => []).add(item); + } + + // finally, all that's left is to merge the map of carts with the map of + // entries + return [ + for (var id in ids) CartWithItems(idToCart[id]!, idToItems[id] ?? []), + ]; + }); + }); + } + // #enddocregion watchAllCarts +} diff --git a/docs/lib/snippets/platforms.dart b/docs/lib/snippets/platforms.dart new file mode 100644 index 00000000..83c1b1fe --- /dev/null +++ b/docs/lib/snippets/platforms.dart @@ -0,0 +1,16 @@ +import 'dart:ffi'; +import 'dart:io'; +import 'package:sqlite3/open.dart'; + +void main() { + open.overrideFor(OperatingSystem.linux, _openOnLinux); + + // After setting all the overrides, you can use drift! +} + +DynamicLibrary _openOnLinux() { + final scriptDir = File(Platform.script.toFilePath()).parent; + final libraryNextToScript = File('${scriptDir.path}/sqlite3.so'); + return DynamicLibrary.open(libraryNextToScript.path); +} +// _openOnWindows could be implemented similarly by opening `sqlite3.dll` diff --git a/docs/lib/snippets/tables/filename.dart b/docs/lib/snippets/tables/filename.dart new file mode 100644 index 00000000..99c506cb --- /dev/null +++ b/docs/lib/snippets/tables/filename.dart @@ -0,0 +1,64 @@ +// ignore_for_file: directives_ordering + +// #docregion open +// These imports are only needed to open the database +import 'dart:io'; + +import 'package:drift/native.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:path/path.dart' as p; + +// #enddocregion open + +// #docregion overview +import 'package:drift/drift.dart'; + +// assuming that your file is called filename.dart. This will give an error at +// first, but it's needed for drift to know about the generated code +part 'filename.g.dart'; + +// this will generate a table called "todos" for us. The rows of that table will +// be represented by a class called "Todo". +class Todos extends Table { + IntColumn get id => integer().autoIncrement()(); + TextColumn get title => text().withLength(min: 6, max: 32)(); + TextColumn get content => text().named('body')(); + IntColumn get category => integer().nullable()(); +} + +// This will make drift generate a class called "Category" to represent a row in +// this table. By default, "Categorie" would have been used because it only +//strips away the trailing "s" in the table name. +@DataClassName('Category') +class Categories extends Table { + IntColumn get id => integer().autoIncrement()(); + TextColumn get description => text()(); +} + +// this annotation tells drift to prepare a database class that uses both of the +// tables we just defined. We'll see how to use that database class in a moment. +// #docregion open +@DriftDatabase(tables: [Todos, Categories]) +class MyDatabase extends _$MyDatabase { + // #enddocregion overview + // we tell the database where to store the data with this constructor + MyDatabase() : super(_openConnection()); + + // you should bump this number whenever you change or add a table definition. + // Migrations are covered later in the documentation. + @override + int get schemaVersion => 1; +// #docregion overview +} +// #enddocregion overview + +LazyDatabase _openConnection() { + // the LazyDatabase util lets us find the right location for the file async. + return LazyDatabase(() async { + // put the database file, called db.sqlite here, into the documents folder + // for your app. + final dbFolder = await getApplicationDocumentsDirectory(); + final file = File(p.join(dbFolder.path, 'db.sqlite')); + return NativeDatabase(file); + }); +} diff --git a/docs/pages/docs/Examples/relationships.md b/docs/pages/docs/Examples/relationships.md index dfbf2a3a..6dbfb489 100644 --- a/docs/pages/docs/Examples/relationships.md +++ b/docs/pages/docs/Examples/relationships.md @@ -5,173 +5,50 @@ data: template: layouts/docs/single --- +{% assign snippets = 'package:moor_documentation/snippets/many_to_many_relationships.dart.excerpt.json' | readString | json_decode %} + ## Defining the model In this example, we're going to model a shopping system and some of its queries in drift. First, we need to store some items that can be bought: -```dart -class BuyableItems extends Table { - IntColumn get id => integer().autoIncrement()(); - TextColumn get description => text()(); - IntColumn get price => integer()(); - // we could add more columns as we wish. -} -``` +{% include "blocks/snippet" snippets=snippets name="buyable_items" %} We're going to define two tables for shopping carts: One for the cart itself, and another one to store the entries in the cart: -```dart -class ShoppingCarts extends Table { - IntColumn get id => integer().autoIncrement()(); - // we could also store some further information about the user creating - // this cart etc. -} -@DataClassName('ShoppingCartEntry') -class ShoppingCartEntries extends Table { - // id of the cart that should contain this item. - IntColumn get shoppingCart => integer()(); - // id of the item in this cart - IntColumn get item => integer()(); - // again, we could store additional information like when the item was - // added, an amount, etc. -} -``` +{% include "blocks/snippet" snippets=snippets name="cart_tables" %} Drift will generate matching classes for the three tables. But having to use three different classes to model a shopping cart in our application would be quite annoying. Let's write a single class to represent an entire shopping -cart that: -```dart -/// Represents a full shopping cart with all its items. -class CartWithItems { - final ShoppingCart cart; - final List items; +cart: - CartWithItems(this.cart, this.items); -} -``` +{% include "blocks/snippet" snippets=snippets name="cart" %} ## Inserts We want to write a `CartWithItems` instance into the database. We assume that all the `BuyableItem`s included already exist in the database (we could store them via `into(buyableItems).insert(BuyableItemsCompanion(...))`). Then, we can insert a full cart with -```dart -Future writeShoppingCart(CartWithItems entry) { - return transaction((_) async { - final cart = entry.cart; - // first, we write the shopping cart - await into(shoppingCarts).insert(cart, mode: InsertMode.replace); - - // we replace the entries of the cart, so first delete the old ones - await (delete(shoppingCartEntries) - ..where((entry) => entry.shoppingCart.equals(cart.id))) - .go(); - - // And write the new ones - for (final item in entry.items) { - await into(shoppingCartEntries).insert(ShoppingCartEntry(shoppingCart: cart.id, item: item.id)); - } - }); -} -``` +{% include "blocks/snippet" snippets=snippets name="writeShoppingCart" %} We could also define a helpful method to create a new, empty shopping cart: -```dart -Future createEmptyCart() async { - final id = await into(shoppingCarts).insert(const ShoppingCartsCompanion()); - final cart = ShoppingCart(id: id); - // we set the items property to [] because we've just created the cart - it will be empty - return CartWithItems(cart, []); -} -``` + +{% include "blocks/snippet" snippets=snippets name="createEmptyCart" %} ## Selecting a cart As our `CartWithItems` class consists of multiple components that are separated in the database (information about the cart, and information about the added items), we'll have -to merge two streams together. The `rxdart` library helps here by providing the +to merge two streams together. The `rxdart` library helps here by providing the `combineLatest2` method, allowing us to write -```dart -Stream watchCart(int id) { - // load information about the cart - final cartQuery = select(shoppingCarts)..where((cart) => cart.id.equals(id)); - // and also load information about the entries in this cart - final contentQuery = select(shoppingCartEntries).join( - [ - innerJoin( - buyableItems, - buyableItems.id.equalsExp(shoppingCartEntries.item), - ), - ], - )..where(shoppingCartEntries.shoppingCart.equals(id)); - - final cartStream = cartQuery.watchSingle(); - - final contentStream = contentQuery.watch().map((rows) { - // we join the shoppingCartEntries with the buyableItems, but we - // only care about the item here. - return rows.map((row) => row.readTable(buyableItems)).toList(); - }); - - // now, we can merge the two queries together in one stream - return Rx.combineLatest2(cartStream, contentStream, - (ShoppingCart cart, List items) { - return CartWithItems(cart, items); - }); -} -``` +{% include "blocks/snippet" snippets=snippets name="watchCart" %} ## Selecting all carts Instead of watching a single cart and all associated entries, we now watch all carts and load all entries for each cart. For this type of transformation, RxDart's `switchMap` comes in handy: -```dart -Stream> watchAllCarts() { - // start by watching all carts - final cartStream = select(shoppingCarts).watch(); - return cartStream.switchMap((carts) { - // this method is called whenever the list of carts changes. For each - // cart, now we want to load all the items in it. - // (we create a map from id to cart here just for performance reasons) - final idToCart = {for (var cart in carts) cart.id: cart}; - final ids = idToCart.keys; - - // select all entries that are included in any cart that we found - final entryQuery = select(shoppingCartEntries).join( - [ - innerJoin( - buyableItems, - buyableItems.id.equalsExp(shoppingCartEntries.item), - ) - ], - )..where(shoppingCartEntries.shoppingCart.isIn(ids)); - - return entryQuery.watch().map((rows) { - // Store the list of entries for each cart, again using maps for faster - // lookups. - final idToItems = >{}; - - // for each entry (row) that is included in a cart, put it in the map - // of items. - for (var row in rows) { - final item = row.readTable(buyableItems); - final id = row.readTable(shoppingCartEntries).shoppingCart; - - idToItems.putIfAbsent(id, () => []).add(item); - } - - // finally, all that's left is to merge the map of carts with the map of - // entries - return [ - for (var id in ids) - CartWithItems(idToCart[id], idToItems[id] ?? []), - ]; - }); - }); -} -``` \ No newline at end of file +{% include "blocks/snippet" snippets=snippets name="watchAllCarts" %} diff --git a/docs/pages/docs/Getting started/index.md b/docs/pages/docs/Getting started/index.md index d47d5cbd..b570b212 100644 --- a/docs/pages/docs/Getting started/index.md +++ b/docs/pages/docs/Getting started/index.md @@ -18,6 +18,7 @@ At the moment, the current version of `drift` is [![Drift version](https://img.s and the latest version of `drift_dev` is [![Generator version](https://img.shields.io/pub/v/drift_dev.svg)](https://pub.dev/packages/drift_dev). {% assign versions = 'package:moor_documentation/versions.json' | readString | json_decode %} +{% assign snippets = 'package:moor_documentation/snippets/tables/filename.dart.excerpt.json' | readString | json_decode %} ```yaml dependencies: @@ -43,40 +44,10 @@ If you're wondering why so many packages are necessary, here's a quick overview {% include "partials/changed_to_ffi" %} ### Declaring tables + Using drift, you can model the structure of your tables with simple dart code: -```dart -import 'package:drift/drift.dart'; -// assuming that your file is called filename.dart. This will give an error at first, -// but it's needed for drift to know about the generated code -part 'filename.g.dart'; - -// this will generate a table called "todos" for us. The rows of that table will -// be represented by a class called "Todo". -class Todos extends Table { - IntColumn get id => integer().autoIncrement()(); - TextColumn get title => text().withLength(min: 6, max: 32)(); - TextColumn get content => text().named('body')(); - IntColumn get category => integer().nullable()(); -} - -// This will make drift generate a class called "Category" to represent a row in this table. -// By default, "Categorie" would have been used because it only strips away the trailing "s" -// in the table name. -@DataClassName("Category") -class Categories extends Table { - - IntColumn get id => integer().autoIncrement()(); - TextColumn get description => text()(); -} - -// this annotation tells drift to prepare a database class that uses both of the -// tables we just defined. We'll see how to use that database class in a moment. -@DriftDatabase(tables: [Todos, Categories]) -class MyDatabase { - -} -``` +{% include "blocks/snippet" snippets = snippets name = "overview" %} __⚠️ Note:__ The column definitions, the table name and the primary key must be known at compile time. For column definitions and the primary key, the function must use the `=>` @@ -90,36 +61,8 @@ where you change your code, run `flutter pub run build_runner watch` instead. After running either command once, the drift generator will have created a class for your database and data classes for your entities. To use it, change the `MyDatabase` class as follows: -```dart -// These imports are only needed to open the database -import 'package:drift/native.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:path/path.dart' as p; -import 'package:drift/drift.dart'; -import 'dart:io'; -LazyDatabase _openConnection() { - // the LazyDatabase util lets us find the right location for the file async. - return LazyDatabase(() async { - // put the database file, called db.sqlite here, into the documents folder - // for your app. - final dbFolder = await getApplicationDocumentsDirectory(); - final file = File(p.join(dbFolder.path, 'db.sqlite')); - return NativeDatabase(file); - }); -} - -@DriftDatabase(tables: [Todos, Categories]) -class MyDatabase extends _$MyDatabase { - // we tell the database where to store the data with this constructor - MyDatabase() : super(_openConnection()); - - // you should bump this number whenever you change or add a table definition. Migrations - // are covered later in this readme. - @override - int get schemaVersion => 1; -} -``` +{% include "blocks/snippet" snippets = snippets name = "open" %} ## Next steps diff --git a/docs/pages/docs/Using SQL/drift_files.md b/docs/pages/docs/Using SQL/drift_files.md index e13e6986..105b97c8 100644 --- a/docs/pages/docs/Using SQL/drift_files.md +++ b/docs/pages/docs/Using SQL/drift_files.md @@ -11,52 +11,21 @@ aliases: template: layouts/docs/single --- +{% assign dart_snippets = "package:moor_documentation/snippets/drift_files/database.dart.excerpt.json" | readString | json_decode %} +{% assign drift_tables = "package:moor_documentation/snippets/drift_files/tables.drift.excerpt.json" | readString | json_decode %} +{% assign small = "package:moor_documentation/snippets/drift_files/small_snippets.drift.excerpt.json" | readString | json_decode %} + Drift files are a new feature that lets you write all your database code in SQL - drift will generate typesafe APIs for them. ## Getting started To use this feature, lets create two files: `database.dart` and `tables.drift`. The Dart file only contains the minimum code to setup the database: -```dart -import 'package:drift/drift.dart'; -import 'package:drift/native.dart'; -part 'database.g.dart'; - -@DriftDatabase( - include: {'tables.drift'}, -) -class MyDb extends _$MyDb { - // This example creates a simple in-memory database (without actual persistence). - // To actually store data, see the database setups from other "Getting started" guides. - MyDb() : super(NativeDatabase.memory()); - - @override - int get schemaVersion => 1; -} -``` +{% include "blocks/snippet" snippets = dart_snippets name = "overview" %} We can now declare tables and queries in the drift file: -```sql -CREATE TABLE todos ( - id INT NOT NULL PRIMARY KEY AUTOINCREMENT, - title TEXT NOT NULL, - content TEXT NOT NULL, - category INTEGER REFERENCES categories(id) -); -CREATE TABLE categories ( - id INT NOT NULL PRIMARY KEY AUTOINCREMENT, - description TEXT NOT NULL -) AS Category; -- the AS xyz after the table defines the data class name - --- 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: -createEntry: INSERT INTO todos (title, content) VALUES (:title, :content); -deleteById: DELETE FROM todos WHERE id = :id; -allTodos: SELECT * FROM todos; -``` +{% include "blocks/snippet" snippets = drift_tables %} After running the build runner with `flutter pub run build_runner build`, drift will write the `database.g.dart` @@ -92,22 +61,17 @@ 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 a variable. For those scenarios, you can also denote the explicit type of a variable: -```sql -myQuery(:variable AS TEXT): SELECT :variable; -``` + +{% include "blocks/snippet" snippets = small name = "q1" %} In addition to the base type, you can also declare that the type is nullable: -```sql -myQuery(:variable AS TEXT OR NULL): SELECT :variable; -``` +{% include "blocks/snippet" snippets = small name = "q2" %} Finally, you can declare that a variable should be required in Dart when using named parameters. To do so, add a `REQUIRED` keyword: -```sql -myQuery(REQUIRED :variable AS TEXT OR NULL): SELECT :variable; -``` +{% include "blocks/snippet" snippets = small name = "q3" %} Note that this only has an effect when the `named_parameters` [build option]({{ '../Advanced Features/builder_options.md' | pageUrl }}) is @@ -116,9 +80,9 @@ enabled. Further, non-nullable variables are required by default. ### Arrays If you want to check whether a value is in an array of values, you can use `IN ?`. That's not valid sql, but drift will desugar that at runtime. So, for this query: -```sql -entriesWithId: SELECT * FROM todos WHERE id IN ?; -``` + +{% include "blocks/snippet" snippets = small name = "entries" %} + Drift will generate a `Selectable entriesWithId(List ids)` method. Running `entriesWithId([1,2])` would generate `SELECT * ... id IN (?1, ?2)` and bind the arguments accordingly. To make sure this works as expected, drift @@ -142,9 +106,9 @@ written as an `INTEGER` column when the table gets created. ## Imports You can put import statements at the top of a `drift` file: -```sql -import 'other.drift'; -- single quotes are required for imports -``` + +{% include "blocks/snippet" snippets = small name = "import" %} + All tables reachable from the other file will then also be visible in the current file and to the database that `includes` it. If you want to declare queries on tables that were defined in another drift @@ -161,28 +125,13 @@ know from Dart. ## Nested results +{% assign nested = "package:moor_documentation/snippets/drift_files/nested.drift.excerpt.json" | readString | json_decode %} + Many queries fetch all columns from some table, typically by using the `SELECT table.*` syntax. That approach can become a bit tedious when applied over multiple tables from a join, as shown in this example: -```sql -CREATE TABLE coordinates ( - id INTEGER NOT NULL PRIMARY KEY, - lat REAL NOT NULL, - long REAL NOT NULL -); - -CREATE TABLE saved_routes ( - id INTEGER NOT NULL PRIMARY KEY, - name TEXT NOT NULL, - "from" INTEGER NOT NULL REFERENCES coordinates (id), - "to" INTEGER NOT NULL REFERENCES coordinates (id) -); - -routesWithPoints: SELECT r.id, r.name, f.*, t.* FROM routes r - INNER JOIN coordinates f ON f.id = r."from" - INNER JOIN coordinates t ON t.id = r."to"; -``` +{% include "blocks/snippet" snippets = nested name = "overview" %} To match the returned column names while avoiding name clashes in Dart, drift will generate a class having an `id`, `name`, `id1`, `lat`, `long`, `lat1` and @@ -190,11 +139,7 @@ a `long1` field. Of course, that's not helpful at all - was `lat1` coming from `from` or `to` again? Let's rewrite the query, this time using nested results: -```sql -routesWithNestedPoints: SELECT r.id, r.name, f.**, t.** FROM routes r - INNER JOIN coordinates f ON f.id = r."from" - INNER JOIN coordinates t ON t.id = r."to"; -``` +{% include "blocks/snippet" snippets = nested name = "nested" %} As you can see, we can nest a result simply by using the drift-specific `table.**` syntax. @@ -235,29 +180,13 @@ Re-using the `coordinates` and `saved_routes` tables introduced in the example for [nested results](#nested-results), we add a new table storing coordinates along a route: -```sql -CREATE TABLE route_points ( - route INTEGER NOT NULL REFERENCES saved_routes (id), - point INTEGER NOT NULL REFERENCES coordinates (id), - index_on_route INTEGER, - PRIMARY KEY (route, point) -); -``` +{% include "blocks/snippet" snippets = nested name = "route_points" %} Now, assume we wanted to query a route with information about all points along the way. While this requires two SQL statements, we can write this as a single drift query that is then split into the two statements automatically: -```sql -routeWithPoints: SELECT - route.**, - LIST(SELECT coordinates.* FROM route_points - INNER JOIN coordinates ON id = point - WHERE route = route.id - ORDER BY index_on_route - ) AS points - FROM saved_routes route; -``` +{% include "blocks/snippet" snippets = nested name = "list" %} This will generate a result set containing a `SavedRoute route` field along with a `List points` list of all points along the route. @@ -279,11 +208,9 @@ supported with the `new_sql_code_generation` [build option]({{ '../Advanced Feat Drift files work perfectly together with drift's existing Dart API: - you can write Dart queries for tables declared in a drift file: -```dart -Future insert(TodosCompanion companion) async { - await into(todos).insert(companion); -} -``` + +{% include "blocks/snippet" snippets = dart_snippets name = "dart_interop_insert" %} + - by importing Dart files into a drift file, you can write sql queries for tables declared in Dart. - generated methods for queries can be used in transactions, they work diff --git a/docs/pages/docs/platforms.md b/docs/pages/docs/platforms.md index 2ec48fdd..521f6ba2 100644 --- a/docs/pages/docs/platforms.md +++ b/docs/pages/docs/platforms.md @@ -104,27 +104,10 @@ install the dynamic library for `sqlite` next to your application executable. This example shows how to do that on Linux, by using a custom `sqlite3.so` that we assume lives next to your application: -```dart -import 'dart:ffi'; -import 'dart:io'; -import 'package:sqlite3/sqlite3.dart'; -import 'package:sqlite3/open.dart'; - -void main() { - open.overrideFor(OperatingSystem.linux, _openOnLinux); - - // After setting all the overrides, you can use drift! -} - -DynamicLibrary _openOnLinux() { - final scriptDir = File(Platform.script.toFilePath()).parent; - final libraryNextToScript = File('${scriptDir.path}/sqlite3.so'); - return DynamicLibrary.open(libraryNextToScript.path); -} -// _openOnWindows could be implemented similarly by opening `sqlite3.dll` -``` +{% assign snippets = 'package:moor_documentation/snippets/platforms.dart.excerpt.json' | readString | json_decode %} +{% include "blocks/snippet" snippets = snippets %} Be sure to use drift _after_ you set the platform-specific overrides. -When you usedrift in [another isolate]({{ 'Advanced Features/isolates.md' | pageUrl }}), +When you use drift in [another isolate]({{ 'Advanced Features/isolates.md' | pageUrl }}), you'll also need to apply the opening overrides on that background isolate. You can call them in the isolate's entrypoint before using any drift apis. diff --git a/docs/pubspec.yaml b/docs/pubspec.yaml index 9e766545..7597ad2d 100644 --- a/docs/pubspec.yaml +++ b/docs/pubspec.yaml @@ -20,6 +20,15 @@ dev_dependencies: linkcheck: ^2.0.19 shelf: ^1.2.0 shelf_static: ^1.1.0 + code_snippets: + hosted: https://simonbinder.eu + version: ^0.0.3 + + # Fake path_provider for snippets + path_provider: + path: assets/path_provider + # Used in examples + rxdart: ^0.27.3 moor: moor_generator: diff --git a/docs/templates/blocks/snippet.html b/docs/templates/blocks/snippet.html new file mode 100644 index 00000000..a8f4bcf5 --- /dev/null +++ b/docs/templates/blocks/snippet.html @@ -0,0 +1,4 @@ +{% assign excerpt = args.name | default: '(full)' %} +
+{{ args.snippets | get: excerpt }}
+
diff --git a/docs/templates/partials/changed_to_ffi.html b/docs/templates/partials/changed_to_ffi.html index 45aa4d5b..9129ae86 100644 --- a/docs/templates/partials/changed_to_ffi.html +++ b/docs/templates/partials/changed_to_ffi.html @@ -1,9 +1,9 @@ -Some versions of the Flutter tool create a broken `settings.gradle` on Android, which can cause problems with `moor_ffi`. +Some versions of the Flutter tool create a broken `settings.gradle` on Android, which can cause problems with `drift/native.dart`. If you get a "Failed to load dynamic library" exception, see [this comment](https://github.com/flutter/flutter/issues/55827#issuecomment-623779910). \ No newline at end of file diff --git a/docs/tool/snippets.dart b/docs/tool/snippets.dart new file mode 100644 index 00000000..b9ec844f --- /dev/null +++ b/docs/tool/snippets.dart @@ -0,0 +1,156 @@ +import 'package:build/build.dart'; +import 'package:code_snippets/builder.dart'; +import 'package:code_snippets/highlight.dart'; +import 'package:source_span/source_span.dart'; +import 'package:sqlparser/sqlparser.dart'; + +class SnippetsBuilder extends CodeExcerptBuilder { + // ignore: avoid_unused_constructor_parameters + SnippetsBuilder([BuilderOptions? options]); + + @override + bool shouldEmitFor(AssetId input, Excerpter excerpts) { + return true; + } + + @override + Future highlighterFor( + AssetId assetId, String content, BuildStep buildStep) async { + switch (assetId.extension) { + case '.drift': + return _DriftHighlighter( + SourceFile.fromString(content, url: assetId.uri)); + default: + return super.highlighterFor(assetId, content, buildStep); + } + } +} + +class _DriftHighlighter extends Highlighter { + _DriftHighlighter(SourceFile file) : super(file); + + @override + void highlight() { + final engine = SqlEngine( + EngineOptions( + useMoorExtensions: true, + version: SqliteVersion.current, + ), + ); + + final result = engine.parseMoorFile(file.span(0).text); + _HighlightingVisitor().visit(result.rootNode, this); + + for (final token in result.tokens) { + const ignoredKeyword = [ + TokenType.$null, + TokenType.$true, + TokenType.$false + ]; + + if (token is KeywordToken && + !ignoredKeyword.contains(token.type) && + !token.isIdentifier) { + reportSql(token, RegionType.keyword); + } else if (token is CommentToken) { + reportSql(token, RegionType.comment); + } else if (token is StringLiteralToken) { + reportSql(token, RegionType.string); + } + } + } + + void reportSql(SyntacticEntity? entity, RegionType type) { + if (entity != null) { + report(HighlightRegion( + type, file.span(entity.firstPosition, entity.lastPosition))); + } + } +} + +class _HighlightingVisitor extends RecursiveVisitor<_DriftHighlighter, void> { + @override + void visitCreateTriggerStatement( + CreateTriggerStatement e, _DriftHighlighter arg) { + arg.reportSql(e.triggerNameToken, RegionType.classTitle); + visitChildren(e, arg); + } + + @override + void visitCreateViewStatement(CreateViewStatement e, _DriftHighlighter arg) { + arg.reportSql(e.viewNameToken, RegionType.classTitle); + visitChildren(e, arg); + } + + @override + void visitColumnDefinition(ColumnDefinition e, _DriftHighlighter arg) { + arg + ..reportSql(e.nameToken, RegionType.variable) + ..reportSql(e.typeNames?.toSingleEntity, RegionType.type); + + visitChildren(e, arg); + } + + @override + void visitColumnConstraint(ColumnConstraint e, _DriftHighlighter arg) { + if (e is NotNull) { + arg.reportSql(e.$null, RegionType.keyword); + } else if (e is NullColumnConstraint) { + arg.reportSql(e.$null, RegionType.keyword); + } + super.visitColumnConstraint(e, arg); + } + + @override + void visitNullLiteral(NullLiteral e, _DriftHighlighter arg) { + arg.reportSql(e, RegionType.builtIn); + } + + @override + void visitNumericLiteral(Literal e, _DriftHighlighter arg) { + arg.reportSql(e, RegionType.number); + } + + @override + void visitMoorSpecificNode(MoorSpecificNode e, _DriftHighlighter arg) { + if (e is DeclaredStatement) { + final name = e.identifier; + if (name is SimpleName) { + arg.reportSql(name.identifier, RegionType.functionTitle); + } + } + super.visitMoorSpecificNode(e, arg); + } + + @override + void visitBooleanLiteral(BooleanLiteral e, _DriftHighlighter arg) { + arg.reportSql(e, RegionType.literal); + } + + @override + void visitReference(Reference e, _DriftHighlighter arg) { + arg.reportSql(e, RegionType.variable); + } + + @override + void visitTableReference(TableReference e, _DriftHighlighter arg) { + arg.reportSql(e.tableNameToken, RegionType.type); + } + + @override + void visitTableInducingStatement( + TableInducingStatement e, _DriftHighlighter arg) { + arg.reportSql(e.tableNameToken, RegionType.classTitle); + + if (e is CreateVirtualTableStatement) { + arg.reportSql(e.moduleNameToken, RegionType.invokedFunctionTitle); + } + + visitChildren(e, arg); + } + + @override + void visitVariable(Variable e, _DriftHighlighter arg) { + arg.reportSql(e, RegionType.variable); + } +} diff --git a/tool/generate_all.sh b/tool/generate_all.sh index ce3707b5..aff03c71 100755 --- a/tool/generate_all.sh +++ b/tool/generate_all.sh @@ -2,6 +2,8 @@ set -e echo "- Generate drift" (cd ../drift && dart pub get && dart run build_runner build --delete-conflicting-outputs) +echo "- Generate docs" +(cd ../docs && dart pub get && dart run build_runner build --delete-conflicting-outputs) echo "- Generate benchmarks" (cd ../extras/benchmarks && dart pub get && dart run build_runner build --delete-conflicting-outputs) echo "- Generate integration_tests/tests"