diff --git a/docs/docs/Platforms/postgres.md b/docs/docs/Platforms/postgres.md index 54fb706e1..f39d3f2ec 100644 --- a/docs/docs/Platforms/postgres.md +++ b/docs/docs/Platforms/postgres.md @@ -86,26 +86,34 @@ These will be moved into separate libraries in a future major release to avoid c aware of them for the time being. This section lists affected APIs and workarounds to make them work PostgreSQL. -1. Most parts of the `Migrator` API are SQLite-specific. You will be able to create tables on PostgreSQL as well, - but methods like `alterTable` will only work with SQLite. - The [migrations](#migrations) section below describes possible workarounds - the recommended approach is to - export your drift schema and then use dedicated migration tools for PostgreSQL. -2. Drift's datetime columns were designed to work with SQLite, which doesn't have dedicated datetime types. - Most of the date time APIs (like `currentDateAndTime`) will not work with PostgreSQL. - When using drift databases with PostgreSQL, we suggest avoiding the default `dateTime()` column type and instead - use `PgTypes.date` or `PgTypes.datetime`. - If you need to support both sqlite3 and Postgres, consider using [dialect-aware types](../sql_api/types.md#dialect-awareness). - ### Migrations +Most parts of the `Migrator` API are SQLite-specific. You will be able to create tables on PostgreSQL as well, +but methods like `alterTable` will only work with SQLite. +While it's possible to use drift migrations with PostgreSQL databases, the recommended approach for now is to +[export your drift schema](../Tools/index.md#exporting) and then use dedicated migration tools for PostgreSQL. + In sqlite3, the current schema version is stored in the database file. To support drift's migration API -being built ontop of this mechanism in Postgres as well, drift creates a `__schema` table storing +being built on top of this mechanism in Postgres as well, drift creates a `__schema` table storing the current schema version. This migration mechanism works for simple deployments, but is unsuitable for large database setups -with many application servers connecting to postgres. For those, an existing migration management +with many application servers connecting to a postgres serve. For those, an existing migration management tool is a more reliable alternative. If you chose to manage migrations with another tool, you can -disable migrations in postgres by passing `enableMigrations: false` to the `PgDatabase` constructor. +disable migrations in drift by passing `enableMigrations: false` to the `PgDatabase` constructor. + +### DateTime columns + +Drift's `datetime()` columns were designed to work with SQLite, which doesn't have dedicated datetime types. +Most of the date time APIs (like `currentDateAndTime`) will not work with PostgreSQL. +When using drift databases with PostgreSQL, we suggest avoiding the default `dateTime()` column type and instead +use `PgTypes.date` or `PgTypes.datetime`: + +{{ load_snippet('time','lib/snippets/platforms/postgres.dart.excerpt.json') }} + +If you need to support both sqlite3 and Postgres, consider using [dialect-aware types](../sql_api/types.md#dialect-awareness): + +{{ load_snippet('time-dialectaware','lib/snippets/platforms/postgres.dart.excerpt.json') }} ## Current state diff --git a/docs/docs/dart_api/tables.md b/docs/docs/dart_api/tables.md index abf1cd2a4..717f42274 100644 --- a/docs/docs/dart_api/tables.md +++ b/docs/docs/dart_api/tables.md @@ -448,4 +448,4 @@ targets: # store_date_time_values_as_text: true ``` -See the [DateTime Guide](../guides/datetime-migrations.md) for more information on how dates are stored and how to switch between storage methods. +See the [DateTime migration guide](../guides/datetime-migrations.md) for more information on how dates are stored and how to switch between storage methods. diff --git a/docs/lib/snippets/_shared/todo_tables.drift.dart b/docs/lib/snippets/_shared/todo_tables.drift.dart index f7476d349..6da5585ec 100644 --- a/docs/lib/snippets/_shared/todo_tables.drift.dart +++ b/docs/lib/snippets/_shared/todo_tables.drift.dart @@ -241,7 +241,41 @@ class $$TodoItemsTableTableManager extends i0.RootTableManager< i1.$$TodoItemsTableReferences(db, table, e) )) .toList(), - prefetchHooksCallback: null, + prefetchHooksCallback: ({category = false}) { + return i0.PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: < + T extends i0.TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic>>(state) { + if (category) { + state = state.withJoin( + currentTable: table, + currentColumn: table.category, + referencedTable: + i1.$$TodoItemsTableReferences._categoryTable(db), + referencedColumn: + i1.$$TodoItemsTableReferences._categoryTable(db).id, + ) as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, )); } @@ -438,7 +472,33 @@ class $$CategoriesTableTableManager extends i0.RootTableManager< i1.$$CategoriesTableReferences(db, table, e) )) .toList(), - prefetchHooksCallback: null, + prefetchHooksCallback: ({todoItemsRefs = false}) { + return i0.PrefetchHooks( + db: db, + explicitlyWatchedTables: [ + if (todoItemsRefs) + i3.ReadDatabaseContainer(db) + .resultSet('todo_items') + ], + addJoins: null, + getPrefetchedDataCallback: (items) async { + return [ + if (todoItemsRefs) + await i0.$_getPrefetchedData( + currentTable: table, + referencedTable: i1.$$CategoriesTableReferences + ._todoItemsRefsTable(db), + managerFromTypedResult: (p0) => i1 + .$$CategoriesTableReferences(db, table, p0) + .todoItemsRefs, + referencedItemsForCurrentItem: (item, + referencedItems) => + referencedItems.where((e) => e.category == item.id), + typedResults: items) + ]; + }, + ); + }, )); } diff --git a/docs/lib/snippets/modular/custom_types/type.dart b/docs/lib/snippets/modular/custom_types/type.dart index a8b94eca6..fedef40c5 100644 --- a/docs/lib/snippets/modular/custom_types/type.dart +++ b/docs/lib/snippets/modular/custom_types/type.dart @@ -44,7 +44,6 @@ class _FallbackDurationType implements CustomSqlType { return 'integer'; } } -// #enddocregion fallback const durationType = DialectAwareSqlType.via( fallback: _FallbackDurationType(), @@ -52,3 +51,4 @@ const durationType = DialectAwareSqlType.via( SqlDialect.postgres: DurationType(), }, ); +// #enddocregion fallback diff --git a/docs/lib/snippets/modular/drift/example.drift.dart b/docs/lib/snippets/modular/drift/example.drift.dart index 1c361ba09..c39862221 100644 --- a/docs/lib/snippets/modular/drift/example.drift.dart +++ b/docs/lib/snippets/modular/drift/example.drift.dart @@ -219,7 +219,39 @@ class $TodosTableManager extends i0.RootTableManager< .map((e) => (e.readTable(table), i1.$TodosReferences(db, table, e))) .toList(), - prefetchHooksCallback: null, + prefetchHooksCallback: ({category = false}) { + return i0.PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: < + T extends i0.TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic>>(state) { + if (category) { + state = state.withJoin( + currentTable: table, + currentColumn: table.category, + referencedTable: i1.$TodosReferences._categoryTable(db), + referencedColumn: i1.$TodosReferences._categoryTable(db).id, + ) as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, )); } @@ -410,7 +442,31 @@ class $CategoriesTableManager extends i0.RootTableManager< .map((e) => (e.readTable(table), i1.$CategoriesReferences(db, table, e))) .toList(), - prefetchHooksCallback: null, + prefetchHooksCallback: ({todosRefs = false}) { + return i0.PrefetchHooks( + db: db, + explicitlyWatchedTables: [ + if (todosRefs) + i2.ReadDatabaseContainer(db).resultSet('todos') + ], + addJoins: null, + getPrefetchedDataCallback: (items) async { + return [ + if (todosRefs) + await i0.$_getPrefetchedData( + currentTable: table, + referencedTable: + i1.$CategoriesReferences._todosRefsTable(db), + managerFromTypedResult: (p0) => + i1.$CategoriesReferences(db, table, p0).todosRefs, + referencedItemsForCurrentItem: (item, + referencedItems) => + referencedItems.where((e) => e.category == item.id), + typedResults: items) + ]; + }, + ); + }, )); } diff --git a/docs/lib/snippets/modular/drift/with_existing.drift.dart b/docs/lib/snippets/modular/drift/with_existing.drift.dart index 25a3f3bd9..8e2908e14 100644 --- a/docs/lib/snippets/modular/drift/with_existing.drift.dart +++ b/docs/lib/snippets/modular/drift/with_existing.drift.dart @@ -381,7 +381,47 @@ class $FriendsTableManager extends i0.RootTableManager< .map((e) => (e.readTable(table), i2.$FriendsReferences(db, table, e))) .toList(), - prefetchHooksCallback: null, + prefetchHooksCallback: ({userA = false, userB = false}) { + return i0.PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: < + T extends i0.TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic>>(state) { + if (userA) { + state = state.withJoin( + currentTable: table, + currentColumn: table.userA, + referencedTable: i2.$FriendsReferences._userATable(db), + referencedColumn: i2.$FriendsReferences._userATable(db).id, + ) as T; + } + if (userB) { + state = state.withJoin( + currentTable: table, + currentColumn: table.userB, + referencedTable: i2.$FriendsReferences._userBTable(db), + referencedColumn: i2.$FriendsReferences._userBTable(db).id, + ) as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, )); } diff --git a/docs/lib/snippets/modular/many_to_many/relational.drift.dart b/docs/lib/snippets/modular/many_to_many/relational.drift.dart index 91b98ca42..76209db5a 100644 --- a/docs/lib/snippets/modular/many_to_many/relational.drift.dart +++ b/docs/lib/snippets/modular/many_to_many/relational.drift.dart @@ -189,7 +189,34 @@ class $$ShoppingCartsTableTableManager extends i0.RootTableManager< i2.$$ShoppingCartsTableReferences(db, table, e) )) .toList(), - prefetchHooksCallback: null, + prefetchHooksCallback: ({shoppingCartEntriesRefs = false}) { + return i0.PrefetchHooks( + db: db, + explicitlyWatchedTables: [ + if (shoppingCartEntriesRefs) + i4.ReadDatabaseContainer(db) + .resultSet( + 'shopping_cart_entries') + ], + addJoins: null, + getPrefetchedDataCallback: (items) async { + return [ + if (shoppingCartEntriesRefs) + await i0.$_getPrefetchedData( + currentTable: table, + referencedTable: i2.$$ShoppingCartsTableReferences + ._shoppingCartEntriesRefsTable(db), + managerFromTypedResult: (p0) => i2 + .$$ShoppingCartsTableReferences(db, table, p0) + .shoppingCartEntriesRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems + .where((e) => e.shoppingCart == item.id), + typedResults: items) + ]; + }, + ); + }, )); } @@ -487,7 +514,53 @@ class $$ShoppingCartEntriesTableTableManager extends i0.RootTableManager< i2.$$ShoppingCartEntriesTableReferences(db, table, e) )) .toList(), - prefetchHooksCallback: null, + prefetchHooksCallback: ({shoppingCart = false, item = false}) { + return i0.PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: < + T extends i0.TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic>>(state) { + if (shoppingCart) { + state = state.withJoin( + currentTable: table, + currentColumn: table.shoppingCart, + referencedTable: i2.$$ShoppingCartEntriesTableReferences + ._shoppingCartTable(db), + referencedColumn: i2.$$ShoppingCartEntriesTableReferences + ._shoppingCartTable(db) + .id, + ) as T; + } + if (item) { + state = state.withJoin( + currentTable: table, + currentColumn: table.item, + referencedTable: + i2.$$ShoppingCartEntriesTableReferences._itemTable(db), + referencedColumn: i2.$$ShoppingCartEntriesTableReferences + ._itemTable(db) + .id, + ) as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, )); } diff --git a/docs/lib/snippets/platforms/postgres.dart b/docs/lib/snippets/platforms/postgres.dart index 1cb9847ff..36f474de1 100644 --- a/docs/lib/snippets/platforms/postgres.dart +++ b/docs/lib/snippets/platforms/postgres.dart @@ -61,3 +61,68 @@ Future openWithPool() async { await pool.close(); } // #enddocregion pool + +// #docregion time +// This table uses proper postgres types to store date/time values. +class TimeStore extends Table { + Column get date => customType(PgTypes.date)(); + Column get timestampWithTimezone => + customType(PgTypes.timestampWithTimezone)(); + Column get timestampWithoutTimezone => + customType(PgTypes.timestampNoTimezone)(); + Column get interval => customType(PgTypes.interval)(); +} +// #enddocregion time + +// #docregion time-dialectaware +class _DialectAwareDateTimeType implements DialectAwareSqlType { + /// The underlying type used when this dialect-aware type is used on postgres + /// databases. + static const _postgres = PgTypes.timestampWithTimezone; + + /// The fallback type used when we're not talking to postgres. + static const _other = DriftSqlType.dateTime; + + const _DialectAwareDateTimeType(); + + @override + String mapToSqlLiteral(GenerationContext context, PgDateTime dartValue) { + return switch (context.dialect) { + SqlDialect.postgres => _postgres.mapToSqlLiteral(dartValue), + _ => context.typeMapping.mapToSqlLiteral(dartValue.dateTime), + }; + } + + @override + Object mapToSqlParameter(GenerationContext context, PgDateTime dartValue) { + return switch (context.dialect) { + SqlDialect.postgres => _postgres.mapToSqlParameter(dartValue), + _ => context.typeMapping.mapToSqlVariable(dartValue.dateTime)!, + }; + } + + @override + PgDateTime read(SqlTypes typeSystem, Object fromSql) { + return switch (typeSystem.dialect) { + SqlDialect.postgres => _postgres.read(fromSql), + _ => PgDateTime(typeSystem.read(_other, fromSql)!), + }; + } + + @override + String sqlTypeName(GenerationContext context) { + return switch (context.dialect) { + SqlDialect.postgres => _postgres.sqlTypeName(context), + _ => _other.sqlTypeName(context), + }; + } +} + +const dateTime = _DialectAwareDateTimeType(); + +class DialectAwareTime extends Table { + // This will use `timestamp with timezone` on postgres, and fall back to the + // default date type (integer or text) on sqlite databases. + Column get timeValue => customType(dateTime)(); +} +// #enddocregion time-dialectaware diff --git a/drift/lib/src/dsl/table.dart b/drift/lib/src/dsl/table.dart index d83fe53f9..1e20445dd 100644 --- a/drift/lib/src/dsl/table.dart +++ b/drift/lib/src/dsl/table.dart @@ -165,7 +165,7 @@ abstract class Table extends HasResultSet { ColumnBuilder boolean() => _isGenerated(); /// Use this as the body of a getter to declare a column that holds date and - /// time. + /// time values. /// /// Drift supports two modes for storing date times: As unix timestamp with /// second accuracy (the default) and as ISO 8601 string with microsecond @@ -177,6 +177,10 @@ abstract class Table extends HasResultSet { /// ``` /// DateTimeColumn get accountCreatedAt => dateTime()(); /// ``` + /// + /// [dateTime] columns are optimized for SQLite. When using drift with another + /// database, such as PostgreSQL, use [native datetime columns](https://drift.simonbinder.eu/platforms/postgres/#avoiding-sqlite-specific-drift-apis). + /// /// [the documentation]: https://drift.simonbinder.eu/docs/getting-started/advanced_dart_tables/#supported-column-types @protected ColumnBuilder dateTime() => _isGenerated();