unsort_db_schema_columns 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 2d5ae8c8bb1699839dfbbd549d0c82241a2d5f8625227057b8a541f04c8485cb
4
+ data.tar.gz: 153946257a01842d4a7957d9e36ac0ce0fcd5dc9394de43f25ec941921730dad
5
+ SHA512:
6
+ metadata.gz: 4166346b74a7139b5c00e84f12e5e3ef5fa3fce72e8f7c7b918063690e6c23186bd814db0408b85f9ecbb1f6b6d561fe88d4ed29fb3072b2c9970200aa856215
7
+ data.tar.gz: '096ad720d60c32088ea1a4f634455cbdf9d1c67d3782197fd034302b078fac79fab14b70a15c873959c12894ae546d11d444e33270f271aa6bdd0f7600d672af'
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jake Moffatt
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,62 @@
1
+ # unsort_db_schema_columns
2
+
3
+ Restore the natural (database ordinal) column order in Rails `schema.rb` dumps.
4
+
5
+ ## Why this exists
6
+
7
+ In [rails/rails#53281](https://github.com/rails/rails/pull/53281), Rails 8 changed `ActiveRecord::SchemaDumper` to sort table columns alphabetically. The intent was to reduce merge conflicts when two developers add migrations to the same table on parallel branches.
8
+
9
+ The downside is that `schema.rb` no longer reflects the actual column layout of the database. That breaks several real workflows:
10
+
11
+ - **`db:schema:load` for production parity.** Since Rails 8 ([#52830](https://github.com/rails/rails/pull/52830)), a fresh `db:migrate` loads `schema.rb` instead of replaying migrations. With sorting on, freshly-loaded databases have a different column order than production databases built incrementally from migrations.
12
+ - **Postgres column-alignment padding.** Postgres pads columns to alignment boundaries; deliberate column ordering (e.g. 8-byte → 4-byte → variable-length) can meaningfully reduce table size. Alphabetical sorting destroys any layout chosen for this reason.
13
+ - **`SELECT *` and bulk-import pipelines.** Tools like `pg_dump`/`mysqldump` round-trips, `PG::Connection#put_copy_data`, and CSV import/export depend on ordinal column position. Mismatched dev/prod column order breaks them.
14
+ - **`add_column :after`/`:before`.** Rails supports positional column options in migrations. With alphabetical sorting in the dump, those options have no effect on schema-loaded databases.
15
+ - **Gems that read `columns_hash` order.** The Rails change affects in-memory ordering after schema reload, not just the file (e.g. it broke a label-picking heuristic in Bullet Train).
16
+
17
+ For full discussion, see [PR #55414](https://github.com/rails/rails/pull/55414) (opt-in restoration), [PR #56842](https://github.com/rails/rails/pull/56842) (full revert), and the original [PR #53281](https://github.com/rails/rails/pull/53281).
18
+
19
+ ## Who should use this
20
+
21
+ You probably want this gem if any of these are true:
22
+
23
+ - You use `db:schema:load` (or Rails 8's `db:migrate` on fresh DBs) to provision new environments and need it to match production column order.
24
+ - You hand-tune column order in migrations for Postgres alignment-padding reasons.
25
+ - You have CSV import/export, `pg_dump` round-trip, or `put_copy_data` pipelines.
26
+ - You use `add_column :after` / `:before` and expect it to stick.
27
+
28
+ You probably don't need it if you're a small team where merge conflicts in `schema.rb` were the bigger pain than any of the above.
29
+
30
+ ## Install
31
+
32
+ ```ruby
33
+ # Gemfile
34
+ gem "unsort_db_schema_columns"
35
+ ```
36
+
37
+ That's it. A Railtie auto-applies the patch when ActiveRecord loads.
38
+
39
+ To regenerate `schema.rb` in DB-natural order:
40
+
41
+ ```sh
42
+ bin/rails db:schema:dump
43
+ ```
44
+
45
+ Note: the gem only affects future dumps. The `schema.rb` already on disk is whatever was last committed. To get a correct natural-order `schema.rb`, run `db:schema:dump` against a database whose column order matches production (typically one built by replaying migrations, not loaded from a sorted schema).
46
+
47
+ ## How it works
48
+
49
+ Prepends `ActiveRecord::SchemaDumper` and overrides `#table` with a copy of the upstream method that omits the `.sort_by(&:name)` call introduced in PR #53281. A boot-time warning fires if loaded against an untested ActiveRecord major version, since the copied method body could drift.
50
+
51
+ ## Why only columns?
52
+
53
+ `SchemaDumper` also sorts tables, indexes, foreign keys, check constraints, and unique constraints. Those sorts have been in Rails for years (some since 2009) and this gem leaves them alone — only column ordering inside a table has functional consequences in the database engine (Postgres alignment padding, `SELECT *` ordinal position, `pg_dump`/`put_copy_data` round-trips, `add_column :after`/`:before`). Tables, indexes, and constraints are independent named objects; the order they're created has no effect on engine behavior, so sorting them in `schema.rb` is pure diff-stability with no downside.
54
+
55
+ ## Caveats
56
+
57
+ - Coupled to ActiveRecord 8.x's `SchemaDumper#table` method body. When you upgrade Rails, re-diff against [`activerecord/lib/active_record/schema_dumper.rb`](https://github.com/rails/rails/blob/main/activerecord/lib/active_record/schema_dumper.rb) and bump the version constraint here.
58
+ - The merge-conflict problem #53281 was solving is back. That's the intended trade.
59
+
60
+ ## License
61
+
62
+ MIT.
@@ -0,0 +1,11 @@
1
+ require "rails/railtie"
2
+
3
+ module UnsortDbSchemaColumns
4
+ class Railtie < ::Rails::Railtie
5
+ initializer "unsort_db_schema_columns.patch_schema_dumper" do
6
+ ActiveSupport.on_load(:active_record) do
7
+ require "unsort_db_schema_columns/schema_dumper_patch"
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,104 @@
1
+ require "active_record"
2
+ require "active_record/schema_dumper"
3
+
4
+ module UnsortDbSchemaColumns
5
+ # Restores the pre-#53281 behavior: dump columns in the order the database
6
+ # returns them (ordinal position), instead of alphabetically by name.
7
+ #
8
+ # We override the entire #table method because Rails sorts inline:
9
+ # columns.sort_by(&:name).each do |column|
10
+ # The body below is copied from
11
+ # activerecord/lib/active_record/schema_dumper.rb at the Rails 8.x main
12
+ # branch, with that single `.sort_by(&:name)` removed. Verified against
13
+ # Rails 8.0.x. If you upgrade Rails and this gem hasn't, watch for an
14
+ # UnexpectedRailsVersion warning at boot.
15
+ module SchemaDumperPatch
16
+ def table(table, stream)
17
+ columns = @connection.columns(table)
18
+ begin
19
+ self.table_name = table
20
+
21
+ tbl = StringIO.new
22
+
23
+ # first dump primary key column
24
+ if @connection.respond_to?(:primary_keys)
25
+ pk = @connection.primary_keys(table)
26
+ pk = pk.first unless pk.size > 1
27
+ else
28
+ pk = @connection.primary_key(table)
29
+ end
30
+
31
+ tbl.print " create_table #{remove_prefix_and_suffix(table).inspect}"
32
+
33
+ case pk
34
+ when String
35
+ tbl.print ", primary_key: #{pk.inspect}" unless pk == "id"
36
+ pkcol = columns.detect { |c| c.name == pk }
37
+ if pkcol
38
+ pkcolspec = column_spec_for_primary_key(pkcol)
39
+ if pkcolspec.present?
40
+ if pkcolspec != pkcolspec.slice(:id, :default)
41
+ pkcolspec = { id: { type: pkcolspec.delete(:id), **pkcolspec }.compact }
42
+ end
43
+ tbl.print ", #{format_colspec(pkcolspec)}"
44
+ end
45
+ end
46
+ when Array
47
+ tbl.print ", primary_key: #{pk.inspect}"
48
+ else
49
+ tbl.print ", id: false"
50
+ end
51
+
52
+ table_options = @connection.table_options(table)
53
+ if table_options.present?
54
+ tbl.print ", #{format_options(table_options)}"
55
+ end
56
+
57
+ tbl.puts ", force: :cascade do |t|"
58
+
59
+ # then dump all non-primary key columns -- in NATURAL DB order, not sorted
60
+ columns.each do |column|
61
+ raise StandardError, "Unknown type '#{column.sql_type}' for column '#{column.name}'" unless @connection.valid_type?(column.type)
62
+ next if column.name == pk
63
+
64
+ type, colspec = column_spec(column)
65
+ if type.is_a?(Symbol)
66
+ tbl.print " t.#{type} #{column.name.inspect}"
67
+ else
68
+ tbl.print " t.column #{column.name.inspect}, #{type.inspect}"
69
+ end
70
+ tbl.print ", #{format_colspec(colspec)}" if colspec.present?
71
+ tbl.puts
72
+ end
73
+
74
+ indexes_in_create(table, tbl)
75
+ remaining = check_constraints_in_create(table, tbl) if @connection.supports_check_constraints?
76
+ exclusion_constraints_in_create(table, tbl) if @connection.supports_exclusion_constraints?
77
+ unique_constraints_in_create(table, tbl) if @connection.supports_unique_constraints?
78
+
79
+ tbl.puts " end"
80
+
81
+ if remaining
82
+ tbl.puts
83
+ tbl.print remaining.string
84
+ end
85
+
86
+ stream.print tbl.string
87
+ rescue => e
88
+ stream.puts "# Could not dump table #{table.inspect} because of following #{e.class}"
89
+ stream.puts "# #{e.message}"
90
+ stream.puts
91
+ ensure
92
+ self.table_name = nil
93
+ end
94
+ end
95
+ end
96
+ end
97
+
98
+ ActiveRecord::SchemaDumper.prepend(UnsortDbSchemaColumns::SchemaDumperPatch)
99
+
100
+ unless ActiveRecord::VERSION::MAJOR == 8
101
+ warn "[unsort_db_schema_columns] Tested against ActiveRecord 8.x; running on " \
102
+ "#{ActiveRecord::VERSION::STRING}. Verify schema dumps look right; " \
103
+ "the upstream SchemaDumper#table method may have drifted."
104
+ end
@@ -0,0 +1,3 @@
1
+ module UnsortDbSchemaColumns
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,3 @@
1
+ require "unsort_db_schema_columns/version"
2
+ require "unsort_db_schema_columns/railtie" if defined?(Rails::Railtie)
3
+ require "unsort_db_schema_columns/schema_dumper_patch" unless defined?(Rails::Railtie)
metadata ADDED
@@ -0,0 +1,96 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: unsort_db_schema_columns
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Jake Moffatt
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 2026-05-14 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: activerecord
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '8.0'
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '9.0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ version: '8.0'
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '9.0'
32
+ - !ruby/object:Gem::Dependency
33
+ name: railties
34
+ requirement: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - ">="
37
+ - !ruby/object:Gem::Version
38
+ version: '8.0'
39
+ - - "<"
40
+ - !ruby/object:Gem::Version
41
+ version: '9.0'
42
+ type: :runtime
43
+ prerelease: false
44
+ version_requirements: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '8.0'
49
+ - - "<"
50
+ - !ruby/object:Gem::Version
51
+ version: '9.0'
52
+ description: |
53
+ Rails 8 (PR #53281) sorts schema.rb columns alphabetically. That breaks
54
+ db:schema:load parity with production, Postgres column-alignment padding,
55
+ add_column :after/:before, and bulk-import pipelines that depend on
56
+ SELECT * column order. This gem prepends ActiveRecord::SchemaDumper to
57
+ dump columns in the order the database actually stores them.
58
+ email:
59
+ - jake.moffatt@gmail.com
60
+ executables: []
61
+ extensions: []
62
+ extra_rdoc_files: []
63
+ files:
64
+ - LICENSE.txt
65
+ - README.md
66
+ - lib/unsort_db_schema_columns.rb
67
+ - lib/unsort_db_schema_columns/railtie.rb
68
+ - lib/unsort_db_schema_columns/schema_dumper_patch.rb
69
+ - lib/unsort_db_schema_columns/version.rb
70
+ homepage: https://github.com/jakeonrails/unsort-db-schema-columns
71
+ licenses:
72
+ - MIT
73
+ metadata:
74
+ homepage_uri: https://github.com/jakeonrails/unsort-db-schema-columns
75
+ source_code_uri: https://github.com/jakeonrails/unsort-db-schema-columns
76
+ bug_tracker_uri: https://github.com/jakeonrails/unsort-db-schema-columns/issues
77
+ changelog_uri: https://github.com/jakeonrails/unsort-db-schema-columns/blob/main/README.md
78
+ rubygems_mfa_required: 'true'
79
+ rdoc_options: []
80
+ require_paths:
81
+ - lib
82
+ required_ruby_version: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: '3.1'
87
+ required_rubygems_version: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - ">="
90
+ - !ruby/object:Gem::Version
91
+ version: '0'
92
+ requirements: []
93
+ rubygems_version: 3.6.2
94
+ specification_version: 4
95
+ summary: Restore natural (database ordinal) column order in Rails schema.rb dumps.
96
+ test_files: []