From ff769454e222748433747dafad049ef9cf682d58 Mon Sep 17 00:00:00 2001 From: sloane Date: Wed, 13 Nov 2024 08:35:47 -0500 Subject: [PATCH] feat: infer belongs_to type via ecto introspection --- .formatter.exs | 4 +- config/config.exs | 3 ++ config/dev.exs | 0 config/prod.exs | 0 config/test.exs | 8 ++++ mix.exs | 15 ++++++- mix.lock | 3 ++ priv/repo/migrations/.formatter.exs | 4 ++ .../20241113140930_create_test_tables.exs | 18 +++++++++ test/support/data_case.ex | 21 ++++++++++ test/support/repo.ex | 5 +++ test/test_helper.exs | 1 + test/type_id/ecto_test.exs | 39 +++++++++++++++++++ 13 files changed, 119 insertions(+), 2 deletions(-) create mode 100644 config/config.exs create mode 100644 config/dev.exs create mode 100644 config/prod.exs create mode 100644 config/test.exs create mode 100644 priv/repo/migrations/.formatter.exs create mode 100644 priv/repo/migrations/20241113140930_create_test_tables.exs create mode 100644 test/support/data_case.ex create mode 100644 test/support/repo.ex create mode 100644 test/type_id/ecto_test.exs diff --git a/.formatter.exs b/.formatter.exs index d2cda26..b42ddd7 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -1,4 +1,6 @@ # Used by "mix format" [ - inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] + import_deps: [:ecto, :ecto_sql], + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"], + subdirectories: ["priv/*/migrations"] ] diff --git a/config/config.exs b/config/config.exs new file mode 100644 index 0000000..d1186fe --- /dev/null +++ b/config/config.exs @@ -0,0 +1,3 @@ +import Config + +import_config "#{config_env()}.exs" diff --git a/config/dev.exs b/config/dev.exs new file mode 100644 index 0000000..e69de29 diff --git a/config/prod.exs b/config/prod.exs new file mode 100644 index 0000000..e69de29 diff --git a/config/test.exs b/config/test.exs new file mode 100644 index 0000000..3902d2f --- /dev/null +++ b/config/test.exs @@ -0,0 +1,8 @@ +import Config + +config :typeid_elixir, ecto_repos: [TypeID.Repo] + +config :typeid_elixir, TypeID.Repo, + database: "type_id_test", + hostname: "localhost", + pool: Ecto.Adapters.SQL.Sandbox diff --git a/mix.exs b/mix.exs index 3b95bfc..cc50406 100644 --- a/mix.exs +++ b/mix.exs @@ -8,9 +8,11 @@ defmodule TypeID.MixProject do app: :typeid_elixir, version: @version, elixir: "~> 1.11", + elixirc_paths: elixirc_paths(Mix.env()), start_permanent: Mix.env() == :prod, description: description(), package: package(), + aliases: aliases(), deps: deps(), name: "TypeID Elixir", source_url: "https://github.com/sloanelybutsurely/typeid-elixir", @@ -23,6 +25,9 @@ defmodule TypeID.MixProject do [extra_applications: [:logger]] end + defp elixirc_paths(:test), do: ["lib", "test/support"] + defp elixirc_paths(_), do: ["lib"] + defp description do "A type-safe, K-sortable, globally unique identifier inspired by Stripe IDs" end @@ -48,7 +53,15 @@ defmodule TypeID.MixProject do {:phoenix, "~> 1.7", optional: true}, {:jason, "~> 1.4", optional: true}, {:ex_doc, "~> 0.27", only: :dev, runtime: false}, - {:yaml_elixir, "~> 2.9", only: [:dev, :test], runtime: false} + {:yaml_elixir, "~> 2.9", only: [:dev, :test], runtime: false}, + {:ecto_sql, "~> 3.10", only: [:dev, :test]}, + {:postgrex, "~> 0.17", only: [:dev, :test]} + ] + end + + defp aliases do + [ + test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"] ] end end diff --git a/mix.lock b/mix.lock index e449106..c1d92a4 100644 --- a/mix.lock +++ b/mix.lock @@ -1,8 +1,10 @@ %{ "castore": {:hex, :castore, "1.0.3", "7130ba6d24c8424014194676d608cb989f62ef8039efd50ff4b3f33286d06db8", [:mix], [], "hexpm", "680ab01ef5d15b161ed6a95449fac5c6b8f60055677a8e79acf01b27baa4390b"}, + "db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"}, "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, "earmark_parser": {:hex, :earmark_parser, "1.4.32", "fa739a0ecfa34493de19426681b23f6814573faee95dfd4b4aafe15a7b5b32c6", [:mix], [], "hexpm", "b8b0dd77d60373e77a3d7e8afa598f325e49e8663a51bcc2b88ef41838cca755"}, "ecto": {:hex, :ecto, "3.10.2", "6b887160281a61aa16843e47735b8a266caa437f80588c3ab80a8a960e6abe37", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6a895778f0d7648a4b34b486af59a1c8009041fbdf2b17f1ac215eb829c60235"}, + "ecto_sql": {:hex, :ecto_sql, "3.10.2", "6b98b46534b5c2f8b8b5f03f126e75e2a73c64f3c071149d32987a5378b0fdbd", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.10.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "68c018debca57cb9235e3889affdaec7a10616a4e3a80c99fa1d01fdafaa9007"}, "ex_doc": {:hex, :ex_doc, "0.29.4", "6257ecbb20c7396b1fe5accd55b7b0d23f44b6aa18017b415cb4c2b91d997729", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "2c6699a737ae46cb61e4ed012af931b57b699643b24dabe2400a8168414bc4f5"}, "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, @@ -16,6 +18,7 @@ "phoenix_template": {:hex, :phoenix_template, "1.0.1", "85f79e3ad1b0180abb43f9725973e3b8c2c3354a87245f91431eec60553ed3ef", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "157dc078f6226334c91cb32c1865bf3911686f8bcd6bcff86736f6253e6993ee"}, "plug": {:hex, :plug, "1.14.2", "cff7d4ec45b4ae176a227acd94a7ab536d9b37b942c8e8fa6dfc0fff98ff4d80", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "842fc50187e13cf4ac3b253d47d9474ed6c296a8732752835ce4a86acdf68d13"}, "plug_crypto": {:hex, :plug_crypto, "1.2.5", "918772575e48e81e455818229bf719d4ab4181fcbf7f85b68a35620f78d89ced", [:mix], [], "hexpm", "26549a1d6345e2172eb1c233866756ae44a9609bd33ee6f99147ab3fd87fd842"}, + "postgrex": {:hex, :postgrex, "0.17.5", "0483d054938a8dc069b21bdd636bf56c487404c241ce6c319c1f43588246b281", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "50b8b11afbb2c4095a3ba675b4f055c416d0f3d7de6633a595fc131a828a67eb"}, "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, "websock": {:hex, :websock, "0.5.2", "b3c08511d8d79ed2c2f589ff430bd1fe799bb389686dafce86d28801783d8351", [:mix], [], "hexpm", "925f5de22fca6813dfa980fb62fd542ec43a2d1a1f83d2caec907483fe66ff05"}, "websock_adapter": {:hex, :websock_adapter, "0.5.3", "4908718e42e4a548fc20e00e70848620a92f11f7a6add8cf0886c4232267498d", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "cbe5b814c1f86b6ea002b52dd99f345aeecf1a1a6964e209d208fb404d930d3d"}, diff --git a/priv/repo/migrations/.formatter.exs b/priv/repo/migrations/.formatter.exs new file mode 100644 index 0000000..49f9151 --- /dev/null +++ b/priv/repo/migrations/.formatter.exs @@ -0,0 +1,4 @@ +[ + import_deps: [:ecto_sql], + inputs: ["*.exs"] +] diff --git a/priv/repo/migrations/20241113140930_create_test_tables.exs b/priv/repo/migrations/20241113140930_create_test_tables.exs new file mode 100644 index 0000000..80e57ab --- /dev/null +++ b/priv/repo/migrations/20241113140930_create_test_tables.exs @@ -0,0 +1,18 @@ +defmodule TypeID.Repo.Migrations.CreateTestTables do + use Ecto.Migration + + def change do + create table(:posts, primary_key: false) do + add :id, :uuid, primary_key: true + add :content, :text + end + + create table(:comments, primary_key: false) do + add :id, :uuid, primary_key: true + add :post_id, references(:posts, type: :uuid, on_delete: :delete_all) + add :content, :text + end + + create index(:comments, [:post_id], name: :comments_post_id_index) + end +end diff --git a/test/support/data_case.ex b/test/support/data_case.ex new file mode 100644 index 0000000..cd092ce --- /dev/null +++ b/test/support/data_case.ex @@ -0,0 +1,21 @@ +defmodule TypeID.DataCase do + use ExUnit.CaseTemplate + + alias Ecto.Adapters.SQL.Sandbox + + using do + quote do + import TypeID.DataCase + end + end + + setup tags do + TypeID.DataCase.setup_sandbox(tags) + :ok + end + + def setup_sandbox(tags) do + pid = Sandbox.start_owner!(TypeID.Repo, shared: not tags[:async]) + on_exit(fn -> Sandbox.stop_owner(pid) end) + end +end diff --git a/test/support/repo.ex b/test/support/repo.ex new file mode 100644 index 0000000..d889e36 --- /dev/null +++ b/test/support/repo.ex @@ -0,0 +1,5 @@ +defmodule TypeID.Repo do + use Ecto.Repo, + otp_app: :typeid_elixir, + adapter: Ecto.Adapters.Postgres +end diff --git a/test/test_helper.exs b/test/test_helper.exs index 869559e..d186afc 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1 +1,2 @@ +TypeID.Repo.start_link() ExUnit.start() diff --git a/test/type_id/ecto_test.exs b/test/type_id/ecto_test.exs new file mode 100644 index 0000000..9e14056 --- /dev/null +++ b/test/type_id/ecto_test.exs @@ -0,0 +1,39 @@ +defmodule TypeID.EctoTest do + use TypeID.DataCase, async: true + + describe "belongs_to relationships" do + defmodule Post do + use Ecto.Schema + + @primary_key {:id, TypeID, autogenerate: true, prefix: "post", type: :uuid} + @foreign_key_type TypeID + schema "posts" do + field :content + has_many :comments, TypeID.EctoTest.Comment + end + end + + defmodule Comment do + use Ecto.Schema + + @primary_key {:id, TypeID, autogenerate: true, prefix: "post", type: :uuid} + @foreign_key_type TypeID + schema "comments" do + field :content + belongs_to :post, TypeID.EctoTest.Post + end + end + + test "infers the database type for a belongs_to relationship" do + post_changeset = Ecto.Changeset.change(%Post{}, %{content: "Hello, World!"}) + assert {:ok, post} = TypeID.Repo.insert(post_changeset) + + comment_changeset = + post + |> Ecto.build_assoc(:comments) + |> Ecto.Changeset.change(%{content: "Great post!"}) + + assert {:ok, comment} = TypeID.Repo.insert(comment_changeset) + end + end +end