From 2403da5b980f8fba8e0cb1085baf8ed72afc6766 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Sat, 16 Sep 2023 18:38:22 +0200 Subject: [PATCH] Write new page on dart tables --- .../datetime_conversion.dart | 0 docs/lib/snippets/dart_api/old_name.dart | 5 + docs/lib/snippets/dart_api/tables.dart | 80 ++++ docs/lib/snippets/setup/database.dart | 2 + docs/pages/docs/Dart API/daos.md | 4 +- docs/pages/docs/Dart API/tables.md | 368 +++++++++++++++++- .../Getting started/advanced_dart_tables.md | 338 ---------------- docs/test/snippet_test.dart | 2 +- drift/lib/src/dsl/table.dart | 2 +- 9 files changed, 459 insertions(+), 342 deletions(-) rename docs/lib/snippets/{migrations => dart_api}/datetime_conversion.dart (100%) create mode 100644 docs/lib/snippets/dart_api/old_name.dart create mode 100644 docs/lib/snippets/dart_api/tables.dart diff --git a/docs/lib/snippets/migrations/datetime_conversion.dart b/docs/lib/snippets/dart_api/datetime_conversion.dart similarity index 100% rename from docs/lib/snippets/migrations/datetime_conversion.dart rename to docs/lib/snippets/dart_api/datetime_conversion.dart diff --git a/docs/lib/snippets/dart_api/old_name.dart b/docs/lib/snippets/dart_api/old_name.dart new file mode 100644 index 00000000..a078ce27 --- /dev/null +++ b/docs/lib/snippets/dart_api/old_name.dart @@ -0,0 +1,5 @@ +import 'package:drift/drift.dart'; + +class EnabledCategories extends Table { + IntColumn get parentCategory => integer()(); +} diff --git a/docs/lib/snippets/dart_api/tables.dart b/docs/lib/snippets/dart_api/tables.dart new file mode 100644 index 00000000..c145748a --- /dev/null +++ b/docs/lib/snippets/dart_api/tables.dart @@ -0,0 +1,80 @@ +import 'package:drift/drift.dart'; + +// #docregion nnbd +class Items extends Table { + IntColumn get category => integer().nullable()(); + // ... +} +// #enddocregion nnbd + +// #docregion names +@DataClassName('EnabledCategory') +class EnabledCategories extends Table { + @override + String get tableName => 'categories'; + + @JsonKey('parent_id') + IntColumn get parentCategory => integer().named('parent')(); +} +// #enddocregion names + +// #docregion references +class TodoItems extends Table { + // ... + IntColumn get category => + integer().nullable().references(TodoCategories, #id)(); +} + +@DataClassName("Category") +class TodoCategories extends Table { + IntColumn get id => integer().autoIncrement()(); + // and more columns... +} +// #enddocregion references + +// #docregion unique-column +class TableWithUniqueColumn extends Table { + IntColumn get unique => integer().unique()(); +} +// #enddocregion unique-column + +// #docregion primary-key +class GroupMemberships extends Table { + IntColumn get group => integer()(); + IntColumn get user => integer()(); + + @override + Set get primaryKey => {group, user}; +} +// #enddocregion primary-key + +// #docregion unique-table +class IngredientInRecipes extends Table { + @override + List> get uniqueKeys => [ + {recipe, ingredient}, + {recipe, amountInGrams} + ]; + + IntColumn get recipe => integer()(); + IntColumn get ingredient => integer()(); + + IntColumn get amountInGrams => integer().named('amount')(); +} +// #enddocregion unique-table + +// #docregion custom-constraint-table +class TableWithCustomConstraints extends Table { + IntColumn get foo => integer()(); + IntColumn get bar => integer()(); + + @override + List get customConstraints => [ + 'FOREIGN KEY (foo, bar) REFERENCES group_memberships ("group", user)', + ]; +} +// #enddocregion custom-constraint-table + +// #docregion index +class Users extends Table {} +// #enddocregion index \ No newline at end of file diff --git a/docs/lib/snippets/setup/database.dart b/docs/lib/snippets/setup/database.dart index c7a94a16..03908ae1 100644 --- a/docs/lib/snippets/setup/database.dart +++ b/docs/lib/snippets/setup/database.dart @@ -17,12 +17,14 @@ import 'package:path/path.dart' as p; // #docregion before_generation part 'database.g.dart'; +// #docregion table 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()(); } +// #enddocregion table // #docregion open @DriftDatabase(tables: [TodoItems]) diff --git a/docs/pages/docs/Dart API/daos.md b/docs/pages/docs/Dart API/daos.md index c24818e7..d20d9ce2 100644 --- a/docs/pages/docs/Dart API/daos.md +++ b/docs/pages/docs/Dart API/daos.md @@ -9,8 +9,9 @@ template: layouts/docs/single --- When you have a lot of queries, putting them all into one class might become -tedious. You can avoid this by extracting some queries into classes that are +tedious. You can avoid this by extracting some queries into classes that are available from your main database class. Consider the following code: + ```dart part 'todos_dao.g.dart'; @@ -33,5 +34,6 @@ class TodosDao extends DatabaseAccessor with _$TodosDaoMixin { } } ``` + If we now change the annotation on the `MyDatabase` class to `@DriftDatabase(tables: [Todos, Categories], daos: [TodosDao])` and re-run the code generation, a generated getter `todosDao` can be used to access the instance of that dao. diff --git a/docs/pages/docs/Dart API/tables.md b/docs/pages/docs/Dart API/tables.md index fa615d6b..f873f8ae 100644 --- a/docs/pages/docs/Dart API/tables.md +++ b/docs/pages/docs/Dart API/tables.md @@ -7,4 +7,370 @@ template: layouts/docs/single path: /docs/getting-started/advanced_dart_tables/ --- -In relational databases, +{% assign snippets = 'package:drift_docs/snippets/dart_api/tables.dart.excerpt.json' | readString | json_decode %} +{% assign setup = 'package:drift_docs/snippets/setup/database.dart.excerpt.json' | readString | json_decode %} + +In relational databases, tables are used to describe the structure of rows. By +adhering to a predefined schema, drift can generate typesafe code for your +database. +As already shown in the [setup]({{ '../setup.md#database-class' | pageUrl }}) +page, drift provides APIs to declare tables in Dart: + +{% include "blocks/snippet" snippets = setup name = 'table' %} + +This page describes the DSL for tables in more detail. + +## Columns + +In each table, you define columns by declaring a getter starting with the type of the column, +its name in Dart, and the definition mapped to SQL. +In the example above, `IntColumn get category => integer().nullable()();` defines a column +holding nullable integer values named `category`. +This section describes all the options available when declaring columns. + +## Supported column types + +Drift supports a variety of column types out of the box. You can store custom classes in columns by using +[type converters]({{ "../Advanced Features/type_converters.md" | pageUrl }}). + +| Dart type | Column | Corresponding SQLite type | +|--------------|---------------|-----------------------------------------------------| +| `int` | `integer()` | `INTEGER` | +| `BigInt` | `int64()` | `INTEGER` (useful for large values on the web) | +| `double` | `real()` | `REAL` | +| `boolean` | `boolean()` | `INTEGER`, which a `CHECK` to only allow `0` or `1` | +| `String` | `text()` | `TEXT` | +| `DateTime` | `dateTime()` | `INTEGER` (default) or `TEXT` depending on [options](#datetime-options) | +| `Uint8List` | `blob()` | `BLOB` | +| `Enum` | `intEnum()` | `INTEGER` (more information available [here]({{ "../Advanced Features/type_converters.md#implicit-enum-converters" | pageUrl }})). | +| `Enum` | `textEnum()` | `TEXT` (more information available [here]({{ "../Advanced Features/type_converters.md#implicit-enum-converters" | pageUrl }})). | + +Note that the mapping for `boolean`, `dateTime` and type converters only applies when storing records in +the database. +They don't affect JSON serialization at all. For instance, `boolean` values are expected as `true` or `false` +in the `fromJson` factory, even though they would be saved as `0` or `1` in the database. +If you want a custom mapping for JSON, you need to provide your own [`ValueSerializer`](https://pub.dev/documentation/drift/latest/drift/ValueSerializer-class.html). + +### `BigInt` support + +Drift supports the `int64()` column builder to indicate that a column stores +large integers and should be mapped to Dart as a `BigInt`. + +This is mainly useful for Dart apps compiled to JavaScript, where an `int` +really is a `double` that can't store large integers without loosing information. +Here, representing integers as `BigInt` (and passing those to the underlying +database implementation) ensures that you can store large intergers without any +loss of precision. +Be aware that `BigInt`s have a higher overhead than `int`s, so we recommend using +`int64()` only for columns where this is necessary: + +{% block "blocks/alert" title="You might not need this!" color="info" %} +In sqlite3, an `INTEGER` column is stored as a 64-bit integer. +For apps running in the Dart VM (e.g. on everything except for the web), the `int` +type in Dart is the _perfect_ match for that since it's also a 64-bit int. +For those apps, we recommend using the regular `integer()` column builder. + +Essentially, you should use `int64()` if both of these are true: + +- you're building an app that needs to work on the web, _and_ +- the column in question may store values larger than 252. + +In all other cases, using a regular `integer()` column is more efficient. +{% endblock %} + +Here are some more pointers on using `BigInt`s in drift: + +- Since an `integer()` and a `int64()` is the same column in sqlite3, you can + switch between the two without writing a schema migration. +- In addition to large columns, it may also be that you have a complex expression + in a select query that would be better represented as a `BigInt`. You can use + `dartCast()` for this: For an expression + `(table.columnA * table.columnB).dartCast()`, drift will report the + resulting value as a `BigInt` even if `columnA` and `columnB` were defined + as regular integers. +- `BigInt`s are not currently supported by `moor_flutter` and `drift_sqflite`. +- To use `BigInt` support on a `WebDatabase`, set the `readIntsAsBigInt: true` + flag when instantiating it. +- Both `NativeDatabase` and `WasmDatabase` have builtin support for bigints. + +### `DateTime` options + +Drift supports two approaches of storing `DateTime` values in SQL: + +1. __As unix timestamp__ (the default): In this mode, drift stores date time + values as an SQL `INTEGER` containing the unix timestamp (in seconds). + When date times are mapped from SQL back to Dart, drift always returns a + non-UTC value. So even when UTC date times are stored, this information is + lost when retrieving rows. +2. __As ISO 8601 string__: In this mode, datetime values are stored in a + textual format based on `DateTime.toIso8601String()`: UTC values are stored + unchanged (e.g. `2022-07-25 09:28:42.015Z`), while local values have their + UTC offset appended (e.g. `2022-07-25T11:28:42.015 +02:00`). + Most of sqlite3's date and time functions operate on UTC values, but parsing + datetimes in SQL respects the UTC offset added to the value. + + When reading values back from the database, drift will use `DateTime.parse` + as following: + - If the textual value ends with `Z`, drift will use `DateTime.parse` + directly. The `Z` suffix will be recognized and a UTC value is returned. + - If the textual value ends with a UTC offset (e.g. `+02:00`), drift first + uses `DateTime.parse` which respects the modifier but returns a UTC + datetime. Drift then calls `toLocal()` on this intermediate result to + return a local value. + - If the textual value neither has a `Z` suffix nor a UTC offset, drift + will parse it as if it had a `Z` modifier, returning a UTC datetime. + The motivation for this is that the `datetime` function in sqlite3 returns + values in this format and uses UTC by default. + + This behavior works well with the date functions in sqlite3 while also + preserving "UTC-ness" for stored values. + +The mode can be changed with the `store_date_time_values_as_text` [build option]({{ '../Advanced Features/builder_options.md' | pageUrl }}). + +Regardless of the option used, drift's builtin support for +[date and time functions]({{ '../Advanced Features/expressions.md#date-and-time' | pageUrl }}) +return an equivalent values. Drift internally inserts the `unixepoch` +[modifier](https://sqlite.org/lang_datefunc.html#modifiers) when unix timestamps +are used to make the date functions work. When comparing dates stored as text, +drift will compare their `julianday` values behind the scenes. + +#### Migrating between the two modes + +While making drift change the date time modes is as simple as changing a build +option, toggling this behavior is not compatible with existing database schemas: + +1. Depending on the build option, drift expects strings or integers for datetime + values. So you need to migrate stored columns to the new format when changing + the option. +2. If you are using SQL statements defined in `.drift` files, use custom SQL + at runtime or manually invoke datetime expressions with a direct + `FunctionCallExpression` instead of using the higher-level date time APIs, you + may have to adapt those usages. + + For instance, comparison operators like `<` work on unix timestamps, but they + will compare textual datetime values lexicographically. So depending on the + mode used, you will have to wrap the value in `unixepoch` or `julianday` to + make them comparable. + +As the second point is specific to usages in your app, this documentation only +describes how to migrate stored columns between the format: + +{% assign conversion = "package:drift_docs/snippets/dart_api/datetime_conversion.dart.excerpt.json" | readString | json_decode %} + +Note that the JSON serialization generated by default is not affected by the +datetime mode chosen. By default, drift will serialize `DateTime` values to a +unix timestamp in milliseconds. You can change this by creating a +`ValueSerializer.defaults(serializeDateTimeValuesAsString: true)` and assigning +it to `driftRuntimeOptions.defaultSerializer`. + +##### Migrating from unix timestamps to text + +To migrate from using timestamps (the default option) to storing datetimes as +text, follow these steps: + +1. Enable the `store_date_time_values_as_text` build option. +2. Add the following method (or an adaption of it suiting your needs) to your + database class. +3. Increment the `schemaVersion` in your database class. +4. Write a migration step in `onUpgrade` that calls + `migrateFromUnixTimestampsToText` for this schema version increase. + __Remember that triggers, views or other custom SQL entries in your database + will require a custom migration that is not covered by this guide.__ + +{% include "blocks/snippet" snippets = conversion name = "unix-to-text" %} + +##### Migrating from text to unix timestamps + +To migrate from datetimes stored as text back to unix timestamps, follow these +steps: + +1. Disable the `store_date_time_values_as_text` build option. +2. Add the following method (or an adaption of it suiting your needs) to your + database class. +3. Increment the `schemaVersion` in your database class. +4. Write a migration step in `onUpgrade` that calls + `migrateFromTextDateTimesToUnixTimestamps` for this schema version increase. + __Remember that triggers, views or other custom SQL entries in your database + will require a custom migration that is not covered by this guide.__ + +{% include "blocks/snippet" snippets = conversion name = "text-to-unix" %} + +Note that this snippet uses the `unixepoch` sqlite3 function, which has been +added in sqlite 3.38. To support older sqlite3 versions, you can use `strftime` +and cast to an integer instead: + +{% include "blocks/snippet" snippets = conversion name = "text-to-unix-old" %} + +When using a `NativeDatabase` with a recent dependency on the +`sqlite3_flutter_libs` package, you can safely assume that you are on a recent +sqlite3 version with support for `unixepoch`. + +### Nullability + +Drift follows Dart's idiom of non-nullable by default types. This means that +columns declared on a table defined in Dart can't store null values by default, +they are generated with a `NOT NULL` constraint in SQL. +When you forget to set a value in an insert, an exception will be thrown. +When using sql, drift also warns about that at compile time. + +If you do want to make a column nullable, just use `nullable()`: + +{% include "blocks/snippet" snippets = snippets name = 'nnbd' %} + +## References + +[Foreign key references](https://www.sqlite.org/foreignkeys.html) can be expressed +in Dart tables with the `references()` method when building a column: + +{% include "blocks/snippet" snippets = snippets name = 'references' %} + +The first parameter to `references` points to the table on which a reference should be created. +The second parameter is a [symbol](https://dart.dev/guides/language/language-tour#symbols) of the column to use for the reference. + +Optionally, the `onUpdate` and `onDelete` parameters can be used to describe what +should happen when the target row gets updated or deleted. + +Be aware that, in sqlite3, foreign key references aren't enabled by default. +They need to be enabled with `PRAGMA foreign_keys = ON`. +A suitable place to issue that pragma with drift is in a [post-migration callback]({{ '../Advanced Features/migrations.md#post-migration-callbacks' | pageUrl }}). + +## Default values + +You can set a default value for a column. When not explicitly set, the default value will +be used when inserting a new row. To set a constant default value, use `withDefault`: + +```dart +class Preferences extends Table { + TextColumn get name => text()(); + BoolColumn get enabled => boolean().withDefault(const Constant(false))(); +} +``` + +When you later use `into(preferences).insert(PreferencesCompanion.forInsert(name: 'foo'));`, the new +row will have its `enabled` column set to false (and not to null, as it normally would). +Note that columns with a default value (either through `autoIncrement` or by using a default), are +still marked as `@required` in generated data classes. This is because they are meant to represent a +full row, and every row will have those values. Use companions when representing partial rows, like +for inserts or updates. + +Of course, constants can only be used for static values. But what if you want to generate a dynamic +default value for each column? For that, you can use `clientDefault`. It takes a function returning +the desired default value. The function will be called for each insert. For instance, here's an +example generating a random Uuid using the `uuid` package: +```dart +final _uuid = Uuid(); + +class Users extends Table { + TextColumn get id => text().clientDefault(() => _uuid.v4())(); + // ... +} +``` + +Don't know when to use which? Prefer to use `withDefault` when the default value is constant, or something +simple like `currentDate`. For more complicated values, like a randomly generated id, you need to use +`clientDefault`. Internally, `withDefault` writes the default value into the `CREATE TABLE` statement. This +can be more efficient, but doesn't support dynamic values. + +### Checks + +If you know that a column (or a row) may only contain certain values, you can use a `CHECK` constraint +in SQL to enforce custom constraints on data. + +In Dart, the `check` method on the column builder adds a check constraint to the generated column: + +```dart + // sqlite3 will enforce that this column only contains timestamps happening after (the beginning of) 1950. + DateTimeColumn get creationTime => dateTime() + .check(creationTime.isBiggerThan(Constant(DateTime(1950)))) + .withDefault(currentDateAndTime)(); +``` + +Note that these `CHECK` constraints are part of the `CREATE TABLE` statement. +If you want to change or remove a `check` constraint, write a [schema migration]({{ '../Advanced Features/migrations.md#changing-column-constraints' | pageUrl }}) to re-create the table without the constraint. + +### Unique column + +When an individual column must be unique for all rows in the table, it can be declared as `unique()` +in its definition: + +{% include "blocks/snippet" snippets = snippets name = "unique-column" %} + +If the combination of more than one column must be unique in the table, you can add a unique +[table constraint](#unique-columns-in-table) to the table. + +### Custom constraints + +Some column and table constraints aren't supported through drift's Dart api. This includes the collation +of columns, which you can apply using `customConstraint`: + +```dart +class Groups extends Table { + TextColumn get name => integer().customConstraint('COLLATE BINARY')(); +} +``` + +Applying a `customConstraint` will override all other constraints that would be included by default. In +particular, that means that we need to also include the `NOT NULL` constraint again. + +You can also add table-wide constraints by overriding the `customConstraints` getter in your table class. + +## Names + +By default, drift uses the `snake_case` name of the Dart getter in the database. For instance, the +table + +{% assign name = 'package:drift_docs/snippets/dart_api/old_name.dart.excerpt.json' | readString | json_decode %} +{% include "blocks/snippet" snippets = name %} + +Would be generated as `CREATE TABLE enabled_categories (parent_category INTEGER NOT NULL)`. + +To override the table name, simply override the `tableName` getter. An explicit name for +columns can be provided with the `named` method: + +{% include "blocks/snippet" snippets = snippets name="names" %} + +The updated class would be generated as `CREATE TABLE categories (parent INTEGER NOT NULL)`. + +To update the name of a column when serializing data to json, annotate the getter with +[`@JsonKey`](https://pub.dev/documentation/drift/latest/drift/JsonKey-class.html). + +You can change the name of the generated data class too. By default, drift will stip a trailing +`s` from the table name (so a `Users` table would have a `User` data class). +That doesn't work in all cases though. With the `EnabledCategories` class from above, we'd get +a `EnabledCategorie` data class. In those cases, you can use the [`@DataClassName`](https://pub.dev/documentation/drift/latest/drift/DataClassName-class.html) +annotation to set the desired name. + +## Table options + +In addition to the options added to individual columns, some constraints apply to the whole +table. + +### Primary keys + +If your table has an `IntColumn` with an `autoIncrement()` constraint, drift recognizes that as the default +primary key. If you want to specify a custom primary key for your table, you can override the `primaryKey` +getter in your table: + +{% include "blocks/snippet" snippets = snippets name="primary-key" %} + +Note that the primary key must essentially be constant so that the generator can recognize it. That means: + +- it must be defined with the `=>` syntax, function bodies aren't supported +- it must return a set literal without collection elements like `if`, `for` or spread operators + +### Unique columns in table + +When the value of one column must be unique in the table, you can [make that column unique](#unique-column). +When the combined value of multiple columns should be unique, this needs to be declared on the +table by overriding the `uniqueKeys` getter: + +{% include "blocks/snippet" snippets = snippets name="unique-table" %} + +### Custom constraints on tables + +Some table constraints are not directly supported in drift yet. Similar to [custom constraints](#custom-constraints) +on columns, you can add those by overriding `customConstraints`: + +{% include "blocks/snippet" snippets = snippets name="custom-constraint-table" %} + +## Index \ No newline at end of file diff --git a/docs/pages/docs/Getting started/advanced_dart_tables.md b/docs/pages/docs/Getting started/advanced_dart_tables.md index 95220c46..0e8dbe7c 100644 --- a/docs/pages/docs/Getting started/advanced_dart_tables.md +++ b/docs/pages/docs/Getting started/advanced_dart_tables.md @@ -26,107 +26,6 @@ class Todos extends Table { In this article, we'll cover some advanced features of this syntax. -## Names - -By default, drift uses the `snake_case` name of the Dart getter in the database. For instance, the -table -```dart -class EnabledCategories extends Table { - IntColumn get parentCategory => integer()(); - // .. -} -``` - -Would be generated as `CREATE TABLE enabled_categories (parent_category INTEGER NOT NULL)`. - -To override the table name, simply override the `tableName` getter. An explicit name for -columns can be provided with the `named` method: -```dart -class EnabledCategories extends Table { - String get tableName => 'categories'; - - IntColumn get parentCategory => integer().named('parent')(); -} -``` - -The updated class would be generated as `CREATE TABLE categories (parent INTEGER NOT NULL)`. - -To update the name of a column when serializing data to json, annotate the getter with -[`@JsonKey`](https://pub.dev/documentation/drift/latest/drift/JsonKey-class.html). - -You can change the name of the generated data class too. By default, drift will stip a trailing -`s` from the table name (so a `Users` table would have a `User` data class). -That doesn't work in all cases though. With the `EnabledCategories` class from above, we'd get -a `EnabledCategorie` data class. In those cases, you can use the [`@DataClassName`](https://pub.dev/documentation/drift/latest/drift/DataClassName-class.html) -annotation to set the desired name. - -## Nullability - -By default, columns may not contain null values. When you forgot to set a value in an insert, -an exception will be thrown. When using sql, drift also warns about that at compile time. - -If you do want to make a column nullable, just use `nullable()`: -```dart -class Items { - IntColumn get category => integer().nullable()(); - // ... -} -``` - -## Checks - -If you know that a column (or a row) may only contain certain values, you can use a `CHECK` constraint -in SQL to enforce custom constraints on data. - -In Dart, the `check` method on the column builder adds a check constraint to the generated column: - -```dart - // sqlite3 will enforce that this column only contains timestamps happening after (the beginning of) 1950. - DateTimeColumn get creationTime => dateTime() - .check(creationTime.isBiggerThan(Constant(DateTime(1950)))) - .withDefault(currentDateAndTime)(); -``` - -Note that these `CHECK` constraints are part of the `CREATE TABLE` statement. -If you want to change or remove a `check` constraint, write a [schema migration]({{ '../Advanced Features/migrations.md#changing-column-constraints' | pageUrl }}) to re-create the table without the constraint. - -## Default values - -You can set a default value for a column. When not explicitly set, the default value will -be used when inserting a new row. To set a constant default value, use `withDefault`: - -```dart -class Preferences extends Table { - TextColumn get name => text()(); - BoolColumn get enabled => boolean().withDefault(const Constant(false))(); -} -``` - -When you later use `into(preferences).insert(PreferencesCompanion.forInsert(name: 'foo'));`, the new -row will have its `enabled` column set to false (and not to null, as it normally would). -Note that columns with a default value (either through `autoIncrement` or by using a default), are -still marked as `@required` in generated data classes. This is because they are meant to represent a -full row, and every row will have those values. Use companions when representing partial rows, like -for inserts or updates. - -Of course, constants can only be used for static values. But what if you want to generate a dynamic -default value for each column? For that, you can use `clientDefault`. It takes a function returning -the desired default value. The function will be called for each insert. For instance, here's an -example generating a random Uuid using the `uuid` package: -```dart -final _uuid = Uuid(); - -class Users extends Table { - TextColumn get id => text().clientDefault(() => _uuid.v4())(); - // ... -} -``` - -Don't know when to use which? Prefer to use `withDefault` when the default value is constant, or something -simple like `currentDate`. For more complicated values, like a randomly generated id, you need to use -`clientDefault`. Internally, `withDefault` writes the default value into the `CREATE TABLE` statement. This -can be more efficient, but doesn't support dynamic values. - ## Primary keys If your table has an `IntColumn` with an `autoIncrement()` constraint, drift recognizes that as the default @@ -148,243 +47,6 @@ Note that the primary key must essentially be constant so that the generator can - it must be defined with the `=>` syntax, function bodies aren't supported - it must return a set literal without collection elements like `if`, `for` or spread operators -## Unique Constraints - -Starting from version 1.6.0, `UNIQUE` SQL constraints can be defined on Dart tables too. -A unique constraint contains one or more columns. The combination of all columns in a constraint -must be unique in the table, or the database will report an error on inserts. - -With drift, a unique constraint can be added to a single column by marking it as `.unique()` in -the column builder. -A unique set spanning multiple columns can be added by overriding the `uniqueKeys` getter in the -`Table` class: - -{% include "blocks/snippet" snippets = snippets name = 'unique' %} - -## Supported column types - -Drift supports a variety of column types out of the box. You can store custom classes in columns by using -[type converters]({{ "../Advanced Features/type_converters.md" | pageUrl }}). - -| Dart type | Column | Corresponding SQLite type | -|--------------|---------------|-----------------------------------------------------| -| `int` | `integer()` | `INTEGER` | -| `BigInt` | `int64()` | `INTEGER` (useful for large values on the web) | -| `double` | `real()` | `REAL` | -| `boolean` | `boolean()` | `INTEGER`, which a `CHECK` to only allow `0` or `1` | -| `String` | `text()` | `TEXT` | -| `DateTime` | `dateTime()` | `INTEGER` (default) or `TEXT` depending on [options](#datetime-options) | -| `Uint8List` | `blob()` | `BLOB` | -| `Enum` | `intEnum()` | `INTEGER` (more information available [here]({{ "../Advanced Features/type_converters.md#implicit-enum-converters" | pageUrl }})). | -| `Enum` | `textEnum()` | `TEXT` (more information available [here]({{ "../Advanced Features/type_converters.md#implicit-enum-converters" | pageUrl }})). | - -Note that the mapping for `boolean`, `dateTime` and type converters only applies when storing records in -the database. -They don't affect JSON serialization at all. For instance, `boolean` values are expected as `true` or `false` -in the `fromJson` factory, even though they would be saved as `0` or `1` in the database. -If you want a custom mapping for JSON, you need to provide your own [`ValueSerializer`](https://pub.dev/documentation/drift/latest/drift/ValueSerializer-class.html). - -### `BigInt` support - -Drift supports the `int64()` column builder to indicate that a column stores -large integers and should be mapped to Dart as a `BigInt`. - -This is mainly useful for Dart apps compiled to JavaScript, where an `int` -really is a `double` that can't store large integers without loosing information. -Here, representing integers as `BigInt` (and passing those to the underlying -database implementation) ensures that you can store large intergers without any -loss of precision. -Be aware that `BigInt`s have a higher overhead than `int`s, so we recommend using -`int64()` only for columns where this is necessary: - -{% block "blocks/alert" title="You might not need this!" color="info" %} -In sqlite3, an `INTEGER` column is stored as a 64-bit integer. -For apps running in the Dart VM (e.g. on everything except for the web), the `int` -type in Dart is the _perfect_ match for that since it's also a 64-bit int. -For those apps, we recommend using the regular `integer()` column builder. - -Essentially, you should use `int64()` if both of these are true: - -- you're building an app that needs to work on the web, _and_ -- the column in question may store values larger than 252. - -In all other cases, using a regular `integer()` column is more efficient. -{% endblock %} - -Here are some more pointers on using `BigInt`s in drift: - -- Since an `integer()` and a `int64()` is the same column in sqlite3, you can - switch between the two without writing a schema migration. -- In addition to large columns, it may also be that you have a complex expression - in a select query that would be better represented as a `BigInt`. You can use - `dartCast()` for this: For an expression - `(table.columnA * table.columnB).dartCast()`, drift will report the - resulting value as a `BigInt` even if `columnA` and `columnB` were defined - as regular integers. -- `BigInt`s are not currently supported by `moor_flutter` and `drift_sqflite`. -- To use `BigInt` support on a `WebDatabase`, set the `readIntsAsBigInt: true` - flag when instantiating it. -- Both `NativeDatabase` and `WasmDatabase` have builtin support for bigints. - -### `DateTime` options - -Drift supports two approaches of storing `DateTime` values in SQL: - -1. __As unix timestamp__ (the default): In this mode, drift stores date time - values as an SQL `INTEGER` containing the unix timestamp (in seconds). - When date times are mapped from SQL back to Dart, drift always returns a - non-UTC value. So even when UTC date times are stored, this information is - lost when retrieving rows. -2. __As ISO 8601 string__: In this mode, datetime values are stored in a - textual format based on `DateTime.toIso8601String()`: UTC values are stored - unchanged (e.g. `2022-07-25 09:28:42.015Z`), while local values have their - UTC offset appended (e.g. `2022-07-25T11:28:42.015 +02:00`). - Most of sqlite3's date and time functions operate on UTC values, but parsing - datetimes in SQL respects the UTC offset added to the value. - - When reading values back from the database, drift will use `DateTime.parse` - as following: - - If the textual value ends with `Z`, drift will use `DateTime.parse` - directly. The `Z` suffix will be recognized and a UTC value is returned. - - If the textual value ends with a UTC offset (e.g. `+02:00`), drift first - uses `DateTime.parse` which respects the modifier but returns a UTC - datetime. Drift then calls `toLocal()` on this intermediate result to - return a local value. - - If the textual value neither has a `Z` suffix nor a UTC offset, drift - will parse it as if it had a `Z` modifier, returning a UTC datetime. - The motivation for this is that the `datetime` function in sqlite3 returns - values in this format and uses UTC by default. - - This behavior works well with the date functions in sqlite3 while also - preserving "UTC-ness" for stored values. - -The mode can be changed with the `store_date_time_values_as_text` [build option]({{ '../Advanced Features/builder_options.md' | pageUrl }}). - -Regardless of the option used, drift's builtin support for -[date and time functions]({{ '../Advanced Features/expressions.md#date-and-time' | pageUrl }}) -return an equivalent values. Drift internally inserts the `unixepoch` -[modifier](https://sqlite.org/lang_datefunc.html#modifiers) when unix timestamps -are used to make the date functions work. When comparing dates stored as text, -drift will compare their `julianday` values behind the scenes. - -#### Migrating between the two modes - -While making drift change the date time modes is as simple as changing a build -option, toggling this behavior is not compatible with existing database schemas: - -1. Depending on the build option, drift expects strings or integers for datetime - values. So you need to migrate stored columns to the new format when changing - the option. -2. If you are using SQL statements defined in `.drift` files, use custom SQL - at runtime or manually invoke datetime expressions with a direct - `FunctionCallExpression` instead of using the higher-level date time APIs, you - may have to adapt those usages. - - For instance, comparison operators like `<` work on unix timestamps, but they - will compare textual datetime values lexicographically. So depending on the - mode used, you will have to wrap the value in `unixepoch` or `julianday` to - make them comparable. - -As the second point is specific to usages in your app, this documentation only -describes how to migrate stored columns between the format: - -{% assign conversion = "package:drift_docs/snippets/migrations/datetime_conversion.dart.excerpt.json" | readString | json_decode %} - -Note that the JSON serialization generated by default is not affected by the -datetime mode chosen. By default, drift will serialize `DateTime` values to a -unix timestamp in milliseconds. You can change this by creating a -`ValueSerializer.defaults(serializeDateTimeValuesAsString: true)` and assigning -it to `driftRuntimeOptions.defaultSerializer`. - -##### Migrating from unix timestamps to text - -To migrate from using timestamps (the default option) to storing datetimes as -text, follow these steps: - -1. Enable the `store_date_time_values_as_text` build option. -2. Add the following method (or an adaption of it suiting your needs) to your - database class. -3. Increment the `schemaVersion` in your database class. -4. Write a migration step in `onUpgrade` that calls - `migrateFromUnixTimestampsToText` for this schema version increase. - __Remember that triggers, views or other custom SQL entries in your database - will require a custom migration that is not covered by this guide.__ - -{% include "blocks/snippet" snippets = conversion name = "unix-to-text" %} - -##### Migrating from text to unix timestamps - -To migrate from datetimes stored as text back to unix timestamps, follow these -steps: - -1. Disable the `store_date_time_values_as_text` build option. -2. Add the following method (or an adaption of it suiting your needs) to your - database class. -3. Increment the `schemaVersion` in your database class. -4. Write a migration step in `onUpgrade` that calls - `migrateFromTextDateTimesToUnixTimestamps` for this schema version increase. - __Remember that triggers, views or other custom SQL entries in your database - will require a custom migration that is not covered by this guide.__ - -{% include "blocks/snippet" snippets = conversion name = "text-to-unix" %} - -Note that this snippet uses the `unixepoch` sqlite3 function, which has been -added in sqlite 3.38. To support older sqlite3 versions, you can use `strftime` -and cast to an integer instead: - -{% include "blocks/snippet" snippets = conversion name = "text-to-unix-old" %} - -When using a `NativeDatabase` with a recent dependency on the -`sqlite3_flutter_libs` package, you can safely assume that you are on a recent -sqlite3 version with support for `unixepoch`. - -## Custom constraints - -Some column and table constraints aren't supported through drift's Dart api. This includes `REFERENCES` clauses on columns, which you can set -through `customConstraint`: - -```dart -class GroupMemberships extends Table { - IntColumn get group => integer().customConstraint('NOT NULL REFERENCES groups (id)')(); - IntColumn get user => integer().customConstraint('NOT NULL REFERENCES users (id)')(); - - @override - Set get primaryKey => {group, user}; -} -``` - -Applying a `customConstraint` will override all other constraints that would be included by default. In -particular, that means that we need to also include the `NOT NULL` constraint again. - -You can also add table-wide constraints by overriding the `customConstraints` getter in your table class. - -## References - -[Foreign key references](https://www.sqlite.org/foreignkeys.html) can be expressed -in Dart tables with the `references()` method when building a column: - -```dart -class Todos extends Table { - // ... - IntColumn get category => integer().nullable().references(Categories, #id)(); -} - -@DataClassName("Category") -class Categories extends Table { - IntColumn get id => integer().autoIncrement()(); - // and more columns... -} -``` - -The first parameter to `references` points to the table on which a reference should be created. -The second parameter is a [symbol](https://dart.dev/guides/language/language-tour#symbols) of the column to use for the reference. - -Optionally, the `onUpdate` and `onDelete` parameters can be used to describe what -should happen when the target row gets updated or deleted. - -Be aware that, in sqlite3, foreign key references aren't enabled by default. -They need to be enabled with `PRAGMA foreign_keys = ON`. -A suitable place to issue that pragma with drift is in a [post-migration callback]({{ '../Advanced Features/migrations.md#post-migration-callbacks' | pageUrl }}). ## Views diff --git a/docs/test/snippet_test.dart b/docs/test/snippet_test.dart index 6b996b7b..16968961 100644 --- a/docs/test/snippet_test.dart +++ b/docs/test/snippet_test.dart @@ -1,6 +1,6 @@ import 'package:drift/drift.dart'; import 'package:drift/native.dart'; -import 'package:drift_docs/snippets/migrations/datetime_conversion.dart'; +import 'package:drift_docs/snippets/dart_api/datetime_conversion.dart'; import 'package:drift_docs/snippets/modular/schema_inspection.dart'; import 'package:test/test.dart'; diff --git a/drift/lib/src/dsl/table.dart b/drift/lib/src/dsl/table.dart index ac358366..072f12f6 100644 --- a/drift/lib/src/dsl/table.dart +++ b/drift/lib/src/dsl/table.dart @@ -72,7 +72,7 @@ abstract class Table extends HasResultSet { /// ```dart /// class IngredientInRecipes extends Table { /// @override - /// Set get uniqueKeys => + /// List> get uniqueKeys => /// [{recipe, ingredient}, {recipe, amountInGrams}]; /// /// IntColumn get recipe => integer()();