Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 16 additions & 8 deletions lib/migration_generator/migration_generator.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3872,7 +3872,7 @@ defmodule AshPostgres.MigrationGenerator do
end

defp do_snapshot(resource, table, schema \\ nil) do
snapshot = %{
%{
attributes: attributes(resource, table),
identities: identities(resource),
table: table || AshPostgres.DataLayer.Info.table(resource),
Expand All @@ -3887,13 +3887,6 @@ defmodule AshPostgres.MigrationGenerator do
has_create_action: has_create_action?(resource),
create_table_options: AshPostgres.DataLayer.Info.create_table_options(resource)
}

hash =
:sha256
|> :crypto.hash(inspect(snapshot))
|> Base.encode16()

Map.put(snapshot, :hash, hash)
end

defp has_create_action?(resource) do
Expand Down Expand Up @@ -4426,7 +4419,22 @@ defmodule AshPostgres.MigrationGenerator do
AshPostgres.DataLayer.Info.migration_defaults(resource)[attribute]
end

# The hash is taken over the encoded snapshot rather than the term, because the
# encoding sorts keys and so doesn't depend on the runtime's map key order.
defp snapshot_to_binary(snapshot) do
snapshot = Map.delete(snapshot, :hash)

hash =
:sha256
|> :crypto.hash(encode_snapshot(snapshot))
|> Base.encode16()

snapshot
|> Map.put(:hash, hash)
|> encode_snapshot()
end

defp encode_snapshot(snapshot) do
snapshot
|> Map.update!(:attributes, fn attributes ->
Enum.map(attributes, &attribute_to_binary/1)
Expand Down
52 changes: 52 additions & 0 deletions test/migration_generator_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -600,6 +600,58 @@ defmodule AshPostgres.MigrationGeneratorTest do
end
end

describe "snapshot hashes" do
setup %{snapshot_path: snapshot_path, migration_path: migration_path} do
defposts do
attributes do
uuid_primary_key(:id)
attribute(:title, :string, public?: true)
end
end

defcomments do
attributes do
uuid_primary_key(:id)
end

relationships do
belongs_to(:post, Post, public?: true)
end
end

defdomain([Post, Comment])

generate_post_migration(Domain, snapshot_path, migration_path)

:ok
end

test "the hash is derived from the canonical snapshot contents", %{
snapshot_path: snapshot_path
} do
# The hash must be taken over the snapshot as written. Hashing the snapshot term
# (e.g. via `inspect/1`) isn't stable, because map key order depends on the
# runtime's atom table, and the snapshot is normalised again before it's written.
files = Path.wildcard("#{snapshot_path}/test_repo/{posts,comments}/*.json")
assert [_, _] = files

for file <- files do
{hash, snapshot} =
file
|> File.read!()
|> Jason.decode!(objects: :ordered_objects)
|> Access.pop("hash")

expected =
:sha256
|> :crypto.hash(Jason.encode!(snapshot, pretty: true))
|> Base.encode16()

assert hash == expected
end
end
end

describe "creating initial snapshots with native uuidv7 on PG 18" do
setup %{snapshot_path: snapshot_path, migration_path: migration_path} do
prev_pg_version_env = System.fetch_env("PG_VERSION")
Expand Down