Write new page on dart tables

This commit is contained in:
Simon Binder 2023-09-16 18:38:22 +02:00
parent e0a6b557e7
commit 2403da5b98
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
9 changed files with 459 additions and 342 deletions

View File

@ -0,0 +1,5 @@
import 'package:drift/drift.dart';
class EnabledCategories extends Table {
IntColumn get parentCategory => integer()();
}

View File

@ -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<Column> get primaryKey => {group, user};
}
// #enddocregion primary-key
// #docregion unique-table
class IngredientInRecipes extends Table {
@override
List<Set<Column>> 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<String> get customConstraints => [
'FOREIGN KEY (foo, bar) REFERENCES group_memberships ("group", user)',
];
}
// #enddocregion custom-constraint-table
// #docregion index
class Users extends Table {}
// #enddocregion index

View File

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

View File

@ -11,6 +11,7 @@ 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
available from your main database class. Consider the following code:
```dart
part 'todos_dao.g.dart';
@ -33,5 +34,6 @@ class TodosDao extends DatabaseAccessor<MyDatabase> 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.

View File

@ -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 2<sup>52</sup>.
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<BigInt>()`, 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

View File

@ -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 2<sup>52</sup>.
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<BigInt>()`, 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<Column> 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

View File

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

View File

@ -72,7 +72,7 @@ abstract class Table extends HasResultSet {
/// ```dart
/// class IngredientInRecipes extends Table {
/// @override
/// Set<Column> get uniqueKeys =>
/// List<Set<Column>> get uniqueKeys =>
/// [{recipe, ingredient}, {recipe, amountInGrams}];
///
/// IntColumn get recipe => integer()();