From 7548e124a175f9613f17fd87012e27e19a79c7e8 Mon Sep 17 00:00:00 2001 From: Elliot Bowes Date: Sun, 5 Oct 2025 16:35:37 +0100 Subject: [PATCH 1/3] improvement: support non-public postgres schemas in resource generator - Add schema field to generated resources when table is in non-public schema - Fix SQL queries to use schema-qualified table names for foreign keys and constraints - Update index queries to respect actual schema instead of hardcoded 'public' - Add test coverage for tables in custom schemas with foreign keys and indexes --- lib/resource_generator/resource_generator.ex | 7 + lib/resource_generator/spec.ex | 16 ++- test/resource_generator_test.exs | 132 +++++++++++++++++++ 3 files changed, 151 insertions(+), 4 deletions(-) diff --git a/lib/resource_generator/resource_generator.ex b/lib/resource_generator/resource_generator.ex index 99da8386..b3a507b7 100644 --- a/lib/resource_generator/resource_generator.ex +++ b/lib/resource_generator/resource_generator.ex @@ -115,6 +115,7 @@ if Code.ensure_loaded?(Igniter) do postgres do table #{inspect(table_spec.table_name)} repo #{inspect(table_spec.repo)} + #{schema_option(table_spec)} #{no_migrate_flag} #{references(table_spec, opts[:no_migrations])} #{custom_indexes(table_spec, opts[:no_migrations])} @@ -144,6 +145,12 @@ if Code.ensure_loaded?(Igniter) do end) end + defp schema_option(%{schema: schema}) when schema != "public" do + "schema #{inspect(schema)}" + end + + defp schema_option(_), do: "" + defp default_actions(opts) do cond do opts[:default_actions] && opts[:public] -> diff --git a/lib/resource_generator/spec.ex b/lib/resource_generator/spec.ex index f2da7ec5..9997bbac 100644 --- a/lib/resource_generator/spec.ex +++ b/lib/resource_generator/spec.ex @@ -122,7 +122,13 @@ defmodule AshPostgres.ResourceGenerator.Spec do result end + defp qualified_table_name(%{schema: schema, table_name: table_name}) do + "#{schema}.#{table_name}" + end + defp add_foreign_keys(spec) do + qualified_table = qualified_table_name(spec) + %Postgrex.Result{rows: fkey_rows} = spec.repo.query!( """ @@ -178,7 +184,7 @@ defmodule AshPostgres.ResourceGenerator.Spec do constraints.update_rule, constraints.delete_rule """, - [spec.table_name, spec.schema], + [qualified_table, spec.schema], log: false ) @@ -218,6 +224,8 @@ defmodule AshPostgres.ResourceGenerator.Spec do end defp add_check_constraints(spec) do + qualified_table = qualified_table_name(spec) + %Postgrex.Result{rows: check_constraint_rows} = spec.repo.query!( """ @@ -230,7 +238,7 @@ defmodule AshPostgres.ResourceGenerator.Spec do contype = 'c' AND conrelid::regclass::text = $1 """, - [spec.table_name], + [qualified_table], log: false ) @@ -278,7 +286,7 @@ defmodule AshPostgres.ResourceGenerator.Spec do LEFT JOIN pg_constraint c ON c.conindid = ix.indexrelid AND c.contype = 'p' JOIN - pg_indexes idx ON idx.indexname = i.relname AND idx.schemaname = 'public' -- Adjust schema name if necessary + pg_indexes idx ON idx.indexname = i.relname AND idx.schemaname = $2 JOIN information_schema.tables ta ON ta.table_name = t.relname WHERE @@ -312,7 +320,7 @@ defmodule AshPostgres.ResourceGenerator.Spec do LEFT JOIN pg_constraint c ON c.conindid = ix.indexrelid AND c.contype = 'p' JOIN - pg_indexes idx ON idx.indexname = i.relname AND idx.schemaname = 'public' -- Adjust schema name if necessary + pg_indexes idx ON idx.indexname = i.relname AND idx.schemaname = $2 JOIN information_schema.tables ta ON ta.table_name = t.relname WHERE diff --git a/test/resource_generator_test.exs b/test/resource_generator_test.exs index c50292aa..ba42e624 100644 --- a/test/resource_generator_test.exs +++ b/test/resource_generator_test.exs @@ -62,4 +62,136 @@ defmodule AshPostgres.ResourceGeenratorTests do end """) end + + test "a resource is generated from a table in a non-public schema with foreign keys and indexes" do + AshPostgres.TestRepo.query!("CREATE SCHEMA IF NOT EXISTS inventory") + + AshPostgres.TestRepo.query!("DROP TABLE IF EXISTS inventory.products CASCADE") + AshPostgres.TestRepo.query!("DROP TABLE IF EXISTS inventory.warehouses CASCADE") + + AshPostgres.TestRepo.query!(""" + CREATE TABLE inventory.warehouses ( + id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, + name VARCHAR(255) NOT NULL, + location VARCHAR(255) + ) + """) + + AshPostgres.TestRepo.query!( + "CREATE INDEX warehouses_name_idx ON inventory.warehouses(name)" + ) + + AshPostgres.TestRepo.query!(""" + CREATE TABLE inventory.products ( + id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, + name VARCHAR(255) NOT NULL, + warehouse_id UUID REFERENCES inventory.warehouses(id) ON DELETE CASCADE, + quantity INTEGER + ) + """) + + AshPostgres.TestRepo.query!( + "CREATE INDEX products_warehouse_id_idx ON inventory.products(warehouse_id)" + ) + + test_project() + |> Igniter.compose_task("ash_postgres.gen.resources", [ + "MyApp.Inventory", + "--tables", + "inventory.warehouses,inventory.products", + "--yes", + "--repo", + "AshPostgres.TestRepo" + ]) + |> assert_creates("lib/my_app/inventory/warehouse.ex", """ + defmodule MyApp.Inventory.Warehouse do + use Ash.Resource, + domain: MyApp.Inventory, + data_layer: AshPostgres.DataLayer + + actions do + defaults([:read, :destroy, create: :*, update: :*]) + end + + postgres do + table("warehouses") + repo(AshPostgres.TestRepo) + schema("inventory") + end + + attributes do + uuid_primary_key :id do + public?(true) + end + + attribute :name, :string do + allow_nil?(false) + public?(true) + end + + attribute :location, :string do + public?(true) + end + end + + relationships do + has_many :products, MyApp.Inventory.Product do + public?(true) + end + end + end + """) + |> assert_creates("lib/my_app/inventory/product.ex", """ + defmodule MyApp.Inventory.Product do + use Ash.Resource, + domain: MyApp.Inventory, + data_layer: AshPostgres.DataLayer + + actions do + defaults([:read, :destroy, create: :*, update: :*]) + end + + postgres do + table("products") + repo(AshPostgres.TestRepo) + schema("inventory") + + references do + reference :warehouse do + on_delete(:delete) + end + end + end + + attributes do + uuid_primary_key :id do + public?(true) + end + + uuid_primary_key :id do + public?(true) + end + + attribute :name, :string do + public?(true) + end + + attribute :name, :string do + allow_nil?(false) + public?(true) + end + + attribute :quantity, :integer do + public?(true) + end + end + + relationships do + belongs_to :warehouse, MyApp.Inventory.Warehouse do + public?(true) + end + end + end + """) + end end From be2ba322e6b8c4f8f79a6bfbed08adc3f419c59d Mon Sep 17 00:00:00 2001 From: Elliot Bowes Date: Sun, 5 Oct 2025 17:11:19 +0100 Subject: [PATCH 2/3] fix: guard against missing snapshot directories in migration generator Fixes crash when generating migrations for resources in non-public schemas where the snapshot directory doesn't exist yet. --- .../migration_generator.ex | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/lib/migration_generator/migration_generator.ex b/lib/migration_generator/migration_generator.ex index fcb44c95..00276e25 100644 --- a/lib/migration_generator/migration_generator.ex +++ b/lib/migration_generator/migration_generator.ex @@ -649,14 +649,19 @@ defmodule AshPostgres.MigrationGenerator do folder = get_snapshot_folder(snapshot, opts) snapshot_path = get_snapshot_path(snapshot, folder) - snapshot_path - |> File.ls!() - |> Enum.filter(&String.contains?(&1, "_dev.json")) - |> Enum.each(fn snapshot_name -> + # Guard against missing directories - can happen for new resources or when + # get_snapshot_path's fallback logic returns a non-existent path for + # resources in non-public schemas + if File.dir?(snapshot_path) do snapshot_path - |> Path.join(snapshot_name) - |> File.rm!() - end) + |> File.ls!() + |> Enum.filter(&String.contains?(&1, "_dev.json")) + |> Enum.each(fn snapshot_name -> + snapshot_path + |> Path.join(snapshot_name) + |> File.rm!() + end) + end end) end From e3b2a1cfde9811168294e753b96a6ce8f2567a02 Mon Sep 17 00:00:00 2001 From: Elliot Bowes Date: Sun, 5 Oct 2025 22:15:22 +0100 Subject: [PATCH 3/3] chore: run mix format --- test/resource_generator_test.exs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/resource_generator_test.exs b/test/resource_generator_test.exs index ba42e624..4878cf6d 100644 --- a/test/resource_generator_test.exs +++ b/test/resource_generator_test.exs @@ -77,9 +77,7 @@ defmodule AshPostgres.ResourceGeenratorTests do ) """) - AshPostgres.TestRepo.query!( - "CREATE INDEX warehouses_name_idx ON inventory.warehouses(name)" - ) + AshPostgres.TestRepo.query!("CREATE INDEX warehouses_name_idx ON inventory.warehouses(name)") AshPostgres.TestRepo.query!(""" CREATE TABLE inventory.products (