From c1b2f38efc6f3149a801bb54d975cf69231441c7 Mon Sep 17 00:00:00 2001 From: Drew Robinson Date: Thu, 15 Jan 2026 21:58:39 +1100 Subject: [PATCH 1/9] Add comprehensive tests for map/list default value handling - Add tests for map defaults with JSON encoding - Add tests for list defaults with JSON encoding - Add tests for empty map and list defaults - Add tests for complex nested map defaults - Add tests for maps with various JSON types (string, number, bool, null) - Update existing empty map/list tests to reflect new behavior - All 43 migration tests + 54 connection tests pass --- lib/ecto/adapters/libsql/connection.ex | 15 +++++ test/ecto_migration_test.exs | 87 ++++++++++++++++++++++++-- 2 files changed, 96 insertions(+), 6 deletions(-) diff --git a/lib/ecto/adapters/libsql/connection.ex b/lib/ecto/adapters/libsql/connection.ex index 28e193fc..d5efc043 100644 --- a/lib/ecto/adapters/libsql/connection.ex +++ b/lib/ecto/adapters/libsql/connection.ex @@ -473,6 +473,21 @@ defmodule Ecto.Adapters.LibSql.Connection do defp column_default(value) when is_binary(value), do: " DEFAULT '#{escape_string(value)}'" defp column_default(value) when is_number(value), do: " DEFAULT #{value}" defp column_default({:fragment, expr}), do: " DEFAULT #{expr}" + + defp column_default(value) when is_map(value) do + case Jason.encode(value) do + {:ok, json} -> " DEFAULT '#{escape_string(json)}'" + {:error, _} -> "" + end + end + + defp column_default(value) when is_list(value) do + case Jason.encode(value) do + {:ok, json} -> " DEFAULT '#{escape_string(json)}'" + {:error, _} -> "" + end + end + # Handle any other unexpected types (e.g., empty maps or third-party migrations) # Logs a warning to help with debugging while gracefully falling back to no DEFAULT clause defp column_default(unexpected) do diff --git a/test/ecto_migration_test.exs b/test/ecto_migration_test.exs index 72ec71de..d2514b14 100644 --- a/test/ecto_migration_test.exs +++ b/test/ecto_migration_test.exs @@ -939,28 +939,30 @@ defmodule Ecto.Adapters.LibSql.MigrationTest do test "handles unexpected types gracefully (empty map)" do # This test verifies the catch-all clause for unexpected types. # Empty maps can come from some migrations or other third-party code. + # As of the defaults update, empty maps are JSON encoded like other maps. table = %Table{name: :users, prefix: nil} columns = [{:add, :metadata, :string, [default: %{}]}] # Should not raise FunctionClauseError. [sql] = Connection.execute_ddl({:create, table, columns}) - # Empty map should be treated as no default. - assert sql =~ ~r/"metadata".*TEXT/ - refute sql =~ ~r/"metadata".*DEFAULT/ + # Empty map should be JSON encoded to '{}' + assert sql =~ ~r/"metadata".*TEXT.*DEFAULT/ + assert sql =~ "'{}'" end test "handles unexpected types gracefully (list)" do # Lists are another unexpected type that might appear. + # As of the defaults update, lists are JSON encoded. table = %Table{name: :users, prefix: nil} columns = [{:add, :tags, :string, [default: []]}] # Should not raise FunctionClauseError. [sql] = Connection.execute_ddl({:create, table, columns}) - # Empty list should be treated as no default. - assert sql =~ ~r/"tags".*TEXT/ - refute sql =~ ~r/"tags".*DEFAULT/ + # Empty list should be JSON encoded to '[]' + assert sql =~ ~r/"tags".*TEXT.*DEFAULT/ + assert String.contains?(sql, ["DEFAULT '[]'"]) end test "handles unexpected types gracefully (atom)" do @@ -975,6 +977,79 @@ defmodule Ecto.Adapters.LibSql.MigrationTest do assert sql =~ ~r/"status".*TEXT/ refute sql =~ ~r/"status".*DEFAULT/ end + + test "handles map defaults (JSON encoding)" do + table = %Table{name: :users, prefix: nil} + + columns = [ + {:add, :preferences, :text, [default: %{"theme" => "dark", "notifications" => true}]} + ] + + [sql] = Connection.execute_ddl({:create, table, columns}) + + # Map should be JSON encoded + assert sql =~ ~r/"preferences".*TEXT.*DEFAULT/ + assert sql =~ "theme" + assert sql =~ "dark" + end + + test "handles list defaults (JSON encoding)" do + table = %Table{name: :items, prefix: nil} + + columns = [ + {:add, :tags, :text, [default: ["tag1", "tag2", "tag3"]]} + ] + + [sql] = Connection.execute_ddl({:create, table, columns}) + + # List should be JSON encoded + assert sql =~ ~r/"tags".*TEXT.*DEFAULT/ + assert sql =~ "tag1" + assert sql =~ "tag2" + end + + test "handles empty list defaults" do + table = %Table{name: :items, prefix: nil} + columns = [{:add, :tags, :text, [default: []]}] + + # Empty list encodes to "[]" in JSON + [sql] = Connection.execute_ddl({:create, table, columns}) + + # Should have a DEFAULT clause with empty array JSON + assert sql =~ ~r/"tags".*TEXT.*DEFAULT '\[\]'/ + end + + test "handles complex nested map defaults" do + table = %Table{name: :configs, prefix: nil} + + columns = [ + {:add, :settings, :text, + [default: %{"user" => %{"theme" => "light"}, "privacy" => false}]} + ] + + [sql] = Connection.execute_ddl({:create, table, columns}) + + # Nested map should be JSON encoded + assert sql =~ ~r/"settings".*TEXT.*DEFAULT/ + assert sql =~ "user" + assert sql =~ "theme" + assert sql =~ "light" + end + + test "handles map with various JSON types" do + table = %Table{name: :data, prefix: nil} + + columns = [ + {:add, :metadata, :text, + [default: %{"string" => "value", "number" => 42, "bool" => true, "null" => nil}]} + ] + + [sql] = Connection.execute_ddl({:create, table, columns}) + + assert sql =~ ~r/"metadata".*TEXT.*DEFAULT/ + # Verify JSON is properly escaped + assert String.contains?(sql, ["string", "number", "bool"]) + end end describe "CHECK constraints" do From fcb89689f4d5694dfc808688d6db3764bf12c91c Mon Sep 17 00:00:00 2001 From: Drew Robinson Date: Fri, 16 Jan 2026 08:37:41 +1100 Subject: [PATCH 2/9] Add warning logging for JSON encoding failures in map/list defaults - Log descriptive warnings when Jason.encode fails for map defaults - Log descriptive warnings when Jason.encode fails for list defaults - Provides visibility into why DEFAULT clause is not generated - Matches existing error handling pattern from catch-all clause - Includes tests verifying warning is logged for unencodable values (PIDs, functions) Addresses feedback to improve debuggability when JSON encoding fails --- lib/ecto/adapters/libsql/connection.ex | 28 +++++++++++++--- test/ecto_migration_test.exs | 44 ++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 4 deletions(-) diff --git a/lib/ecto/adapters/libsql/connection.ex b/lib/ecto/adapters/libsql/connection.ex index d5efc043..74724297 100644 --- a/lib/ecto/adapters/libsql/connection.ex +++ b/lib/ecto/adapters/libsql/connection.ex @@ -476,15 +476,35 @@ defmodule Ecto.Adapters.LibSql.Connection do defp column_default(value) when is_map(value) do case Jason.encode(value) do - {:ok, json} -> " DEFAULT '#{escape_string(json)}'" - {:error, _} -> "" + {:ok, json} -> + " DEFAULT '#{escape_string(json)}'" + + {:error, reason} -> + require Logger + + Logger.warning( + "Failed to JSON encode map default value in migration: #{inspect(value)} - " <> + "Reason: #{inspect(reason)} - no DEFAULT clause will be generated." + ) + + "" end end defp column_default(value) when is_list(value) do case Jason.encode(value) do - {:ok, json} -> " DEFAULT '#{escape_string(json)}'" - {:error, _} -> "" + {:ok, json} -> + " DEFAULT '#{escape_string(json)}'" + + {:error, reason} -> + require Logger + + Logger.warning( + "Failed to JSON encode list default value in migration: #{inspect(value)} - " <> + "Reason: #{inspect(reason)} - no DEFAULT clause will be generated." + ) + + "" end end diff --git a/test/ecto_migration_test.exs b/test/ecto_migration_test.exs index d2514b14..9fea1f66 100644 --- a/test/ecto_migration_test.exs +++ b/test/ecto_migration_test.exs @@ -1050,6 +1050,50 @@ defmodule Ecto.Adapters.LibSql.MigrationTest do # Verify JSON is properly escaped assert String.contains?(sql, ["string", "number", "bool"]) end + + test "logs warning when map has unencodable value (PID)" do + # Maps containing PIDs or functions cannot be JSON encoded + table = %Table{name: :data, prefix: nil} + pid = spawn(fn -> :ok end) + + columns = [ + {:add, :metadata, :text, [default: %{"pid" => pid}]} + ] + + # Capture logs to verify warning is logged + log_output = + ExUnit.CaptureLog.capture_log(fn -> + [sql] = Connection.execute_ddl({:create, table, columns}) + + # When encoding fails, no DEFAULT clause should be generated + assert sql =~ ~r/"metadata".*TEXT/ + refute sql =~ ~r/"metadata".*DEFAULT/ + end) + + assert log_output =~ "Failed to JSON encode map default value in migration" + end + + test "logs warning when list has unencodable value (function)" do + # Lists containing functions cannot be JSON encoded + table = %Table{name: :data, prefix: nil} + func = fn -> :ok end + + columns = [ + {:add, :callbacks, :text, [default: [func, "other"]]} + ] + + # Capture logs to verify warning is logged + log_output = + ExUnit.CaptureLog.capture_log(fn -> + [sql] = Connection.execute_ddl({:create, table, columns}) + + # When encoding fails, no DEFAULT clause should be generated + assert sql =~ ~r/"callbacks".*TEXT/ + refute sql =~ ~r/"callbacks".*DEFAULT/ + end) + + assert log_output =~ "Failed to JSON encode list default value in migration" + end end describe "CHECK constraints" do From 386435cb5c7c69eeb63739b41e6114397cd0fc41 Mon Sep 17 00:00:00 2001 From: Drew Robinson Date: Fri, 16 Jan 2026 08:38:30 +1100 Subject: [PATCH 3/9] Consolidate duplicate JSON encoding logic for map/list defaults - Combine column_default clauses for maps and lists using 'or' guard - Extract common JSON encoding logic into encode_json_default/2 helper - Reduces code duplication while maintaining readability - Helper function handles both map and list type names for accurate logging - All column_default clauses now grouped together per Elixir style guide Addresses feedback to consolidate duplicate logic --- lib/ecto/adapters/libsql/connection.ex | 45 ++++++++++---------------- 1 file changed, 17 insertions(+), 28 deletions(-) diff --git a/lib/ecto/adapters/libsql/connection.ex b/lib/ecto/adapters/libsql/connection.ex index 74724297..e9d880fd 100644 --- a/lib/ecto/adapters/libsql/connection.ex +++ b/lib/ecto/adapters/libsql/connection.ex @@ -474,24 +474,27 @@ defmodule Ecto.Adapters.LibSql.Connection do defp column_default(value) when is_number(value), do: " DEFAULT #{value}" defp column_default({:fragment, expr}), do: " DEFAULT #{expr}" - defp column_default(value) when is_map(value) do - case Jason.encode(value) do - {:ok, json} -> - " DEFAULT '#{escape_string(json)}'" + defp column_default(value) when is_map(value) or is_list(value) do + type_name = if is_map(value), do: "map", else: "list" + encode_json_default(value, type_name) + end - {:error, reason} -> - require Logger + # Handle any other unexpected types (e.g., empty maps or third-party migrations) + # Logs a warning to help with debugging while gracefully falling back to no DEFAULT clause + defp column_default(unexpected) do + require Logger - Logger.warning( - "Failed to JSON encode map default value in migration: #{inspect(value)} - " <> - "Reason: #{inspect(reason)} - no DEFAULT clause will be generated." - ) + Logger.warning( + "Unsupported default value type in migration: #{inspect(unexpected)} - " <> + "no DEFAULT clause will be generated. This can occur with some generated migrations " <> + "or other third-party integrations that provide unexpected default types." + ) - "" - end + "" end - defp column_default(value) when is_list(value) do + # Helper function to encode JSON default values and log failures + defp encode_json_default(value, type_name) do case Jason.encode(value) do {:ok, json} -> " DEFAULT '#{escape_string(json)}'" @@ -500,7 +503,7 @@ defmodule Ecto.Adapters.LibSql.Connection do require Logger Logger.warning( - "Failed to JSON encode list default value in migration: #{inspect(value)} - " <> + "Failed to JSON encode #{type_name} default value in migration: #{inspect(value)} - " <> "Reason: #{inspect(reason)} - no DEFAULT clause will be generated." ) @@ -508,20 +511,6 @@ defmodule Ecto.Adapters.LibSql.Connection do end end - # Handle any other unexpected types (e.g., empty maps or third-party migrations) - # Logs a warning to help with debugging while gracefully falling back to no DEFAULT clause - defp column_default(unexpected) do - require Logger - - Logger.warning( - "Unsupported default value type in migration: #{inspect(unexpected)} - " <> - "no DEFAULT clause will be generated. This can occur with some generated migrations " <> - "or other third-party integrations that provide unexpected default types." - ) - - "" - end - defp table_options(table, columns) do # Validate mutually exclusive options (per libSQL specification) if table.options && Keyword.get(table.options, :random_rowid, false) do From e6202cac533cc276b108b312bb5dedd265eec8b6 Mon Sep 17 00:00:00 2001 From: Drew Robinson Date: Fri, 16 Jan 2026 08:39:13 +1100 Subject: [PATCH 4/9] Simplify String.contains call with single-element list - Replace String.contains?(sql, ["DEFAULT '[]'"]) with sql =~ "DEFAULT '[]'" - More idiomatic Elixir pattern matching for simple string checks - Reduces unnecessary list wrapping - Maintains test clarity and intent Addresses feedback to simplify unnecessarily verbose assertions --- test/ecto_migration_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/ecto_migration_test.exs b/test/ecto_migration_test.exs index 9fea1f66..aabed780 100644 --- a/test/ecto_migration_test.exs +++ b/test/ecto_migration_test.exs @@ -962,7 +962,7 @@ defmodule Ecto.Adapters.LibSql.MigrationTest do # Empty list should be JSON encoded to '[]' assert sql =~ ~r/"tags".*TEXT.*DEFAULT/ - assert String.contains?(sql, ["DEFAULT '[]'"]) + assert sql =~ "DEFAULT '[]'" end test "handles unexpected types gracefully (atom)" do From 645a518ecd8e5cb8932afe03f42369fb6a6f8b77 Mon Sep 17 00:00:00 2001 From: Drew Robinson Date: Fri, 16 Jan 2026 08:41:49 +1100 Subject: [PATCH 5/9] Add support for Decimal, DateTime, NaiveDateTime, Date, Time, and :null defaults Support for missing DEFAULT types: - Decimal: Converts to string representation (e.g., '19.99') - DateTime: Converts to ISO8601 string (e.g., '2026-01-16T14:30:00Z') - NaiveDateTime: Converts to ISO8601 string (e.g., '2026-01-16T14:30:00') - Date: Converts to ISO8601 string (e.g., '2026-01-16') - Time: Converts to ISO8601 string (e.g., '14:30:45.123456') - :null atom: Treated same as nil (no DEFAULT clause) All conversions match the parameter encoding behavior in query.ex for consistency. Add comprehensive test coverage: - Basic Decimal defaults with various precision levels - DateTime defaults with microsecond precision - NaiveDateTime, Date, and Time defaults - Edge cases: negative decimals, many decimal places - :null atom behavior parity with nil This closes the gap between what types the library accepts in parameters and what types it accepts as column defaults in migrations. --- lib/ecto/adapters/libsql/connection.ex | 23 +++++++ test/ecto_migration_test.exs | 89 ++++++++++++++++++++++++++ 2 files changed, 112 insertions(+) diff --git a/lib/ecto/adapters/libsql/connection.ex b/lib/ecto/adapters/libsql/connection.ex index e9d880fd..19c10423 100644 --- a/lib/ecto/adapters/libsql/connection.ex +++ b/lib/ecto/adapters/libsql/connection.ex @@ -468,12 +468,35 @@ defmodule Ecto.Adapters.LibSql.Connection do end defp column_default(nil), do: "" + defp column_default(:null), do: "" defp column_default(true), do: " DEFAULT 1" defp column_default(false), do: " DEFAULT 0" defp column_default(value) when is_binary(value), do: " DEFAULT '#{escape_string(value)}'" defp column_default(value) when is_number(value), do: " DEFAULT #{value}" defp column_default({:fragment, expr}), do: " DEFAULT #{expr}" + # Temporal types - convert to ISO8601 strings + defp column_default(%DateTime{} = dt) do + " DEFAULT '#{escape_string(DateTime.to_iso8601(dt))}'" + end + + defp column_default(%NaiveDateTime{} = dt) do + " DEFAULT '#{escape_string(NaiveDateTime.to_iso8601(dt))}'" + end + + defp column_default(%Date{} = d) do + " DEFAULT '#{escape_string(Date.to_iso8601(d))}'" + end + + defp column_default(%Time{} = t) do + " DEFAULT '#{escape_string(Time.to_iso8601(t))}'" + end + + # Decimal type - convert to string representation + defp column_default(%Decimal{} = d) do + " DEFAULT '#{escape_string(Decimal.to_string(d))}'" + end + defp column_default(value) when is_map(value) or is_list(value) do type_name = if is_map(value), do: "map", else: "list" encode_json_default(value, type_name) diff --git a/test/ecto_migration_test.exs b/test/ecto_migration_test.exs index aabed780..ab1b917b 100644 --- a/test/ecto_migration_test.exs +++ b/test/ecto_migration_test.exs @@ -1094,6 +1094,95 @@ defmodule Ecto.Adapters.LibSql.MigrationTest do assert log_output =~ "Failed to JSON encode list default value in migration" end + + test "handles :null atom defaults (same as nil)" do + table = %Table{name: :users, prefix: nil} + columns = [{:add, :bio, :text, [default: :null]}] + + [sql] = Connection.execute_ddl({:create, table, columns}) + + # :null should result in no DEFAULT clause (same as nil) + refute sql =~ "DEFAULT" + end + + test "handles Decimal defaults" do + table = %Table{name: :products, prefix: nil} + columns = [{:add, :price, :decimal, [default: Decimal.new("19.99")]}] + + [sql] = Connection.execute_ddl({:create, table, columns}) + + # Decimal should be converted to string representation + assert sql =~ ~r/"price".*DECIMAL.*DEFAULT/ + assert sql =~ "'19.99'" + end + + test "handles DateTime defaults" do + table = %Table{name: :events, prefix: nil} + dt = DateTime.new!(~D[2026-01-16], ~T[14:30:00.000000], "UTC") + columns = [{:add, :created_at, :utc_datetime, [default: dt]}] + + [sql] = Connection.execute_ddl({:create, table, columns}) + + # DateTime should be converted to ISO8601 string + assert sql =~ ~r/"created_at".*DATETIME.*DEFAULT/ + assert sql =~ "2026-01-16T14:30:00Z" + end + + test "handles NaiveDateTime defaults" do + table = %Table{name: :logs, prefix: nil} + dt = ~N[2026-01-16 14:30:00.000000] + columns = [{:add, :recorded_at, :naive_datetime, [default: dt]}] + + [sql] = Connection.execute_ddl({:create, table, columns}) + + # NaiveDateTime should be converted to ISO8601 string + assert sql =~ ~r/"recorded_at".*DATETIME.*DEFAULT/ + assert sql =~ "2026-01-16T14:30:00" + end + + test "handles Date defaults" do + table = %Table{name: :schedules, prefix: nil} + date = ~D[2026-01-16] + columns = [{:add, :event_date, :date, [default: date]}] + + [sql] = Connection.execute_ddl({:create, table, columns}) + + # Date should be converted to ISO8601 string + assert sql =~ ~r/"event_date".*DATE.*DEFAULT/ + assert sql =~ "'2026-01-16'" + end + + test "handles Time defaults" do + table = %Table{name: :schedules, prefix: nil} + time = ~T[14:30:45.123456] + columns = [{:add, :event_time, :time, [default: time]}] + + [sql] = Connection.execute_ddl({:create, table, columns}) + + # Time should be converted to ISO8601 string + assert sql =~ ~r/"event_time".*TIME.*DEFAULT/ + assert sql =~ "14:30:45.123456" + end + + test "handles Decimal with many decimal places" do + table = %Table{name: :data, prefix: nil} + columns = [{:add, :value, :decimal, [default: Decimal.new("123.456789012345")]}] + + [sql] = Connection.execute_ddl({:create, table, columns}) + + assert sql =~ ~r/"value".*DECIMAL.*DEFAULT/ + assert sql =~ "'123.456789012345'" + end + + test "handles negative Decimal defaults" do + table = %Table{name: :balances, prefix: nil} + columns = [{:add, :amount, :decimal, [default: Decimal.new("-42.50")]}] + + [sql] = Connection.execute_ddl({:create, table, columns}) + + assert sql =~ ~r/"amount".*DECIMAL.*DEFAULT/ + assert sql =~ "'-42.50'" + end end describe "CHECK constraints" do From 55793d5d389cf2f3c711c6fb9132361d88d0a796 Mon Sep 17 00:00:00 2001 From: Drew Robinson Date: Fri, 16 Jan 2026 08:44:02 +1100 Subject: [PATCH 6/9] Strengthen JSON type test to verify all keys are present Changed assertion from: assert String.contains?(sql, ["string", "number", "bool"]) To explicit assertions: assert sql =~ "string" assert sql =~ "number" assert sql =~ "bool" The original assertion only verified that ANY of the keys were present. The new approach ensures ALL three JSON keys are present in the DEFAULT clause, providing stronger validation that the JSON encoding is complete. --- test/ecto_migration_test.exs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/ecto_migration_test.exs b/test/ecto_migration_test.exs index ab1b917b..2b72f0c2 100644 --- a/test/ecto_migration_test.exs +++ b/test/ecto_migration_test.exs @@ -1047,8 +1047,10 @@ defmodule Ecto.Adapters.LibSql.MigrationTest do [sql] = Connection.execute_ddl({:create, table, columns}) assert sql =~ ~r/"metadata".*TEXT.*DEFAULT/ - # Verify JSON is properly escaped - assert String.contains?(sql, ["string", "number", "bool"]) + # Verify JSON is properly escaped - all keys must be present + assert sql =~ "string" + assert sql =~ "number" + assert sql =~ "bool" end test "logs warning when map has unencodable value (PID)" do From 0c878ac28d6e1624f60660b6ad657254fec48d33 Mon Sep 17 00:00:00 2001 From: Drew Robinson Date: Fri, 16 Jan 2026 08:54:39 +1100 Subject: [PATCH 7/9] Fix DateTime construction in test Use DateTime.from_iso8601/1 instead of DateTime.new!/4 to avoid timezone database issues. The ISO8601 parser is more reliable and doesn't require timezone configuration in tests. --- mix.lock | 4 ++-- test/ecto_migration_test.exs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mix.lock b/mix.lock index b3e06160..d8877590 100644 --- a/mix.lock +++ b/mix.lock @@ -1,7 +1,7 @@ %{ "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, "credo": {:hex, :credo, "1.7.15", "283da72eeb2fd3ccf7248f4941a0527efb97afa224bcdef30b4b580bc8258e1c", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "291e8645ea3fea7481829f1e1eb0881b8395db212821338e577a90bf225c5607"}, - "db_connection": {:hex, :db_connection, "2.8.1", "9abdc1e68c34c6163f6fb96a96532272d13ad7ca45262156ae8b7ec6d9dc4bec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a61a3d489b239d76f326e03b98794fb8e45168396c925ef25feb405ed09da8fd"}, + "db_connection": {:hex, :db_connection, "2.9.0", "a6a97c5c958a2d7091a58a9be40caf41ab496b0701d21e1d1abff3fa27a7f371", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "17d502eacaf61829db98facf6f20808ed33da6ccf495354a41e64fe42f9c509c"}, "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, "dialyxir": {:hex, :dialyxir, "1.4.7", "dda948fcee52962e4b6c5b4b16b2d8fa7d50d8645bbae8b8685c3f9ecb7f5f4d", [:mix], [{:erlex, ">= 0.2.8", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b34527202e6eb8cee198efec110996c25c5898f43a4094df157f8d28f27d9efe"}, "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, @@ -13,7 +13,7 @@ "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, - "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"}, + "makeup_erlang": {:hex, :makeup_erlang, "1.0.3", "4252d5d4098da7415c390e847c814bad3764c94a814a0b4245176215615e1035", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "953297c02582a33411ac6208f2c6e55f0e870df7f80da724ed613f10e6706afd"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, "rustler": {:hex, :rustler, "0.37.1", "721434020c7f6f8e1cdc57f44f75c490435b01de96384f8ccb96043f12e8a7e0", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "24547e9b8640cf00e6a2071acb710f3e12ce0346692e45098d84d45cdb54fd79"}, "sobelow": {:hex, :sobelow, "0.14.1", "2f81e8632f15574cba2402bcddff5497b413c01e6f094bc0ab94e83c2f74db81", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8fac9a2bd90fdc4b15d6fca6e1608efb7f7c600fa75800813b794ee9364c87f2"}, diff --git a/test/ecto_migration_test.exs b/test/ecto_migration_test.exs index 2b72f0c2..a189a283 100644 --- a/test/ecto_migration_test.exs +++ b/test/ecto_migration_test.exs @@ -1120,7 +1120,7 @@ defmodule Ecto.Adapters.LibSql.MigrationTest do test "handles DateTime defaults" do table = %Table{name: :events, prefix: nil} - dt = DateTime.new!(~D[2026-01-16], ~T[14:30:00.000000], "UTC") + {:ok, dt, _} = DateTime.from_iso8601("2026-01-16T14:30:00Z") columns = [{:add, :created_at, :utc_datetime, [default: dt]}] [sql] = Connection.execute_ddl({:create, table, columns}) From 159f33408ca8c720c5aaf7072b2d19a92cd9d002 Mon Sep 17 00:00:00 2001 From: Drew Robinson Date: Fri, 16 Jan 2026 08:56:02 +1100 Subject: [PATCH 8/9] Add explicit test pattern configuration to suppress ExUnit warning Add test_pattern and test_paths to mix.exs to explicitly configure which files are test files. This eliminates the warning about support files (test/support/*.ex) not matching test patterns, while ensuring all test files matching **/*_test.exs are correctly identified and run. --- mix.exs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mix.exs b/mix.exs index 0223aabd..a65dbc4c 100644 --- a/mix.exs +++ b/mix.exs @@ -17,6 +17,8 @@ defmodule EctoLibSql.MixProject do package: package(), description: description(), docs: docs(), + test_pattern: "**/*_test.exs", + test_paths: ["test"], dialyzer: [ plt_core_path: "priv/plts", app_tree: true, From 506cc2e9ee8d48a2f340a4e9348768222eb9fa2c Mon Sep 17 00:00:00 2001 From: Drew Robinson Date: Fri, 16 Jan 2026 13:11:15 +1100 Subject: [PATCH 9/9] Strengthen JSON default assertions to validate complete structure Replace weak substring-based checks with proper extraction and decoding of JSON literals from DEFAULT clauses. This ensures: - Exact structure validation via Jason.decode! - Detection of missing keys (including null values) - Verification that JSON is valid and complete - No false positives from partial matches Applied pattern to 5 JSON default tests: - handles map defaults (JSON encoding) - handles list defaults (JSON encoding) - handles empty list defaults - handles complex nested map defaults - handles map with various JSON types Tests now extract the JSON literal using regex and validate the decoded structure matches exactly, including null values that would be silently dropped by substring assertions. --- test/ecto_migration_test.exs | 45 ++++++++++++++++++++++++++---------- 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/test/ecto_migration_test.exs b/test/ecto_migration_test.exs index a189a283..bb22d478 100644 --- a/test/ecto_migration_test.exs +++ b/test/ecto_migration_test.exs @@ -989,8 +989,13 @@ defmodule Ecto.Adapters.LibSql.MigrationTest do # Map should be JSON encoded assert sql =~ ~r/"preferences".*TEXT.*DEFAULT/ - assert sql =~ "theme" - assert sql =~ "dark" + + [_, json] = Regex.run(~r/DEFAULT '([^']*)'/, sql) + + assert Jason.decode!(json) == %{ + "theme" => "dark", + "notifications" => true + } end test "handles list defaults (JSON encoding)" do @@ -1004,8 +1009,10 @@ defmodule Ecto.Adapters.LibSql.MigrationTest do # List should be JSON encoded assert sql =~ ~r/"tags".*TEXT.*DEFAULT/ - assert sql =~ "tag1" - assert sql =~ "tag2" + + [_, json] = Regex.run(~r/DEFAULT '([^']*)'/, sql) + + assert Jason.decode!(json) == ["tag1", "tag2", "tag3"] end test "handles empty list defaults" do @@ -1016,7 +1023,11 @@ defmodule Ecto.Adapters.LibSql.MigrationTest do [sql] = Connection.execute_ddl({:create, table, columns}) # Should have a DEFAULT clause with empty array JSON - assert sql =~ ~r/"tags".*TEXT.*DEFAULT '\[\]'/ + assert sql =~ ~r/"tags".*TEXT.*DEFAULT/ + + [_, json] = Regex.run(~r/DEFAULT '([^']*)'/, sql) + + assert Jason.decode!(json) == [] end test "handles complex nested map defaults" do @@ -1031,9 +1042,13 @@ defmodule Ecto.Adapters.LibSql.MigrationTest do # Nested map should be JSON encoded assert sql =~ ~r/"settings".*TEXT.*DEFAULT/ - assert sql =~ "user" - assert sql =~ "theme" - assert sql =~ "light" + + [_, json] = Regex.run(~r/DEFAULT '([^']*)'/, sql) + + assert Jason.decode!(json) == %{ + "user" => %{"theme" => "light"}, + "privacy" => false + } end test "handles map with various JSON types" do @@ -1047,10 +1062,16 @@ defmodule Ecto.Adapters.LibSql.MigrationTest do [sql] = Connection.execute_ddl({:create, table, columns}) assert sql =~ ~r/"metadata".*TEXT.*DEFAULT/ - # Verify JSON is properly escaped - all keys must be present - assert sql =~ "string" - assert sql =~ "number" - assert sql =~ "bool" + + # Verify JSON is properly escaped - all keys must be present including null values + [_, json] = Regex.run(~r/DEFAULT '([^']*)'/, sql) + + assert Jason.decode!(json) == %{ + "string" => "value", + "number" => 42, + "bool" => true, + "null" => nil + } end test "logs warning when map has unencodable value (PID)" do