From d21a8204b5d45347a9c6abb39ca44ed5638605e6 Mon Sep 17 00:00:00 2001 From: Nassim Date: Fri, 30 May 2025 17:37:16 +0200 Subject: [PATCH] feat: strict table support Add option in the DSL to generate [strict table](https://www.sqlite.org/stricttables.html), which enforces types more strictly (default is false). Ecto already supports custom options that will be appended after the generated create table statement https://hexdocs.pm/ecto_sql/Ecto.Migration.html#table/2-options --- documentation/dsls/DSL-AshSqlite.DataLayer.md | 1 + lib/data_layer.ex | 7 +++ lib/data_layer/info.ex | 5 ++ .../migration_generator.ex | 10 ++-- lib/migration_generator/operation.ex | 2 +- lib/migration_generator/phase.ex | 11 +++-- test/migration_generator_test.exs | 47 +++++++++++++++++++ 7 files changed, 76 insertions(+), 7 deletions(-) diff --git a/documentation/dsls/DSL-AshSqlite.DataLayer.md b/documentation/dsls/DSL-AshSqlite.DataLayer.md index 50c4776..777fee2 100644 --- a/documentation/dsls/DSL-AshSqlite.DataLayer.md +++ b/documentation/dsls/DSL-AshSqlite.DataLayer.md @@ -48,6 +48,7 @@ end | [`migration_ignore_attributes`](#sqlite-migration_ignore_attributes){: #sqlite-migration_ignore_attributes } | `list(atom)` | `[]` | A list of attributes that will be ignored when generating migrations. | | [`table`](#sqlite-table){: #sqlite-table } | `String.t` | | The table to store and read the resource from. If this is changed, the migration generator will not remove the old table. | | [`polymorphic?`](#sqlite-polymorphic?){: #sqlite-polymorphic? } | `boolean` | `false` | Declares this resource as polymorphic. See the [polymorphic resources guide](/documentation/topics/resources/polymorphic-resources.md) for more. | +| [`strict?`](#sqlite-strict?){: #sqlite-strict? } | `boolean` | `false` | Whether the migration generator should create a [strict table](https://www.sqlite.org/stricttables.html), which enforces types more strictly. | ### sqlite.custom_indexes diff --git a/lib/data_layer.ex b/lib/data_layer.ex index 09536c3..4c26d83 100644 --- a/lib/data_layer.ex +++ b/lib/data_layer.ex @@ -278,6 +278,13 @@ defmodule AshSqlite.DataLayer do doc: """ Declares this resource as polymorphic. See the [polymorphic resources guide](/documentation/topics/resources/polymorphic-resources.md) for more. """ + ], + strict?: [ + type: :boolean, + default: false, + doc: """ + Whether the migration generator should create a [strict table](https://www.sqlite.org/stricttables.html), which enforces types more strictly. + """ ] ] } diff --git a/lib/data_layer/info.ex b/lib/data_layer/info.ex index abb5193..2a77e2b 100644 --- a/lib/data_layer/info.ex +++ b/lib/data_layer/info.ex @@ -114,4 +114,9 @@ defmodule AshSqlite.DataLayer.Info do def skip_unique_indexes(resource) do Extension.get_opt(resource, [:sqlite], :skip_unique_indexes, []) end + + @doc "Whether the migration generator should create a strict table" + def strict?(resource) do + Extension.get_opt(resource, [:sqlite], :strict?, false) + end end diff --git a/lib/migration_generator/migration_generator.ex b/lib/migration_generator/migration_generator.ex index 90ee0c8..274f69a 100644 --- a/lib/migration_generator/migration_generator.ex +++ b/lib/migration_generator/migration_generator.ex @@ -1103,7 +1103,8 @@ defmodule AshSqlite.MigrationGenerator do defp group_into_phases( [ - %Operation.CreateTable{table: table, multitenancy: multitenancy} | rest + %Operation.CreateTable{table: table, options: options, multitenancy: multitenancy} + | rest ], nil, acc @@ -1120,6 +1121,7 @@ defmodule AshSqlite.MigrationGenerator do %Phase.Create{ table: table, multitenancy: multitenancy, + options: options, operations: has_to_be_in_this_phase }, acc @@ -1543,7 +1545,8 @@ defmodule AshSqlite.MigrationGenerator do %Operation.CreateTable{ table: snapshot.table, multitenancy: snapshot.multitenancy, - old_multitenancy: empty_snapshot.multitenancy + old_multitenancy: empty_snapshot.multitenancy, + options: [strict?: snapshot.strict?] } | acc ]) @@ -2204,7 +2207,8 @@ defmodule AshSqlite.MigrationGenerator do repo: AshSqlite.DataLayer.Info.repo(resource), multitenancy: multitenancy(resource), base_filter: AshSqlite.DataLayer.Info.base_filter_sql(resource), - has_create_action: has_create_action?(resource) + has_create_action: has_create_action?(resource), + strict?: AshSqlite.DataLayer.Info.strict?(resource) } hash = diff --git a/lib/migration_generator/operation.ex b/lib/migration_generator/operation.ex index 54d2a8b..107158a 100644 --- a/lib/migration_generator/operation.ex +++ b/lib/migration_generator/operation.ex @@ -67,7 +67,7 @@ defmodule AshSqlite.MigrationGenerator.Operation do defmodule CreateTable do @moduledoc false - defstruct [:table, :multitenancy, :old_multitenancy] + defstruct [:table, :multitenancy, :old_multitenancy, options: []] end defmodule AddAttribute do diff --git a/lib/migration_generator/phase.ex b/lib/migration_generator/phase.ex index 1ed4f3e..cfc7316 100644 --- a/lib/migration_generator/phase.ex +++ b/lib/migration_generator/phase.ex @@ -3,12 +3,17 @@ defmodule AshSqlite.MigrationGenerator.Phase do defmodule Create do @moduledoc false - defstruct [:table, :multitenancy, operations: [], commented?: false] + defstruct [:table, :multitenancy, operations: [], options: [], commented?: false] import AshSqlite.MigrationGenerator.Operation.Helper, only: [as_atom: 1] - def up(%{table: table, operations: operations}) do - opts = "" + def up(%{table: table, operations: operations, options: options}) do + opts = + if options[:strict?] do + ~s', options: "STRICT"' + else + "" + end "create table(:#{as_atom(table)}, primary_key: false#{opts}) do\n" <> Enum.map_join(operations, "\n", fn operation -> operation.__struct__.up(operation) end) <> diff --git a/test/migration_generator_test.exs b/test/migration_generator_test.exs index a0a5e3d..3976cf2 100644 --- a/test/migration_generator_test.exs +++ b/test/migration_generator_test.exs @@ -145,6 +145,53 @@ defmodule AshSqlite.MigrationGeneratorTest do end end + describe "strict table" do + setup do + on_exit(fn -> + File.rm_rf!("test_snapshots_path") + File.rm_rf!("test_migration_path") + end) + + defposts do + sqlite do + strict?(true) + end + + attributes do + uuid_primary_key(:id) + attribute(:title, :string) + end + end + + defdomain([Post]) + + Mix.shell(Mix.Shell.Process) + + AshSqlite.MigrationGenerator.generate(Domain, + snapshot_path: "test_snapshots_path", + migration_path: "test_migration_path", + quiet: true, + format: false, + auto_name: true + ) + + :ok + end + + test "creates the table with the strict option" do + # the snapshot exists and contains valid json + assert File.read!(Path.wildcard("test_snapshots_path/test_repo/posts/*.json")) + |> Jason.decode!(keys: :atoms!) + + assert [file] = Path.wildcard("test_migration_path/**/*_migrate_resources*.exs") + + file_contents = File.read!(file) + + # the migration creates the table + assert file_contents =~ ~s'create table(:posts, primary_key: false, options: "STRICT") do' + end + end + describe "creating follow up migrations" do setup do on_exit(fn ->