From a4d370acd9f9f115a9aafd30d07dcf30775bc0a8 Mon Sep 17 00:00:00 2001 From: Lucas Mazza Date: Thu, 13 Aug 2026 11:42:05 -0300 Subject: [PATCH] Sort column definitions stably in `order_column_definitions` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Running `db:migrate` on macOS reordered CHECK constraint lines in db/structure.sql, while the same migration in Docker left the file untouched. Same gem version, same Ruby, same database — only the platform differed. `order_column_definitions` keys each line inside a CREATE TABLE body on its first word. Column lines key on the column name, so they get unique keys. Every inline CHECK constraint keys on the literal word "CONSTRAINT", so they all tie. `sort_by!` is not stable and delegates to the platform's qsort: glibc happens to preserve the input order of tied lines, BSD shuffles them. Breaking ties by the original index keeps pg_dump's own ordering, which is what the Linux output already produced. Feeding a real 1558-constraint structure.sql back through the sort now returns it byte for byte, so existing schema files see no churn. One caveat on the regression test: it fails on macOS without the fix, but passes either way on glibc, where the unstable sort happens to do the right thing. CI runs on ubuntu, so it guards the behavior rather than reproducing the failure there. --- lib/activerecord-clean-db-structure/clean_dump.rb | 6 +++++- test/clean_dump_test.rb | 11 +++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/lib/activerecord-clean-db-structure/clean_dump.rb b/lib/activerecord-clean-db-structure/clean_dump.rb index db51094..c4f0aa0 100644 --- a/lib/activerecord-clean-db-structure/clean_dump.rb +++ b/lib/activerecord-clean-db-structure/clean_dump.rb @@ -192,7 +192,11 @@ def order_column_definitions(source) elsif source_line.start_with?(")") if inside_table inside_table = false - columns.sort_by!(&:first) + + # Every inline CHECK constraint parses to the same name ("CONSTRAINT"), and + # Ruby's sort is not stable, so tied lines get reordered differently depending + # on the platform's qsort. Breaking ties by index keeps pg_dump's own ordering. + columns = columns.sort_by.with_index { |(name, _line), index| [name, index] } columns[0..-2].each do |_, line| result << with_column_separator[line] diff --git a/test/clean_dump_test.rb b/test/clean_dump_test.rb index 7421344..8ad79c0 100644 --- a/test/clean_dump_test.rb +++ b/test/clean_dump_test.rb @@ -13,6 +13,17 @@ def test_order_column_definitions assert_cleans_dump "data/input.sql", "expectations/order_column_definitions.sql", order_column_definitions: true end + def test_order_column_definitions_keeps_the_order_of_inline_constraints + names = %w[alpha bravo charlie delta echo foxtrot golf hotel india juliet kilo lima] + constraints = names.map { |name| " CONSTRAINT things_#{name}_check CHECK ((#{name} > 0)),\n" } + source = "CREATE TABLE public.things (\n id bigint NOT NULL,\n#{constraints.join})\n" + + cleaner = ActiveRecordCleanDbStructure::CleanDump.new(+"", order_column_definitions: true) + dump = cleaner.order_column_definitions(source) + + assert_equal names, dump.scan(/things_(\w+)_check/).flatten + end + def test_order_schema_migrations_values assert_cleans_dump "data/input.sql", "expectations/order_schema_migrations_values.sql", order_schema_migrations_values: true end