diff --git a/docs/builders/src/css_classes.dart b/docs/builders/src/css_classes.dart index 7acf2e975..42d137a7a 100644 --- a/docs/builders/src/css_classes.dart +++ b/docs/builders/src/css_classes.dart @@ -195,7 +195,7 @@ String lookupClassName(DynamicTextStyle cssClass) { if (className == null) { throw Exception( 'A class name was not found for a given style. ' - 'Please add the style to the styles map. ' + 'Please add the style to the styles map in the css_classes.dart file. ' 'Then you must add the class to the CSS file. ' 'Style: $cssClass', ); @@ -292,6 +292,17 @@ final styles = { fontStyle: null, fontWeight: null, decoration: null)): "SyntaxHighlight-16", + DynamicTextStyle( + lightStyle: TextStyle( + color: Color(4294901760), + fontStyle: null, + fontWeight: null, + decoration: null), + darkStyle: TextStyle( + color: Color(4294901760), + fontStyle: null, + fontWeight: null, + decoration: null)): "SyntaxHighlight-17" }; // Small script for generating the CSS file from the styles map. diff --git a/docs/docs/css/syntax_highlight.css b/docs/docs/css/syntax_highlight.css index 90a10cde1..c4ff3f2f2 100644 --- a/docs/docs/css/syntax_highlight.css +++ b/docs/docs/css/syntax_highlight.css @@ -46,12 +46,9 @@ .SyntaxHighlight-16 { color: #000088; } @media (prefers-color-scheme: dark) { .SyntaxHighlight-16 { color: #b9eeff; } } +.SyntaxHighlight-17 { color: #ff0000; } +@media (prefers-color-scheme: dark) { .SyntaxHighlight-17 { color: #ff0000; } } -.md-typeset__table table:not([class]) { - font-size: .74rem; -} - - diff --git a/docs/docs/queries.md b/docs/docs/queries.md index 76cbf99f5..57718a65b 100644 --- a/docs/docs/queries.md +++ b/docs/docs/queries.md @@ -1,13 +1,13 @@ --- -title: Queries +title: Query description: Create, read, update, and delete data in your database. --- -Drift makes it easy to write type-safe queries, ensuring that your database interactions are reliable and error-free. Additionally, Drift can watch for changes in your data, allowing your application to react in real-time to updates, inserts, and deletions. +## Overview -Drift offers 3 main ways to interact with your database: +Drift offers 3 way to write queries: 1. **Manager API**: A simple, high-level API for interacting with your database. The Manager API is the easiest way to get started with Drift, and is perfect for simple queries and updates. 2. **Core API**: A low-level API that allows you to write custom queries and interact with your database in a more fine-grained way. @@ -19,16 +19,18 @@ This page will cover the Manager API. For information on the Core API and Type-S

Manager API

Drift generates a manager for each table in your database. This manager provides a simple API for creating, reading, updating, and deleting data in your database. -It can be accessed via the `managers` property on the `Database` object. +It should be accessed via the `managers` property on the `Database` object. + +Example: {{ load_snippet('superhero_query','lib/snippets/schema.dart.excerpt.json') }} -If you don't plan to use the Manager API, you can disable it by setting `generate_manager: false` in your `build.yaml` file. -
+!!! note "Disabling the Manager API" + + If you don't plan to use the Manager API, you can disable it by setting `generate_manager: false` in your `build.yaml` file. -=== "`build.yaml`" - ```yaml + ```yaml title="build.yaml" targets: $default: builders: @@ -38,32 +40,22 @@ If you don't plan to use the Manager API, you can disable it by setting `generat # generate_manager: false ``` - -
- - ## Query Builder The Manager API provides a query builder that allows you to build complex queries using a fluent API. By chaining methods together, you can filter, sort, and paginate your data with ease. -Once you have built your query, you can execute it using the [`get`](#get-all-records), [`watch`](#get-all-records), [`update`](#update), [`delete`](#delete), [`count`](#count), or [`exists`](#exists) methods. +Once the query is built, you can execute it using the [`get`](#get-all-records), [`watch`](#get-all-records), [`update`](#update), [`delete`](#delete), [`count`](#count), and [`exists`](#exists) methods. ### Filter Use the `filter` method to filter records based on a condition. -In Dart, logical operators `&&` (AND) and `||` (OR) are commonly used within parentheses to combine multiple conditions for logical expressions. -```dart -if (condition1 && condition2) { - // do something -} -``` -Similarly, in Drift, an additional set of operators, `&` (AND) and `|` (OR), are available to combine conditions. +Use the `&` (AND) and `|` (OR), are available to combine conditions together. {{ load_snippet('filter','lib/snippets/queries.dart.excerpt.json') }} -To negate a condition, use the `not` method. +To negate a condition, use the `not` method after the `filter` method. {{ load_snippet('filter-not','lib/snippets/queries.dart.excerpt.json') }} @@ -76,7 +68,7 @@ The `&` operator is used to combine multiple sorting conditions. ### Limit -Performing queries that return a large number of records can be inefficient. +Performing queries that return many records at once can be inefficient. Use the `limit` and `offset` methods to paginate the results. {{ load_snippet('pagination','lib/snippets/queries.dart.excerpt.json') }} @@ -97,22 +89,19 @@ Records can be read/watched using the `get` and `watch` methods provided by the {{ load_snippet('retrieve_all','lib/snippets/queries.dart.excerpt.json') }} -### Get a single record +### Get Single Record -The following example demonstrates how to retrieve a single record: +To retrieve a single record, use the `getSingle`/ `watchSingle` method. {{ load_snippet('retrieve_single','lib/snippets/queries.dart.excerpt.json') }} -Drift provides helper methods for retrieving singletons: +This method will throw an exception if no records are found. If you want to handle this case gracefully, use the `getSingleOrNull`/ `watchSingleOrNull` method instead. -- `getSingle` - Retrieve a single record, throwing an exception if exactly one record is not found. -- `getSingleOrNull` - Retrieve a single record, throwing an exception if more than one record is found. -- `watchSingle` - Same as `getSingle`, but returns a stream of the record. -- `watchSingleOrNull` - Same as `getSingleOrNull`, but returns a stream of the record. +{{ load_snippet('retrieve_single_or_null','lib/snippets/queries.dart.excerpt.json') }} ### Distinct -When you perform complex queries that involve multiple tables, it is possible that duplicate records will be returned. +When performing complex queries, you may encounter duplicate records in the result set. To avoid this, set `distinct: true`. This will remove duplicates. @@ -126,7 +115,7 @@ Drift makes it easy to retrieve referenced fields from other tables using the `w {{ load_snippet('with-references-summary','lib/snippets/queries.dart.excerpt.json') }} -See the [Read References](referenced-queries.md) documentation for more information on referencing other tables. +See the [Read References](references.md#query-references) documentation for more information on referencing other tables. ## Update @@ -152,11 +141,6 @@ Use the `count` method to count the number of records in a table. {{ load_snippet('manager_count','lib/snippets/queries.dart.excerpt.json') }} -!!! note "Distinct" - When counting records, `distinct` is set to `true` by default. - This differs from the `get` and `watch` methods, where `distinct` is set to `false` by default. - See the [Distinct](#distinct) section for more information. - ## Exists To check if any records exist that match a certain condition, use the `exists` method. @@ -169,8 +153,7 @@ To create a new record, use the `create` method on the manager. This method will { .annotate } 1. If the primary key of the table is an auto-incrementing integer, the ID will be the value of the primary key. - If the primary key is anything else, Drift adds a column named `rowid` to the table and returns the value of that column. - + If the primary key is anything else, the `rowid` of the new record will be returned. {{ load_snippet('manager_simple_create_single','lib/snippets/queries.dart.excerpt.json') }} @@ -213,7 +196,6 @@ You can use the `create` method to perform an upsert operation. An upsert combin Note that this is a lower-level operation and is part of the Core API. For more details, refer to the [Core API](/docs/core) page. - ## Performance Considerations Database operations on a very large dataset can be time-consuming and may block the main thread, causing your application to become unresponsive. diff --git a/docs/docs/references.md b/docs/docs/references.md new file mode 100644 index 000000000..9ff63df68 --- /dev/null +++ b/docs/docs/references.md @@ -0,0 +1,153 @@ +--- + +title: Relationship +description: Define relationships between tables. + +--- + +## Overview + +A foreign key is a column in one table that refers to the primary key in another table. It establishes a link between two tables, creating a relationship between them. + +#### Example + +Consider the relationship between a `User` table and a `Group` table. +Each user belongs to a group, and each group can have multiple users. + +**Group Table** + +| id | group_name | +| --- | ---------- | +| 1 | Admin | +| 2 | User | + +**User Table** + +| id | user_name | group | +| --- | --------- | ----- | +| 1 | Alice | 1 | +| 2 | Bob | 2 | +| 3 | Carol | 2 | + +Here, the `group` column in the User table is a foreign key referencing `id` in the Group table. + +The following sections will show how to define these relationships using Drift. + +## Many-to-One Relationships + +To define a reference, use the `references` method in the table schema. +This method takes 2 arguments: + +- The table that the reference points to. +- The column in the other table that the reference points to. + +Here's how the `User` table from the example above could be defined: +
+{{ load_snippet('user_group_schema','lib/snippets/references.dart.excerpt.json') }} +
+1. This `@ReferenceName("users") `annotation is optional, It's used to name the reference in the generated code. If you don't provide it, one will be generated for you. +2. This `#id` syntax may look unfamiliar to you. It's a uncommonly used syntax in Dart called [Symbol literals](https://dart.dev/guides/language/language-tour#symbols). + +## Constraints + +By default, SQLite does not enforce foreign key constraints. +Meaning, if you were to create a user with a group that does not exist, SQLite would allow it. + +**This is not ideal for maintaining data integrity.** + +To enable foreign key constraints in your database, add the custom SQL statement `PRAGMA foreign_keys = ON;` to the `beforeOpen` callback of your database's migration as follows: + +{{ load_snippet('foreign_keys_on','lib/snippets/references.dart.excerpt.json') }} + +For more details on common pragmas and additional database configuration options, refer to the [Database]() documentation. + +#### Violating Constraints + +By default, if [foreign key constraints](#foreign-key-constraints) are enabled and a foreign key constraint is violated, an exception will be thrown. + +These constraints can be violated by: + +- Deleting a row in the referenced table. (e.g A group is deleted, but users still reference it.) + +- Updating the primary key in the referenced table. (e.g A group's ID is changed, but users still reference the old ID.) + +This behavior can be customized using the `onDelete` and `onUpdate` parameters in the `references` method. + +
+ +- **`KeyAction.cascade`**: The referenced rows are deleted or updated when the referenced row is deleted or updated. +- **`KeyAction.setDefault`** & **`KeyAction.setNull`**: The referenced column is set to a default value or `null` when the referenced row is deleted or updated. +- **`KeyAction.noAction`** (default) & **`KeyAction.restrict`**: An exception is thrown when the referenced row is deleted or updated. (1) + +
+ +1. The only difference between `KeyAction.noAction` and `KeyAction.restrict` is that deferred foreign key constraints will still be enforced mid-transaction with `KeyAction.restrict`. + +## Query References + +When fetching data from the database, you can use the `withReferences` method to fetch references along with the main table. + +{{ load_snippet('manager_references','lib/snippets/references.dart.excerpt.json') }} + +#### Prefetching references + +Drift provides a way to prefetch references in a single query to avoid inefficient queries. This is done by using the callback in the `withReferences` method. The referenced item will then be available in the referenced managers `prefetchedData` field. + +{{ load_snippet('manager_prefetch_references','lib/snippets/references.dart.excerpt.json') }} + +## Filtering and Ordering + +Filters may be applied from the many side of the relationship. For example, to find todos of a specific category: + +{{ load_snippet('manager_filter_forward_references','lib/snippets/references.dart.excerpt.json') }} + +And from the one side of the relationship. For example, to find the category of a specific todo: + +{{ load_snippet('manager_filter_back_references','lib/snippets/references.dart.excerpt.json') }} + +The same is true for ordering: + +{{ load_snippet('manager_order_forward_references','lib/snippets/references.dart.excerpt.json') }} + +!!! info "Filtering on Reference Columns" + + **If you have [foreign key constraints enabled](#constraints), filtering on reference columns works as expected.** + + However, without foreign key constraints, there are special considerations: + + 1. Filters on reference columns apply to the local column, not the referenced table. + 2. Example: `todos.filter((f) => f.category.id(1))` filters on the `category` column in `todos`, not `id` in `categories`. + 3. This doesn't verify if the referenced category actually exists. + + To ensure data integrity and expected behavior, always enable foreign key constraints. + +## Many-to-Many Relationships + +Drift does not support many-to-many relationships directly. Instead, you can create a junction table to represent the relationship. + +For example, consider a many-to-many relationship between `Books` and `Tags` tables. A book can have multiple tags, and a tag can be associated with multiple books. + +This relationship can be represented using a junction table `TagBookRelationship`: + +{{ load_snippet('many_to_many_schema','lib/snippets/references.dart.excerpt.json') }} + +You can now use this table to query books by tag or tags by book. + +{{ load_snippet('many_to_many_usage','lib/snippets/references.dart.excerpt.json') }} + + +## Advanced + +### Deferred Constraints + +By default, foreign key constraints are checked immediately. So the following code will throw an exception: + +{{ load_snippet('deferred_constraints','lib/snippets/references.dart.excerpt.json') }} + +Although an author with id `f7b3b3e0...` will exist at the end of the transaction, the foreign key constraint is checked immediately after the insert, causing an exception. + +To defer the constraint check until the end of the transaction, use the `initiallyDeferred` parameter in the `references` method: + +{{ load_snippet('define_deferred_constraints','lib/snippets/references.dart.excerpt.json') }} + +Now, the foreign key constraint will be checked at the end of the transaction, allowing the code to run without throwing an exception. \ No newline at end of file diff --git a/docs/docs/schema.md b/docs/docs/schema.md index bdb274603..318a9084a 100644 --- a/docs/docs/schema.md +++ b/docs/docs/schema.md @@ -31,7 +31,7 @@ In the following sections, we'll dive deeper into the various aspects of schema --- -## Tables +## Table In Drift, a table is represented by any class which extends the `Table` class. @@ -49,6 +49,20 @@ class Categories extends Table { !!! tip "Table Naming" Table classes should be named in plural form (e.g., `Superheros`, `Categories`). This convention generally leads to more appropriately named generated classes. For more information, see the [Naming](#naming) section. +### Primary Key + +Every table must have a primary key - a column (or set of columns) that uniquely identifies each row. + +For most tables, a single auto-incrementing column is sufficient. This column will automatically generate a unique value for each row. + +{{ load_snippet('pk-example','lib/snippets/schema.dart.excerpt.json') }} + +If a single auto-incrementing column is defined, Drift will automatically set it as the primary key. + +!!! warning "No Primary Key" + When using Drift with SQLite, always explicitly define a primary key for your tables. If you don't, SQLite automatically creates a hidden `rowid` column as the primary key, which can lead to unexpected behavior. To ensure consistency and avoid potential issues, it's best to define your own primary key rather than relying on this SQLite default behavior. + +See [Custom Primary Keys](#custom-primary-keys) for more information on defining custom primary keys. ## Columns @@ -79,7 +93,7 @@ The following table lists the built-in column types: In addition to these built-in types, you can also store custom types by converting them to one of these built-in types. See the [Custom Types](#custom-types) section for more information. -## Required +### Required By default, all columns are required. To make a column optional, use the `nullable()` method. @@ -89,7 +103,7 @@ Example: Now the `age` column is optional. If you try to insert a record without an `age`, it will be set to `null`. -## Defaults +### Defaults To set default values for your database fields, use the `clientDefault()` method. @@ -118,37 +132,7 @@ In the above example, the `isAdmin` field will default to `false` if no value is In most cases, you should use `clientDefault`. It's more flexible and doesn't require you to migrate the database when changing the default value. Drift includes `withDefault` for SQL database compatibility, but its practical use cases are limited. -## Primary Keys - -Every table in a relational database needs a primary key - a column (or set of columns) that uniquely identifies each row. - -This is the recommended way to define a primary key for a table: - -```dart -class Superheros extends Table { - late final id = integer().autoIncrement()(); - // other columns... -} -``` - -When you use define a single `integer().autoIncrement()()` column on a table, Drift automatically sets this column as the primary key. You don't need to do anything else. - -!!! tip "Reusable Mixin" - In fact, the above column definition is so common that Drift provides a mixin to make it easier. You can use the `PrimaryKey` - - {{ load_snippet('base_pk_class','lib/snippets/schema.dart.excerpt.json') }} - -### Custom Primary Keys - -If you want to use a different column (or set of columns) as the primary key, you can override the `primaryKey` getter in your table class: - -{{ load_snippet('custom_pk','lib/snippets/schema.dart.excerpt.json') }} - -In this example, the `email` column is set as the primary key. - - - -## Unique Columns +### Unique Columns To ensure that a column can only contain unique values, use the `unique` method. @@ -167,7 +151,7 @@ For example, in a restaurant management app, you might want to ensure that a tab Now if we created a record with the same time and the same table, an exception will be thrown. -## Custom Types +### Custom Types Any Dart type can be stored in the database by converting it to one of the built-in types. @@ -195,7 +179,7 @@ Now we can use the `Duration` type as if it were a built-in type. -## Enums +### Enums Drift provides support for storing Dart enums in your database. Enums can be stored either as integers (using their index) or as strings (using their name). @@ -208,15 +192,7 @@ Drift provides support for storing Dart enums in your database. Enums can be sto 2. **Renaming Enum Values**: If you use `textEnum`, renaming an enum value will make it impossible to read existing data for that value. - 3. **Adding New Values**: Adding new enum values (especially in the middle of the enum) can cause issues with existing data or queries that assume a certain set of values. - - - - - - - -## `DateTime` Columns +### `DateTime` Drift handles most of the complexity of working with `DateTime` objects for you. You can use `DateTime` objects directly in your Dart code, and Drift will take care of converting them to the correct format for the database. @@ -230,7 +206,7 @@ Under the hood, Drift can store `DateTime` objects in one of two ways: By default, Drift stores `DateTime` objects as Unix timestamps for backward compatibility reasons. However, we recommend using ISO-8601 strings for new projects. To enable this, set the `store_date_time_values_as_text` option in your `build.yaml` file. -```yaml +```yaml title="build.yaml" targets: $default: builders: @@ -243,7 +219,7 @@ targets: ## Naming -Drift generates quite a bit of SQL and Dart code for you. This section will help you customize the names of tables and columns in the database. +Drift generates a significant amount of SQL and Dart code automatically. This section guides you on how to customize the names of tables and columns in your database. ### Data Class Name @@ -266,7 +242,7 @@ If you want to customize the key in the JSON map, use the `@JsonKey` decorator. Drift also has an option to use the column name as the key in the JSON map. To enable this, set the `use_column_name_as_json_key` option in your `build.yaml` file. -```yaml +```yaml title="build.yaml" targets: $default: builders: @@ -291,7 +267,7 @@ To customize the name of the table in SQL, override the `tableName` getter in yo You can also change what "case" is used by settings a generator option in your `build.yaml` file. -```yaml +```yaml title="build.yaml" targets: $default: builders: @@ -354,6 +330,15 @@ If any record is inserted with an `age` less than 0, an exception will be thrown Keep in mind that this check is run in the database, so if you change this check you will need to migrate the database. +### Custom Primary Keys + +If you want to use a different column (or set of columns) as the primary key, you can override the `primaryKey` getter in your table class: + +{{ load_snippet('custom_pk','lib/snippets/schema.dart.excerpt.json') }} + +In this example, the `email` column is set as the primary key. + ### `BigInt` Columns Use the standard `int` type for storing integers as it is efficient for typical values. Only use `BigInt` for extremely large numbers when compiling to JavaScript, as it ensures accuracy but has a performance cost. For more details, refer to the dart-lang [documentation](https://dart.dev/guides/language/numbers#what-should-you-do). + diff --git a/docs/lib/snippets/queries.dart b/docs/lib/snippets/queries.dart index d68f4316b..124256846 100644 --- a/docs/lib/snippets/queries.dart +++ b/docs/lib/snippets/queries.dart @@ -175,6 +175,11 @@ void examples() { await db.managers.todoItems.filter((f) => f.id(1)).getSingle(); // #enddocregion retrieve_single + // #docregion retrieve_single_or_null + // Retrieve the item with an id of 1 or null if it doesn't exist + await db.managers.todoItems.filter((f) => f.id(1)).getSingleOrNull(); + // #enddocregion retrieve_single_or_null + // #docregion retrieve_first // Retrieve the first item, or null if there are no items await db.managers.todoItems.limit(1).getSingleOrNull(); diff --git a/docs/lib/snippets/references.dart b/docs/lib/snippets/references.dart new file mode 100644 index 000000000..2f14ec62e --- /dev/null +++ b/docs/lib/snippets/references.dart @@ -0,0 +1,325 @@ +// ignore_for_file: unused_local_variable, unused_element + +import 'package:drift/drift.dart'; + +import 'package:drift_flutter/drift_flutter.dart'; +import 'package:uuid/uuid.dart'; +part 'references.g.dart'; + +// #docregion user_group_schema +class Group extends Table { + late final id = integer().autoIncrement()(); + late final name = text()(); +} + +class User extends Table { + late final id = integer().autoIncrement()(); + late final name = text().unique()(); + + @ReferenceName("users") //(1)! + late final group = integer().references(Group, #id /*(2)!*/)(); +} +// #enddocregion user_group_schema + +class Author extends Table { + late final uuid = text().clientDefault(() => Uuid().v4())(); + late final name = text()(); + + @override + Set get primaryKey => {uuid}; +} + +// #docregion define_deferred_constraints +class Post extends Table { + late final id = integer().autoIncrement()(); + late final author = + text().references(Author, #uuid, initiallyDeferred: true)(); + late final content = text()(); +} +// #enddocregion define_deferred_constraints + +// #docregion many_to_many_schema +class Books extends Table { + late final id = integer().autoIncrement()(); + late final title = text()(); +} + +class Tags extends Table { + late final id = integer().autoIncrement()(); + late final name = text()(); +} + +class TagBookRelationship extends Table { + late final book = integer().references(Books, #id)(); + late final tag = integer().references(Tags, #id)(); + + @override + Set get primaryKey => {book, tag}; +} +// #enddocregion many_to_many_schema + +@DriftDatabase( + tables: [User, Group, Author, Post, Books, Tags, TagBookRelationship]) +// #docregion foreign_keys_on +class AppDatabase extends _$AppDatabase { + AppDatabase(super.e); + + @override + int get schemaVersion => 1; + + @override + MigrationStrategy get migration { + return MigrationStrategy( + beforeOpen: (details) async { + await customStatement('pragma foreign_keys = ON;'); + }, + ); + } +} +// #enddocregion foreign_keys_on + +class User2 extends Table { + // #docregion key_action_cascade + @ReferenceName("users") //(1)! + late final group = + integer().references(Group, #id, onDelete: KeyAction.setNull)(); + // #enddocregion key_action_cascade +} + +Future main() async { + // #docregion user_group_schema_usage + // Initialize the database + final db = AppDatabase(driftDatabase(name: 'app.db')); + + // Create the teacher and student groups + final teacherGroup = + await db.managers.group.createReturning((o) => o(name: "Teachers")); + final studentGroup = + await db.managers.group.createReturning((o) => o(name: "Students")); + + // Create some users + await db.managers.user + .createReturning((o) => o(name: "Alice", group: teacherGroup.id)); + await db.managers.user + .createReturning((o) => o(name: "Clark", group: teacherGroup.id)); + await db.managers.user + .createReturning((o) => o(name: "Bob", group: studentGroup.id)); + await db.managers.user + .createReturning((o) => o(name: "David", group: studentGroup.id)); + await db.managers.user + .createReturning((o) => o(name: "Simon", group: studentGroup.id)); + + // Query all users in the teacher group + final teachers = + await db.managers.user.filter((f) => f.group.id(teacherGroup.id)).get(); + + // Get the group which Alice belongs to + final alicesGroup = await db.managers.group + .filter((f) => f.users((f) => f.name("Alice"))) + .get(); + // #enddocregion user_group_schema_usage + + // #docregion deferred_constraints + await db.transaction(() async { + final authorId = "f7b3b3e0-4b7b-11ec-8d3d-0242ac130003"; + final post = await db.managers.post + .createReturning((o) => o(author: authorId, content: "Lorem ipsum...")); + final author = await db.managers.author + .createReturning((o) => o(name: "Alice", uuid: Value(authorId))); + }); + + // #enddocregion deferred_constraints + + db.transaction( + () async { + // #docregion many_to_many_usage + // Create some books + final harryPotter = await db.managers.books + .createReturning((o) => o(title: "Harry Potter")); + final clifforTheBigRedDog = await db.managers.books + .createReturning((o) => o(title: "Clifford The Big Red Dog")); + + // Create some tags + final magic = + await db.managers.tags.createReturning((o) => o(name: "Magic")); + final scienceFiction = await db.managers.tags + .createReturning((o) => o(name: "Science Fiction")); + final kids = + await db.managers.tags.createReturning((o) => o(name: "Kids")); + final friendship = + await db.managers.tags.createReturning((o) => o(name: "Friendship")); + + // Assign tags to the book + await db.managers.tagBookRelationship.bulkCreate((o) => [ + o(book: harryPotter.id, tag: magic.id), + o(book: harryPotter.id, tag: scienceFiction.id), + o(book: harryPotter.id, tag: friendship.id), + o(book: clifforTheBigRedDog.id, tag: kids.id), + o(book: clifforTheBigRedDog.id, tag: friendship.id), + ]); + + // Query all books with the tag "Magic" + final magicBooks = await db.managers.tagBookRelationship + .filter((f) => f.tag.id(magic.id)) + .withReferences((prefetch) => prefetch(book: true, tag: true)) + .get(); + + // Print the title of the books and the tag name + for (final (_, refs) in magicBooks) { + final book = refs.book!.prefetchedData!.single; + final tag = refs.tag!.prefetchedData!.single; + print("${book.title} is tagged as ${tag.name}"); + } + // #enddocregion many_to_many_usage + }, + ); +} + +class TodoItems 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().references(TodoCategory, #id)(); + DateTimeColumn get createdAt => dateTime().nullable()(); +} + +class TodoCategory extends Table { + IntColumn get id => integer().autoIncrement()(); + TextColumn get description => text()(); + IntColumn get user => integer().nullable().references(Users, #id)(); +} + +// #docregion user_group_tables +class Users extends Table { + IntColumn get id => integer().autoIncrement()(); + TextColumn get name => text()(); +} + +class Groups extends Table { + IntColumn get id => integer().autoIncrement()(); + TextColumn get name => text()(); + @ReferenceName("administeredGroups") + IntColumn get admin => integer().nullable().references(Users, #id)(); + @ReferenceName("ownedGroups") + IntColumn get owner => integer().references(Users, #id)(); +} + +// #enddocregion user_group_tables + +@DriftDatabase(tables: [TodoItems, TodoCategory, Groups, Users]) +class Database extends _$Database { + Database(super.e); + @override + int get schemaVersion => 1; +} + +extension ManagerExamples on Database { +// #docregion manager_filter_forward_references + Future relationalFilter() async { + // Get all items with a category description of "School" + managers.todoItems.filter((f) => f.category.description("School")); + + // These can be combined with other filters + // For example, get all items with a title of "Title" or a category description of "School" + await managers.todoItems + .filter( + (f) => f.title("Title") | f.category.description("School"), + ) + .exists(); + } +// #enddocregion manager_filter_forward_references + + Future relationalOrder() async { + // #docregion manager_order_forward_references + // First order by category description, then by title + managers.todoItems.orderBy( + (f) => f.category.description.asc() & f.title.asc(), + ); + // #enddocregion manager_order_forward_references + } + +// #docregion manager_filter_back_references + Future reverseRelationalFilter() async { + // Get the category that has a todo item with an id of 1 + managers.todoCategory.filter((f) => f.todoItemsRefs((f) => f.id(1))); + + // These can be combined with other filters + // For example, get all categories with a description of "School" or a todo item with an id of 1 + managers.todoCategory.filter( + (f) => f.description("School") | f.todoItemsRefs((f) => f.id(1)), + ); + } +// #enddocregion manager_filter_back_references + +// #docregion manager_filter_custom_back_references + Future reverseNamedRelationalFilter() async { + // Get all users who are administrators of a group with a name containing "Business" + // or who own a group with an id of 1, 2, 4, or 5 + managers.users.filter( + (f) => + f.administeredGroups((f) => f.name.contains("Business")) | + f.ownedGroups((f) => f.id.isIn([1, 2, 4, 5])), + ); + } +// #enddocregion manager_filter_custom_back_references + + Future references() async { + // #docregion manager_references + /// Get each todo, along with a its categories + final todosWithRefs = await managers.todoItems.withReferences().get(); + for (final (todo, refs) in todosWithRefs) { + final category = await refs.category?.getSingle(); + } + + /// This also works in the reverse + final categoriesWithRefs = + await managers.todoCategory.withReferences().get(); + for (final (category, refs) in categoriesWithRefs) { + final todos = await refs.todoItemsRefs.get(); + } + // #enddocregion manager_references + } + + Future referencesPrefetch() async { + // #docregion manager_prefetch_references + /// Get each todo, along with a its categories + final categoriesWithReferences = await managers.todoItems + .withReferences( + (prefetch) => prefetch(category: true), + ) + .get(); + for (final (todo, refs) in categoriesWithReferences) { + final category = refs.category?.prefetchedData?.firstOrNull; + } + + /// This also works in the reverse + final todosWithRefs = await managers.todoCategory + .withReferences((prefetch) => prefetch(todoItemsRefs: true)) + .get(); + for (final (category, refs) in todosWithRefs) { + final todos = refs.todoItemsRefs.prefetchedData; + } + // #enddocregion manager_prefetch_references + } + + Future referencesPrefetchStream() async { +// #docregion manager_prefetch_references_stream + /// Get each todo, along with a its categories + managers.todoCategory + .withReferences((prefetch) => prefetch(todoItemsRefs: true, user: true)) + .watch() + .listen( + (catWithRefs) { + for (final (cat, refs) in catWithRefs) { + // Updates to the user table will trigger a query + final users = refs.user?.prefetchedData; + + // However, updates to the TodoItems table will not trigger a query + final todos = refs.todoItemsRefs.prefetchedData; + } + }, + ); +// #enddocregion manager_prefetch_references_stream + } +} diff --git a/docs/lib/snippets/schema.dart b/docs/lib/snippets/schema.dart index 09b43c524..9bf811669 100644 --- a/docs/lib/snippets/schema.dart +++ b/docs/lib/snippets/schema.dart @@ -304,3 +304,9 @@ class Student extends Table { integer().nullable().check(age.isBiggerOrEqualValue(0))(); } // #enddocregion custom-check + +// #docregion pk-example +class Item extends Table { + late final id = integer().autoIncrement()(); + // More columns... +} diff --git a/docs/mkdocs/Dockerfile b/docs/mkdocs/Dockerfile index 5fcb5a529..5c092f7bf 100644 --- a/docs/mkdocs/Dockerfile +++ b/docs/mkdocs/Dockerfile @@ -3,4 +3,4 @@ FROM squidfunk/mkdocs-material:9 RUN apk add --no-cache g++ RUN apk add --no-cache gcc RUN pip install mkdocs-macros-plugin -RUN pip install mkdocs-git-revision-date-localized-plugin +RUN pip install mkdocs-git-revision-date-localized-plugin \ No newline at end of file diff --git a/docs/mkdocs/mkdocs.yml b/docs/mkdocs/mkdocs.yml index 0fc22d894..caf968685 100644 --- a/docs/mkdocs/mkdocs.yml +++ b/docs/mkdocs/mkdocs.yml @@ -65,6 +65,7 @@ plugins: - macros markdown_extensions: + - pymdownx.tilde - admonition - pymdownx.details - attr_list @@ -78,7 +79,11 @@ markdown_extensions: anchor_linenums: true use_pygments: true - pymdownx.betterem - - pymdownx.superfences + - pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_code_format - pymdownx.inlinehilite - pymdownx.tabbed: alternate_style: true @@ -154,6 +159,7 @@ nav: - New Docs: - schema.md - queries.md + - references.md docs_dir: "../docs"