From bf70323680f09c8865c2a6809dfb39c16fa56b92 Mon Sep 17 00:00:00 2001 From: omgitzjoe Date: Thu, 24 Sep 2026 17:43:41 -0600 Subject: [PATCH 1/3] feat: add opt-in timestamptz datetime storage --- lib/data_layer.ex | 30 +++- .../migration_generator.ex | 18 ++- lib/repo.ex | 10 ++ test/migration_generator_test.exs | 140 ++++++++++++++++++ 4 files changed, 196 insertions(+), 2 deletions(-) diff --git a/lib/data_layer.ex b/lib/data_layer.ex index 70b9ff57..51cbc7b9 100644 --- a/lib/data_layer.ex +++ b/lib/data_layer.ex @@ -865,7 +865,35 @@ defmodule AshPostgres.DataLayer do # value Postgres can't round-trip directly. `Ash.Type.Range`'s value is an # `%Ash.Range{}`, which Postgrex can neither encode nor produce — it needs a # `%Postgrex.Range{}`. Routing the schema field through `AshPostgres.Type.Range` - # makes both `insert_all` (dump) and `repo.all` (load) go through our type. + # makes both `insert_all` (dump) and `repo.all` (load) go through our type + + def attribute_ecto_type(resource, %{type: Ash.Type.DateTime, constraints: constraints}) do + repo = AshPostgres.DataLayer.Info.repo(resource, :mutate) + + if repo do + case Code.ensure_compiled(repo) do + {:module, _} -> + if function_exported?(repo, :use_timestamptz?, 0) && + repo.use_timestamptz?() do + case Keyword.get(constraints, :precision) do + :microsecond -> + Ash.Type.ecto_type(AshPostgres.TimestamptzUsec) + + _ -> + Ash.Type.ecto_type(AshPostgres.Timestamptz) + end + else + nil + end + + _ -> + nil + end + else + nil + end + end + def attribute_ecto_type(_resource, %{type: Ash.Type.Range}) do Ash.Type.ecto_type(AshPostgres.Type.Range) end diff --git a/lib/migration_generator/migration_generator.ex b/lib/migration_generator/migration_generator.ex index 6acafae6..2a828d62 100644 --- a/lib/migration_generator/migration_generator.ex +++ b/lib/migration_generator/migration_generator.ex @@ -3915,10 +3915,26 @@ defmodule AshPostgres.MigrationGenerator do |> Enum.map(fn attribute -> default = default(attribute, resource, repo) + migration_type_override = + AshPostgres.DataLayer.Info.migration_types(resource)[attribute.name] + type = - AshPostgres.DataLayer.Info.migration_types(resource)[attribute.name] || + migration_type_override || migration_type(attribute.type, attribute.constraints) + type = + if is_nil(migration_type_override) && + function_exported?(repo, :use_timestamptz?, 0) && + repo.use_timestamptz?() do + case type do + :utc_datetime -> :timestamptz + :utc_datetime_usec -> :"timestamptz(6)" + type -> type + end + else + type + end + type = if function_exported?(repo, :override_migration_type, 1) do repo.override_migration_type(type) diff --git a/lib/repo.ex b/lib/repo.ex index e3c75f1e..7f49663f 100644 --- a/lib/repo.ex +++ b/lib/repo.ex @@ -135,6 +135,10 @@ defmodule AshPostgres.Repo do @doc "Allows overriding a given migration type for *all* fields, for example if you wanted to always use :timestamptz for :utc_datetime fields" @callback override_migration_type(atom) :: atom + + @doc "Whether or not to use PostgreSQL `timestamptz` for `Ash.Type.DateTime` fields." + @callback use_timestamptz?() :: boolean + @doc "Whether or not to use the built-in `uuidv7` function (as opposed to the Ash `uuid_generate_v7` function) for `UUIDv7` fields." @callback use_builtin_uuidv7_function? :: boolean @doc "Should the repo should be created by `mix ash_postgres.create`?" @@ -185,6 +189,11 @@ defmodule AshPostgres.Repo do def default_prefix, do: "public" def override_migration_type(type), do: type + @doc """ + Whether to use 'timestamptz' for data storage, defaults to false + """ + def use_timestamptz?, do: false + def use_builtin_uuidv7_function? do %Version{major: major} = min_pg_version() major >= 18 @@ -363,6 +372,7 @@ defmodule AshPostgres.Repo do migrations_path: 0, default_prefix: 0, override_migration_type: 1, + use_timestamptz?: 0, create?: 0, drop?: 0, disable_atomic_actions?: 0, diff --git a/test/migration_generator_test.exs b/test/migration_generator_test.exs index 6d56082e..2e224638 100644 --- a/test/migration_generator_test.exs +++ b/test/migration_generator_test.exs @@ -269,6 +269,146 @@ defmodule AshPostgres.MigrationGeneratorTest do end end + defmodule TimestamptzToggleRepo do + use AshPostgres.Repo, + otp_app: :ash_postgres + + def use_timestamptz?, do: true + + def min_pg_version do + %Version{major: 16, minor: 0, patch: 0} + end + end + + defmodule TimestamptzPost do + use Ash.Resource, + domain: nil, + data_layer: AshPostgres.DataLayer + + postgres do + table "timestamptz_posts" + repo TimestamptzToggleRepo + migration_types(explicit_type: :utc_datetime) + end + + attributes do + uuid_primary_key(:id) + attribute(:starts_at, :datetime, public?: true) + attribute(:explicit_type, :datetime, public?: true) + end + + actions do + create :create do + accept([:starts_at]) + end + + read :read do + primary?(true) + end + end + end + + defmodule TimestamptzDomain do + use Ash.Domain + + resources do + resource(TimestamptzPost) + end + end + + describe "timestamptz datetime toggle" do + setup %{snapshot_path: snapshot_path, migration_path: migration_path} do + {:ok, _pid} = + TimestamptzToggleRepo.start_link( + username: "postgres", + password: "postgres", + database: "ash_postgres_test", + hostname: "localhost", + pool: Ecto.Adapters.SQL.Sandbox, + types: AshPostgres.Test.PostgrexTypes + ) + + :ok = Ecto.Adapters.SQL.Sandbox.checkout(TimestamptzToggleRepo) + + Ecto.Adapters.SQL.Sandbox.mode( + TimestamptzToggleRepo, + {:shared, self()} + ) + + Ecto.Adapters.SQL.query!( + TimestamptzToggleRepo, + """ + CREATE TABLE timestamptz_posts ( + id uuid PRIMARY KEY, + starts_at timestamptz, + explicit_type timestamp without time zone + ) + """ + ) + + assert AshPostgres.SqlImplementation.storage_type( + TimestamptzPost, + :starts_at + ) == nil + + AshPostgres.MigrationGenerator.generate( + TimestamptzDomain, + snapshot_path: snapshot_path, + migration_path: migration_path, + quiet: true, + format: false, + auto_name: true + ) + + {:ok, timestamptz_post: TimestamptzPost} + end + + test "uses timestamptz for :datetime when enabled", %{ + migration_path: migration_path + } do + files = + Path.wildcard("#{migration_path}/**/*_migrate_resources*.exs") + |> Enum.reject(&String.contains?(&1, "extensions")) + + assert [file] = files + + file_contents = File.read!(file) + + assert file_contents =~ ~S[add :starts_at, :timestamptz] + assert file_contents =~ ~S[add :explicit_type, :utc_datetime] + end + + test "uses timestamptz Ecto type when enabled", %{timestamptz_post: resource} do + assert resource.__schema__(:type, :starts_at) == + {:parameterized, + {AshPostgres.Timestamptz.EctoType, + [precision: :second, cast_dates_as: :start_of_day, timezone: :utc]}} + end + + test "preserves datetime across database timezone changes", %{ + timestamptz_post: resource + } do + datetime = ~U[2026-01-15 12:00:00Z] + + post = + resource + |> Ash.Changeset.for_create(:create, %{starts_at: datetime}, domain: TimestamptzDomain) + |> Ash.create!() + + {:ok, _result} = + Ecto.Adapters.SQL.query( + TimestamptzToggleRepo, + "SET timezone = 'America/Los_Angeles'" + ) + + loaded = + resource + |> Ash.get!(post.id, domain: TimestamptzDomain) + + assert DateTime.compare(loaded.starts_at, datetime) == :eq + end + end + describe "get_operations_from_snapshots" do test "explicit fk attribute order does not change create table emission" do # This also reproduces if a non-identity attribute like :note appears between From a4b6b88cdaf4377569df0e36d6c744d6908a85f5 Mon Sep 17 00:00:00 2001 From: omgitzjoe Date: Mon, 28 Sep 2026 15:55:33 -0600 Subject: [PATCH 2/3] :test add timestamptz toggle DSL --- .../dsls/DSL-AshPostgres.DataLayer.md | 156 +++++++++--------- 1 file changed, 78 insertions(+), 78 deletions(-) diff --git a/documentation/dsls/DSL-AshPostgres.DataLayer.md b/documentation/dsls/DSL-AshPostgres.DataLayer.md index cb13f0ab..b4fc8505 100644 --- a/documentation/dsls/DSL-AshPostgres.DataLayer.md +++ b/documentation/dsls/DSL-AshPostgres.DataLayer.md @@ -3,11 +3,11 @@ This file was generated by Spark. Do not edit it by hand. --> # AshPostgres.DataLayer -A postgres data layer that leverages Ecto's postgres capabilities. +A postgres data layer that leverages Ecto's postgres capabilities. ## postgres -Postgres data layer configuration +Postgres data layer configuration ### Nested DSLs @@ -24,10 +24,10 @@ Postgres data layer configuration ### Examples ``` -postgres do - repo MyApp.Repo - table "organizations" -end +postgres do + repo MyApp.Repo + table "organizations" +end ``` @@ -40,7 +40,7 @@ end |------|------|---------|------| | [`repo`](#postgres-repo){: #postgres-repo .spark-required} | `module \| (any, any -> any)` | | The repo that will be used to fetch your data. See the `AshPostgres.Repo` documentation for more. Can also be a function that takes a resource and a type `:read \| :mutate` and returns the repo | | [`migrate?`](#postgres-migrate?){: #postgres-migrate? } | `boolean` | `true` | Whether or not to include this resource in the generated migrations with `mix ash.generate_migrations` | -| [`view?`](#postgres-view?){: #postgres-view? } | `boolean` | `false` | Declares that the table is a PostgreSQL view (or materialized view) rather than a base table. View resources are excluded from generated migrations, and records returned from upserts into them carry no `:upsert_action` metadata, since views have no `xmax` system column. | +| [`view?`](#postgres-view?){: #postgres-view? } | `boolean` | `false` | Declares that the table is a PostgreSQL view (or materialized view) rather than a base table. View resources are excluded from generated migrations, and records returned from upserts into them carry no `:upsert_action` metadata, since views have no `xmax` system column. | | [`storage_types`](#postgres-storage_types){: #postgres-storage_types } | `keyword` | `[]` | A keyword list of attribute names to the ecto type that should be used for that attribute. Only necessary if you need to override the defaults. | | [`migration_types`](#postgres-migration_types){: #postgres-migration_types } | `keyword` | `[]` | A keyword list of attribute names to the ecto migration type that should be used for that attribute. Only necessary if you need to override the defaults. | | [`migration_defaults`](#postgres-migration_defaults){: #postgres-migration_defaults } | `keyword` | `[]` | A keyword list of attribute names to the ecto migration default that should be used for that attribute. The string you use will be placed verbatim in the migration. Use fragments like `fragment(\\"now()\\")`, or for `nil`, use `\\"nil\\"`. For custom `Ash.Type` modules, see `c:AshPostgres.Type.value_to_postgres_default/3`. | @@ -61,10 +61,10 @@ end ### postgres.custom_indexes -A section for configuring indexes to be created by the migration generator. +A section for configuring indexes to be created by the migration generator. -In general, prefer to use `identities` for simple unique constraints. This is a tool to allow -for declaring more complex indexes. +In general, prefer to use `identities` for simple unique constraints. This is a tool to allow +for declaring more complex indexes. ### Nested DSLs @@ -73,9 +73,9 @@ for declaring more complex indexes. ### Examples ``` -custom_indexes do - index [:column1, :column2], unique: true, where: "thing = TRUE" -end +custom_indexes do + index [:column1, :column2], unique: true, where: "thing = TRUE" +end ``` @@ -88,7 +88,7 @@ index fields ``` -Add an index to be managed by the migration generator. +Add an index to be managed by the migration generator. @@ -104,7 +104,7 @@ index ["column", "column2"], unique: true, where: "thing = TRUE" | Name | Type | Default | Docs | |------|------|---------|------| -| [`fields`](#postgres-custom_indexes-index-fields){: #postgres-custom_indexes-index-fields } | `atom \| String.t \| {:asc \| :desc, atom \| String.t} \| list(atom \| String.t \| {:asc \| :desc, atom \| String.t})` | | The fields to include in the index. Each entry can be an atom, string, or a tuple with an order using :desc or :asc and a field. | +| [`fields`](#postgres-custom_indexes-index-fields){: #postgres-custom_indexes-index-fields } | `atom \| String.t \| {:asc \| :desc, atom \| String.t} \| list(atom \| String.t \| {:asc \| :desc, atom \| String.t})` | | The fields to include in the index. Each entry can be an atom, string, or a tuple with an order using :desc or :asc and a field. | ### Options | Name | Type | Default | Docs | @@ -132,19 +132,19 @@ Target: `AshPostgres.CustomIndex` ### postgres.custom_statements -A section for configuring custom statements to be added to migrations. +A section for configuring custom statements to be added to migrations. -By default, a statement has no declared dependency on other tables, so `down` statements run before any other -operation and `up` statements run after all other operations for that statement's table. If your statement's `up` -depends on structure from another table (e.g. a foreign key referencing a unique index defined via `identities`), -declare it with `after_tables` so the migration generator orders it correctly relative to that table's structure. -Custom statements on the same table run in declaration order, such as creating a function before a trigger that invokes it. +By default, a statement has no declared dependency on other tables, so `down` statements run before any other +operation and `up` statements run after all other operations for that statement's table. If your statement's `up` +depends on structure from another table (e.g. a foreign key referencing a unique index defined via `identities`), +declare it with `after_tables` so the migration generator orders it correctly relative to that table's structure. +Custom statements on the same table run in declaration order, such as creating a function before a trigger that invokes it. -Additionally, when changing a custom statement, we must make some assumptions, i.e that we should migrate -the old structure down using the previously configured `down` and recreate it. +Additionally, when changing a custom statement, we must make some assumptions, i.e that we should migrate +the old structure down using the previously configured `down` and recreate it. -This may not be desired, and so what you may end up doing is simply modifying the old migration and deleting whatever was -generated by the migration generator. As always: read your migrations after generating them! +This may not be desired, and so what you may end up doing is simply modifying the old migration and deleting whatever was +generated by the migration generator. As always: read your migrations after generating them! ### Nested DSLs @@ -153,26 +153,26 @@ generated by the migration generator. As always: read your migrations after gene ### Examples ``` -custom_statements do - # the name is used to detect if you remove or modify the statement - statement :pgweb_idx do - up "CREATE INDEX pgweb_idx ON pgweb USING GIN (to_tsvector('english', title || ' ' || body));" - down "DROP INDEX pgweb_idx;" - end - - statement :children_parent_composite_fk do - # ensures this runs after `parents`'s columns and unique indexes are finalized - after_tables ["parents"] - up "ALTER TABLE children ADD CONSTRAINT children_parent_fk FOREIGN KEY (region_id, parent_id) REFERENCES parents (region_id, id);" - down "ALTER TABLE children DROP CONSTRAINT children_parent_fk;" - end - - statement :create_audit_trigger do - after_tables ["audit_entries"] - up "CREATE TRIGGER ..." - down "DROP TRIGGER ..." - end -end +custom_statements do + # the name is used to detect if you remove or modify the statement + statement :pgweb_idx do + up "CREATE INDEX pgweb_idx ON pgweb USING GIN (to_tsvector('english', title || ' ' || body));" + down "DROP INDEX pgweb_idx;" + end + + statement :children_parent_composite_fk do + # ensures this runs after `parents`'s columns and unique indexes are finalized + after_tables ["parents"] + up "ALTER TABLE children ADD CONSTRAINT children_parent_fk FOREIGN KEY (region_id, parent_id) REFERENCES parents (region_id, id);" + down "ALTER TABLE children DROP CONSTRAINT children_parent_fk;" + end + + statement :create_audit_trigger do + after_tables ["audit_entries"] + up "CREATE TRIGGER ..." + down "DROP TRIGGER ..." + end +end ``` @@ -185,17 +185,17 @@ statement name ``` -Add a custom statement for migrations. +Add a custom statement for migrations. ### Examples ``` -statement :pgweb_idx do - up "CREATE INDEX pgweb_idx ON pgweb USING GIN (to_tsvector('english', title || ' ' || body));" - down "DROP INDEX pgweb_idx;" -end +statement :pgweb_idx do + up "CREATE INDEX pgweb_idx ON pgweb USING GIN (to_tsvector('english', title || ' ' || body));" + down "DROP INDEX pgweb_idx;" +end ``` @@ -226,18 +226,18 @@ Target: `AshPostgres.Statement` ### postgres.manage_tenant -Configuration for the behavior of a resource that manages a tenant +Configuration for the behavior of a resource that manages a tenant ### Examples ``` -manage_tenant do - template ["organization_", :id] - create? true - update? false -end +manage_tenant do + template ["organization_", :id] + create? true + update? false +end ``` @@ -256,10 +256,10 @@ end ### postgres.references -A section for configuring the references (foreign keys) in resource migrations. +A section for configuring the references (foreign keys) in resource migrations. -This section is only relevant if you are using the migration generator with this resource. -Otherwise, it has no effect. +This section is only relevant if you are using the migration generator with this resource. +Otherwise, it has no effect. ### Nested DSLs @@ -268,9 +268,9 @@ Otherwise, it has no effect. ### Examples ``` -references do - reference :post, on_delete: :delete, on_update: :update, name: "comments_to_posts_fkey" -end +references do + reference :post, on_delete: :delete, on_update: :update, name: "comments_to_posts_fkey" +end ``` @@ -292,13 +292,13 @@ reference relationship ``` -Configures the reference for a relationship in resource migrations. +Configures the reference for a relationship in resource migrations. -Keep in mind that multiple relationships can theoretically involve the same destination and foreign keys. -In those cases, you only need to configure the `reference` behavior for one of them. Any conflicts will result -in an error, across this resource and any other resources that share a table with this one. For this reason, -instead of adding a reference configuration for `:nothing`, its best to just leave the configuration out, as that -is the default behavior if *no* relationship anywhere has configured the behavior of that reference. +Keep in mind that multiple relationships can theoretically involve the same destination and foreign keys. +In those cases, you only need to configure the `reference` behavior for one of them. Any conflicts will result +in an error, across this resource and any other resources that share a table with this one. For this reason, +instead of adding a reference configuration for `:nothing`, its best to just leave the configuration out, as that +is the default behavior if *no* relationship anywhere has configured the behavior of that reference. @@ -340,9 +340,9 @@ Target: `AshPostgres.Reference` ### postgres.check_constraints -A section for configuring the check constraints for a given table. +A section for configuring the check constraints for a given table. -This can be used to automatically create those check constraints, or just to provide message when they are raised +This can be used to automatically create those check constraints, or just to provide message when they are raised ### Nested DSLs @@ -351,9 +351,9 @@ This can be used to automatically create those check constraints, or just to pro ### Examples ``` -check_constraints do - check_constraint :price, "price_must_be_positive", check: "price > 0", message: "price must be positive" -end +check_constraints do + check_constraint :price, "price_must_be_positive", check: "price > 0", message: "price must be positive" +end ``` @@ -366,20 +366,20 @@ check_constraint attribute, name ``` -Add a check constraint to be validated. +Add a check constraint to be validated. -If a check constraint exists on the table but not in this section, and it produces an error, a runtime error will be raised. +If a check constraint exists on the table but not in this section, and it produces an error, a runtime error will be raised. -Provide a list of attributes instead of a single attribute to add the message to multiple attributes. +Provide a list of attributes instead of a single attribute to add the message to multiple attributes. -By adding the `check` option, the migration generator will include it when generating migrations. +By adding the `check` option, the migration generator will include it when generating migrations. ### Examples ``` -check_constraint :price, "price_must_be_positive", check: "price > 0", message: "price must be positive" +check_constraint :price, "price_must_be_positive", check: "price > 0", message: "price must be positive" ``` From 7beb02b967cac65a061fc4a9ef1b9619677cdb74 Mon Sep 17 00:00:00 2001 From: omgitzjoe Date: Mon, 28 Sep 2026 17:09:04 -0600 Subject: [PATCH 3/3] chore: restore unrelated files --- .../dsls/DSL-AshPostgres.DataLayer.md | 156 +++++++++--------- 1 file changed, 78 insertions(+), 78 deletions(-) diff --git a/documentation/dsls/DSL-AshPostgres.DataLayer.md b/documentation/dsls/DSL-AshPostgres.DataLayer.md index b4fc8505..cb13f0ab 100644 --- a/documentation/dsls/DSL-AshPostgres.DataLayer.md +++ b/documentation/dsls/DSL-AshPostgres.DataLayer.md @@ -3,11 +3,11 @@ This file was generated by Spark. Do not edit it by hand. --> # AshPostgres.DataLayer -A postgres data layer that leverages Ecto's postgres capabilities. +A postgres data layer that leverages Ecto's postgres capabilities. ## postgres -Postgres data layer configuration +Postgres data layer configuration ### Nested DSLs @@ -24,10 +24,10 @@ Postgres data layer configuration ### Examples ``` -postgres do - repo MyApp.Repo - table "organizations" -end +postgres do + repo MyApp.Repo + table "organizations" +end ``` @@ -40,7 +40,7 @@ end |------|------|---------|------| | [`repo`](#postgres-repo){: #postgres-repo .spark-required} | `module \| (any, any -> any)` | | The repo that will be used to fetch your data. See the `AshPostgres.Repo` documentation for more. Can also be a function that takes a resource and a type `:read \| :mutate` and returns the repo | | [`migrate?`](#postgres-migrate?){: #postgres-migrate? } | `boolean` | `true` | Whether or not to include this resource in the generated migrations with `mix ash.generate_migrations` | -| [`view?`](#postgres-view?){: #postgres-view? } | `boolean` | `false` | Declares that the table is a PostgreSQL view (or materialized view) rather than a base table. View resources are excluded from generated migrations, and records returned from upserts into them carry no `:upsert_action` metadata, since views have no `xmax` system column. | +| [`view?`](#postgres-view?){: #postgres-view? } | `boolean` | `false` | Declares that the table is a PostgreSQL view (or materialized view) rather than a base table. View resources are excluded from generated migrations, and records returned from upserts into them carry no `:upsert_action` metadata, since views have no `xmax` system column. | | [`storage_types`](#postgres-storage_types){: #postgres-storage_types } | `keyword` | `[]` | A keyword list of attribute names to the ecto type that should be used for that attribute. Only necessary if you need to override the defaults. | | [`migration_types`](#postgres-migration_types){: #postgres-migration_types } | `keyword` | `[]` | A keyword list of attribute names to the ecto migration type that should be used for that attribute. Only necessary if you need to override the defaults. | | [`migration_defaults`](#postgres-migration_defaults){: #postgres-migration_defaults } | `keyword` | `[]` | A keyword list of attribute names to the ecto migration default that should be used for that attribute. The string you use will be placed verbatim in the migration. Use fragments like `fragment(\\"now()\\")`, or for `nil`, use `\\"nil\\"`. For custom `Ash.Type` modules, see `c:AshPostgres.Type.value_to_postgres_default/3`. | @@ -61,10 +61,10 @@ end ### postgres.custom_indexes -A section for configuring indexes to be created by the migration generator. +A section for configuring indexes to be created by the migration generator. -In general, prefer to use `identities` for simple unique constraints. This is a tool to allow -for declaring more complex indexes. +In general, prefer to use `identities` for simple unique constraints. This is a tool to allow +for declaring more complex indexes. ### Nested DSLs @@ -73,9 +73,9 @@ for declaring more complex indexes. ### Examples ``` -custom_indexes do - index [:column1, :column2], unique: true, where: "thing = TRUE" -end +custom_indexes do + index [:column1, :column2], unique: true, where: "thing = TRUE" +end ``` @@ -88,7 +88,7 @@ index fields ``` -Add an index to be managed by the migration generator. +Add an index to be managed by the migration generator. @@ -104,7 +104,7 @@ index ["column", "column2"], unique: true, where: "thing = TRUE" | Name | Type | Default | Docs | |------|------|---------|------| -| [`fields`](#postgres-custom_indexes-index-fields){: #postgres-custom_indexes-index-fields } | `atom \| String.t \| {:asc \| :desc, atom \| String.t} \| list(atom \| String.t \| {:asc \| :desc, atom \| String.t})` | | The fields to include in the index. Each entry can be an atom, string, or a tuple with an order using :desc or :asc and a field. | +| [`fields`](#postgres-custom_indexes-index-fields){: #postgres-custom_indexes-index-fields } | `atom \| String.t \| {:asc \| :desc, atom \| String.t} \| list(atom \| String.t \| {:asc \| :desc, atom \| String.t})` | | The fields to include in the index. Each entry can be an atom, string, or a tuple with an order using :desc or :asc and a field. | ### Options | Name | Type | Default | Docs | @@ -132,19 +132,19 @@ Target: `AshPostgres.CustomIndex` ### postgres.custom_statements -A section for configuring custom statements to be added to migrations. +A section for configuring custom statements to be added to migrations. -By default, a statement has no declared dependency on other tables, so `down` statements run before any other -operation and `up` statements run after all other operations for that statement's table. If your statement's `up` -depends on structure from another table (e.g. a foreign key referencing a unique index defined via `identities`), -declare it with `after_tables` so the migration generator orders it correctly relative to that table's structure. -Custom statements on the same table run in declaration order, such as creating a function before a trigger that invokes it. +By default, a statement has no declared dependency on other tables, so `down` statements run before any other +operation and `up` statements run after all other operations for that statement's table. If your statement's `up` +depends on structure from another table (e.g. a foreign key referencing a unique index defined via `identities`), +declare it with `after_tables` so the migration generator orders it correctly relative to that table's structure. +Custom statements on the same table run in declaration order, such as creating a function before a trigger that invokes it. -Additionally, when changing a custom statement, we must make some assumptions, i.e that we should migrate -the old structure down using the previously configured `down` and recreate it. +Additionally, when changing a custom statement, we must make some assumptions, i.e that we should migrate +the old structure down using the previously configured `down` and recreate it. -This may not be desired, and so what you may end up doing is simply modifying the old migration and deleting whatever was -generated by the migration generator. As always: read your migrations after generating them! +This may not be desired, and so what you may end up doing is simply modifying the old migration and deleting whatever was +generated by the migration generator. As always: read your migrations after generating them! ### Nested DSLs @@ -153,26 +153,26 @@ generated by the migration generator. As always: read your migrations after gene ### Examples ``` -custom_statements do - # the name is used to detect if you remove or modify the statement - statement :pgweb_idx do - up "CREATE INDEX pgweb_idx ON pgweb USING GIN (to_tsvector('english', title || ' ' || body));" - down "DROP INDEX pgweb_idx;" - end - - statement :children_parent_composite_fk do - # ensures this runs after `parents`'s columns and unique indexes are finalized - after_tables ["parents"] - up "ALTER TABLE children ADD CONSTRAINT children_parent_fk FOREIGN KEY (region_id, parent_id) REFERENCES parents (region_id, id);" - down "ALTER TABLE children DROP CONSTRAINT children_parent_fk;" - end - - statement :create_audit_trigger do - after_tables ["audit_entries"] - up "CREATE TRIGGER ..." - down "DROP TRIGGER ..." - end -end +custom_statements do + # the name is used to detect if you remove or modify the statement + statement :pgweb_idx do + up "CREATE INDEX pgweb_idx ON pgweb USING GIN (to_tsvector('english', title || ' ' || body));" + down "DROP INDEX pgweb_idx;" + end + + statement :children_parent_composite_fk do + # ensures this runs after `parents`'s columns and unique indexes are finalized + after_tables ["parents"] + up "ALTER TABLE children ADD CONSTRAINT children_parent_fk FOREIGN KEY (region_id, parent_id) REFERENCES parents (region_id, id);" + down "ALTER TABLE children DROP CONSTRAINT children_parent_fk;" + end + + statement :create_audit_trigger do + after_tables ["audit_entries"] + up "CREATE TRIGGER ..." + down "DROP TRIGGER ..." + end +end ``` @@ -185,17 +185,17 @@ statement name ``` -Add a custom statement for migrations. +Add a custom statement for migrations. ### Examples ``` -statement :pgweb_idx do - up "CREATE INDEX pgweb_idx ON pgweb USING GIN (to_tsvector('english', title || ' ' || body));" - down "DROP INDEX pgweb_idx;" -end +statement :pgweb_idx do + up "CREATE INDEX pgweb_idx ON pgweb USING GIN (to_tsvector('english', title || ' ' || body));" + down "DROP INDEX pgweb_idx;" +end ``` @@ -226,18 +226,18 @@ Target: `AshPostgres.Statement` ### postgres.manage_tenant -Configuration for the behavior of a resource that manages a tenant +Configuration for the behavior of a resource that manages a tenant ### Examples ``` -manage_tenant do - template ["organization_", :id] - create? true - update? false -end +manage_tenant do + template ["organization_", :id] + create? true + update? false +end ``` @@ -256,10 +256,10 @@ end ### postgres.references -A section for configuring the references (foreign keys) in resource migrations. +A section for configuring the references (foreign keys) in resource migrations. -This section is only relevant if you are using the migration generator with this resource. -Otherwise, it has no effect. +This section is only relevant if you are using the migration generator with this resource. +Otherwise, it has no effect. ### Nested DSLs @@ -268,9 +268,9 @@ Otherwise, it has no effect. ### Examples ``` -references do - reference :post, on_delete: :delete, on_update: :update, name: "comments_to_posts_fkey" -end +references do + reference :post, on_delete: :delete, on_update: :update, name: "comments_to_posts_fkey" +end ``` @@ -292,13 +292,13 @@ reference relationship ``` -Configures the reference for a relationship in resource migrations. +Configures the reference for a relationship in resource migrations. -Keep in mind that multiple relationships can theoretically involve the same destination and foreign keys. -In those cases, you only need to configure the `reference` behavior for one of them. Any conflicts will result -in an error, across this resource and any other resources that share a table with this one. For this reason, -instead of adding a reference configuration for `:nothing`, its best to just leave the configuration out, as that -is the default behavior if *no* relationship anywhere has configured the behavior of that reference. +Keep in mind that multiple relationships can theoretically involve the same destination and foreign keys. +In those cases, you only need to configure the `reference` behavior for one of them. Any conflicts will result +in an error, across this resource and any other resources that share a table with this one. For this reason, +instead of adding a reference configuration for `:nothing`, its best to just leave the configuration out, as that +is the default behavior if *no* relationship anywhere has configured the behavior of that reference. @@ -340,9 +340,9 @@ Target: `AshPostgres.Reference` ### postgres.check_constraints -A section for configuring the check constraints for a given table. +A section for configuring the check constraints for a given table. -This can be used to automatically create those check constraints, or just to provide message when they are raised +This can be used to automatically create those check constraints, or just to provide message when they are raised ### Nested DSLs @@ -351,9 +351,9 @@ This can be used to automatically create those check constraints, or just to pro ### Examples ``` -check_constraints do - check_constraint :price, "price_must_be_positive", check: "price > 0", message: "price must be positive" -end +check_constraints do + check_constraint :price, "price_must_be_positive", check: "price > 0", message: "price must be positive" +end ``` @@ -366,20 +366,20 @@ check_constraint attribute, name ``` -Add a check constraint to be validated. +Add a check constraint to be validated. -If a check constraint exists on the table but not in this section, and it produces an error, a runtime error will be raised. +If a check constraint exists on the table but not in this section, and it produces an error, a runtime error will be raised. -Provide a list of attributes instead of a single attribute to add the message to multiple attributes. +Provide a list of attributes instead of a single attribute to add the message to multiple attributes. -By adding the `check` option, the migration generator will include it when generating migrations. +By adding the `check` option, the migration generator will include it when generating migrations. ### Examples ``` -check_constraint :price, "price_must_be_positive", check: "price > 0", message: "price must be positive" +check_constraint :price, "price_must_be_positive", check: "price > 0", message: "price must be positive" ```