activerecord-pg-format-db-structure 0.1.4 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b1360637cb5c24ce89e734dd5bd544c80cd29e019102e2ea7268e49ab32f6f71
4
- data.tar.gz: 4ff8913438ffb2991e1c23d734cd8b5b8e0327b1286ca483c5d46e2b422ac7c4
3
+ metadata.gz: 2a0b4f6730ac7a280afddd46b476a8443e4cbbb3eac4004b92692223437f432c
4
+ data.tar.gz: b23c3a6ed71d6c46efa516d21f4c546dbdd3d2638f7c60046af0ff015d576f4d
5
5
  SHA512:
6
- metadata.gz: 5b95bb982ba987fed4a4ae273a337e3d00ee738012bf6c913a81c4d87b2c0e1a3e5ba5caa55bb829df6c242143ffff59e8cecfaab1ab4c517da2232d081023e1
7
- data.tar.gz: 9e6b856118e64bf1de2370206057082f9e34cfea208c201c71f0cc6cdd52ad6c9bdd1c06b0c0ea5e4183b6a9711a126724a9c6398471d1e5fd32ec3c689a4bd7
6
+ metadata.gz: f74568c5dcdecd2b0fe20d0823167e97a59da8593d534f877370b8e91b333a1a1c397c37913d5c81e91d8ccdfd254fc594f930ea1a0dab1ada589815ed8c38b5
7
+ data.tar.gz: 0eec46264e79d1ff729e12bef71bf6fd90291dbaff13ed88ae259f4e26b15cf7584ddb91a581d05d00df6c5b1ce4add028b4dc5f1318b32f89ef34dbbb8b0ddb
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.2.0] - 2025-02-15
4
+
5
+ - Remove preprocessors (no longer relevant now that we don't reuse the source string in the output)
6
+
7
+ ## [0.1.5] - 2025-02-08
8
+
9
+ - Sort table columns
10
+
3
11
  ## [0.1.4] - 2025-02-08
4
12
 
5
13
  - Sort schema migrations inserts
data/README.md CHANGED
@@ -1,6 +1,11 @@
1
1
  # activerecord-pg-format-db-structure
2
+ [![Gem Version](https://img.shields.io/gem/v/activerecord-pg-format-db-structure)](https://rubygems.org/gems/activerecord-pg-format-db-structure)
3
+ ![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/ReifyAB/activerecord-pg-format-db-structure/main.yml)
2
4
 
3
- Automatically cleans up your `structure.sql` file after each rails migration.
5
+
6
+ Automatically cleans up your PostgreSQL `structure.sql` file after each rails migration.
7
+
8
+ Say good-bye to small those small diffs you get between coworkers!
4
9
 
5
10
  By default, it will:
6
11
 
@@ -9,9 +14,13 @@ By default, it will:
9
14
  * Inline table constraints
10
15
  * Move index creation below their corresponding tables
11
16
  * Group `ALTER TABLE` statements into a single statement per table
12
- * Removes unnecessary whitespace
17
+ * Sorts table column declarations (primary key / foreign keys / data / timestamp / constraints)
18
+ * Sorts `schema_migrations` inserts
19
+ * Format and indent the entire file consistently
20
+
21
+ It can also optionally inline foreign key declarations (see below).
13
22
 
14
- The task will transform this raw `structure.sql`:
23
+ As an example, the task will transform this raw `structure.sql`:
15
24
 
16
25
  <details>
17
26
 
@@ -179,7 +188,7 @@ INSERT INTO "schema_migrations" (version) VALUES
179
188
  ```
180
189
  </details>
181
190
 
182
- into this much more compact and normalized version:
191
+ into this normalize (and much more compatch & readable) version:
183
192
 
184
193
  ```sql
185
194
 
@@ -225,8 +234,9 @@ INSERT INTO schema_migrations (version) VALUES
225
234
  ;
226
235
  ```
227
236
 
228
- which is a lot more compact, easier to read, and reduces the risk of
229
- getting random diffs between machines after each migration.
237
+ The goal is to make your `structure.sql` file easier to read and to
238
+ reduce the risk of getting random diffs between machines after each
239
+ migration.
230
240
 
231
241
  Those transformations are made by manipulating the SQL AST directly
232
242
  using [pg_query](https://github.com/pganalyze/pg_query), and each
@@ -240,7 +250,7 @@ You can also add your own transforms (see below).
240
250
  Add the following to your Gemfile:
241
251
 
242
252
  ```ruby
243
- gem 'activerecord-clean-db-structure'
253
+ gem 'activerecord-pg-format-db-structure'
244
254
  ```
245
255
 
246
256
  ## Usage
@@ -253,10 +263,6 @@ If you want to configure which transforms to use, you can configure the library
253
263
 
254
264
  ```ruby
255
265
  Rails.application.configure do
256
- config.activerecord_pg_format_db_structure.preprocessors = [
257
- ActiveRecordPgFormatDbStructure::Preprocessors::RemoveWhitespaces
258
- ]
259
-
260
266
  config.activerecord_pg_format_db_structure.transforms = [
261
267
  ActiveRecordPgFormatDbStructure::Transforms::RemoveCommentsOnExtensions,
262
268
  ActiveRecordPgFormatDbStructure::Transforms::SortSchemaMigrations,
@@ -265,7 +271,8 @@ Rails.application.configure do
265
271
  ActiveRecordPgFormatDbStructure::Transforms::InlineSerials,
266
272
  ActiveRecordPgFormatDbStructure::Transforms::InlineConstraints,
267
273
  ActiveRecordPgFormatDbStructure::Transforms::MoveIndicesAfterCreateTable,
268
- ActiveRecordPgFormatDbStructure::Transforms::GroupAlterTableStatements
274
+ ActiveRecordPgFormatDbStructure::Transforms::GroupAlterTableStatements,
275
+ ActiveRecordPgFormatDbStructure::Transforms::SortTableColumns,
269
276
  ]
270
277
 
271
278
  config.activerecord_pg_format_db_structure.deparser = ActiveRecordPgFormatDbStructure::Deparser
@@ -282,12 +289,6 @@ formatted = ActiveRecordPgFormatDbStructure::Formatter.new.format(structure)
282
289
  File.write("db/structure.sql", formatted)
283
290
  ```
284
291
 
285
- ## Preprocessors
286
-
287
- ### RemoveWhitespaces
288
-
289
- Remove unnecessary comment, whitespase and empty lines.
290
-
291
292
  ## Transformers
292
293
 
293
294
  ### RemoveCommentsOnExtensions
@@ -339,6 +340,49 @@ table.
339
340
 
340
341
  Should be run after other operations that inline alter statements.
341
342
 
343
+ ### SortTableColumns
344
+
345
+ Sort table columns, by order of priority and alphabetically:
346
+
347
+ 1. primary key
348
+ 2. foreign keys
349
+ 3. generic columns
350
+ 4. timestamps
351
+ 5. constraints
352
+
353
+ Note that you can define your own ordering by replacing the default `priority_mapping`:
354
+
355
+ ```ruby
356
+ ActiveRecordPgFormatDbStructure::Transforms::SortTableColumns.priority_mapping = lambda do |sortable_entry|
357
+ case sortable_entry
358
+ in is_column: true, is_primary_key: true, name:
359
+ [0, name]
360
+ in is_column: true, is_foreign_key: true, name:
361
+ [1, name]
362
+ in is_column: true, is_timestamp: false, name:
363
+ [2, name]
364
+ in is_column: true, is_timestamp: true, name:
365
+ [3, name]
366
+ in is_constraint: true, name:
367
+ [5, name]
368
+ end
369
+ end
370
+ ```
371
+
372
+ where `sortable_entry` is an instance of:
373
+
374
+ ```ruby
375
+ SORTABLE_ENTRY = Data.define(
376
+ :name,
377
+ :is_column,
378
+ :is_constraint,
379
+ :is_primary_key,
380
+ :is_foreign_key,
381
+ :is_timestamp,
382
+ :raw_entry
383
+ )
384
+ ```
385
+
342
386
  ## Deparser
343
387
 
344
388
  Returns an SQL string from raw PgQuery statements.
@@ -6,23 +6,17 @@ require_relative "../activerecord-pg-format-db-structure"
6
6
  module ActiveRecordPgFormatDbStructure
7
7
  # Formats & normalizes in place the given SQL string
8
8
  class Formatter
9
- attr_reader :preprocessors, :transforms, :deparser
9
+ attr_reader :transforms, :deparser
10
10
 
11
11
  def initialize(
12
- preprocessors: DEFAULT_PREPROCESSORS,
13
12
  transforms: DEFAULT_TRANSFORMS,
14
13
  deparser: DEFAULT_DEPARSER
15
14
  )
16
- @preprocessors = preprocessors
17
15
  @transforms = transforms
18
16
  @deparser = deparser
19
17
  end
20
18
 
21
19
  def format(source)
22
- preprocessors.each do |preprocessor|
23
- preprocessor.new(source).preprocess!
24
- end
25
-
26
20
  raw_statements = PgQuery.parse(source).tree.stmts
27
21
 
28
22
  transforms.each do |transform|
@@ -4,7 +4,6 @@ module ActiveRecordPgFormatDbStructure
4
4
  # Setup for Rails
5
5
  class Railtie < Rails::Railtie
6
6
  config.activerecord_pg_format_db_structure = ActiveSupport::OrderedOptions.new
7
- config.activerecord_pg_format_db_structure.preprocessors = DEFAULT_PREPROCESSORS.dup
8
7
  config.activerecord_pg_format_db_structure.transforms = DEFAULT_TRANSFORMS.dup
9
8
  config.activerecord_pg_format_db_structure.deparser = DEFAULT_DEPARSER
10
9
 
@@ -0,0 +1,220 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module ActiveRecordPgFormatDbStructure
6
+ module Transforms
7
+ # Sort table columns
8
+ class SortTableColumns < Base
9
+ SORTABLE_ENTRY = Data.define(
10
+ :name,
11
+ :is_column,
12
+ :is_constraint,
13
+ :is_primary_key,
14
+ :is_foreign_key,
15
+ :is_timestamp,
16
+ :raw_entry
17
+ )
18
+
19
+ class << self
20
+ attr_accessor :priority_mapping
21
+ end
22
+
23
+ self.priority_mapping = lambda do |sortable_entry|
24
+ case sortable_entry
25
+ in is_column: true, is_primary_key: true, name:
26
+ [0, name]
27
+ in is_column: true, is_foreign_key: true, name:
28
+ [1, name]
29
+ in is_column: true, is_timestamp: false, name:
30
+ [2, name]
31
+ in is_column: true, is_timestamp: true, name:
32
+ [3, name]
33
+ in is_constraint: true, name:
34
+ [5, name]
35
+ # :nocov:
36
+ # non-reachable else
37
+ # :nocov:
38
+ end
39
+ end
40
+
41
+ def transform!
42
+ foreign_keys = extract_foreign_keys
43
+ primary_keys = extract_primary_keys
44
+ raw_statements.each do |raw_statement|
45
+ next unless raw_statement.stmt.to_h in create_stmt: { relation: { schemaname:, relname: } }
46
+
47
+ raw_statement.stmt.create_stmt.table_elts.sort_by! do |table_elt|
48
+ elt_priority(
49
+ table_elt:,
50
+ primary_keys: primary_keys.fetch({ schemaname:, relname: }, Set.new),
51
+ foreign_keys: foreign_keys.fetch({ schemaname:, relname: }, Set.new)
52
+ )
53
+ end
54
+ end
55
+ end
56
+
57
+ private
58
+
59
+ def extract_primary_keys
60
+ primary_keys = {}
61
+ raw_statements.each do |raw_statement|
62
+ case raw_statement.stmt.to_h
63
+ in alter_table_stmt: {
64
+ objtype: :OBJECT_TABLE,
65
+ relation: {
66
+ schemaname:,
67
+ relname:
68
+ }
69
+ }
70
+ primary_keys[{ schemaname:, relname: }] ||= Set.new
71
+ raw_statement.stmt.alter_table_stmt.cmds.each do |cmd|
72
+ extract_primary_keys_from_alter_table_cmd(cmd:).each do |key|
73
+ primary_keys[{ schemaname:, relname: }] << key
74
+ end
75
+ end
76
+ in create_stmt: { relation: { schemaname:, relname: } }
77
+ primary_keys[{ schemaname:, relname: }] ||= Set.new
78
+ raw_statement.stmt.create_stmt.table_elts.each do |table_elt|
79
+ extract_primary_keys_from_table_elt(table_elt:).each do |key|
80
+ primary_keys[{ schemaname:, relname: }] << key
81
+ end
82
+ end
83
+ else
84
+ end
85
+ end
86
+ primary_keys
87
+ end
88
+
89
+ def extract_primary_keys_from_alter_table_cmd(cmd:)
90
+ if cmd.to_h in {
91
+ alter_table_cmd: {
92
+ subtype: :AT_AddConstraint,
93
+ def: {
94
+ constraint: {
95
+ contype: :CONSTR_PRIMARY
96
+ }
97
+ }
98
+ }
99
+ }
100
+ cmd.alter_table_cmd.def.constraint.keys.map do |key|
101
+ key.string.sval
102
+ end
103
+ else
104
+ []
105
+ end
106
+ end
107
+
108
+ def extract_primary_keys_from_table_elt(table_elt:)
109
+ case table_elt.to_h
110
+ in column_def: { constraints: [*, {constraint: {contype: :CONSTR_PRIMARY}}, *] }
111
+ [table_elt.column_def.colname]
112
+ in constraint: { contype: :CONSTR_PRIMARY }
113
+ table_elt.constraint.keys.map do |key|
114
+ key.string.sval
115
+ end
116
+ else
117
+ []
118
+ end
119
+ end
120
+
121
+ def extract_foreign_keys
122
+ foreign_keys = {}
123
+ raw_statements.each do |raw_statement|
124
+ case raw_statement.stmt.to_h
125
+ in alter_table_stmt: {
126
+ objtype: :OBJECT_TABLE,
127
+ relation: {
128
+ schemaname:,
129
+ relname:
130
+ }
131
+ }
132
+ foreign_keys[{ schemaname:, relname: }] ||= Set.new
133
+ raw_statement.stmt.alter_table_stmt.cmds.each do |cmd|
134
+ extract_foreign_keys_from_alter_table_cmd(cmd:).each do |key|
135
+ foreign_keys[{ schemaname:, relname: }] << key
136
+ end
137
+ end
138
+ in create_stmt: { relation: { schemaname:, relname: } }
139
+ foreign_keys[{ schemaname:, relname: }] ||= Set.new
140
+ raw_statement.stmt.create_stmt.table_elts.each do |table_elt|
141
+ extract_foreign_keys_from_table_elt(table_elt:).each do |key|
142
+ foreign_keys[{ schemaname:, relname: }] << key
143
+ end
144
+ end
145
+ else
146
+ end
147
+ end
148
+ foreign_keys
149
+ end
150
+
151
+ def extract_foreign_keys_from_alter_table_cmd(cmd:)
152
+ if cmd.to_h in {
153
+ alter_table_cmd: {
154
+ subtype: :AT_AddConstraint,
155
+ def: {
156
+ constraint: {
157
+ contype: :CONSTR_FOREIGN
158
+ }
159
+ }
160
+ }
161
+ }
162
+ cmd.alter_table_cmd.def.constraint.fk_attrs.map do |fk_attr|
163
+ fk_attr.string.sval
164
+ end
165
+ else
166
+ []
167
+ end
168
+ end
169
+
170
+ def extract_foreign_keys_from_table_elt(table_elt:)
171
+ case table_elt.to_h
172
+ in column_def: { constraints: [*, {constraint: {contype: :CONSTR_FOREIGN}}, *] }
173
+ [table_elt.column_def.colname]
174
+ in constraint: { contype: :CONSTR_FOREIGN }
175
+ table_elt.constraint.fk_attrs.map do |fk_attr|
176
+ fk_attr.string.sval
177
+ end
178
+ else
179
+ []
180
+ end
181
+ end
182
+
183
+ def elt_priority(table_elt:, primary_keys:, foreign_keys:)
184
+ case table_elt.to_h
185
+ in column_def: _
186
+ self.class.priority_mapping[
187
+ SORTABLE_ENTRY.new(
188
+ name: table_elt.column_def.colname,
189
+ is_column: true,
190
+ is_constraint: false,
191
+ is_primary_key: primary_keys.include?(table_elt.column_def.colname),
192
+ is_timestamp: timestamp?(table_elt.column_def),
193
+ is_foreign_key: foreign_keys.include?(table_elt.column_def.colname),
194
+ raw_entry: table_elt
195
+ )
196
+ ]
197
+ in constraint: _
198
+ self.class.priority_mapping[
199
+ SORTABLE_ENTRY.new(
200
+ name: table_elt.constraint.conname,
201
+ is_column: false,
202
+ is_constraint: true,
203
+ is_primary_key: false,
204
+ is_timestamp: false,
205
+ is_foreign_key: false,
206
+ raw_entry: table_elt
207
+ )
208
+ ]
209
+ # :nocov:
210
+ # non-reachable else
211
+ # :nocov:
212
+ end
213
+ end
214
+
215
+ def timestamp?(table_elt)
216
+ table_elt.type_name.names.any? { |name| name.to_h in string: { sval: "timestamp" } }
217
+ end
218
+ end
219
+ end
220
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveRecordPgFormatDbStructure
4
- VERSION = "0.1.4"
4
+ VERSION = "0.2.0"
5
5
  end
@@ -3,7 +3,6 @@
3
3
  require_relative "activerecord-pg-format-db-structure/version"
4
4
 
5
5
  require_relative "activerecord-pg-format-db-structure/deparser"
6
- require_relative "activerecord-pg-format-db-structure/preprocessors/remove_whitespaces"
7
6
  require_relative "activerecord-pg-format-db-structure/transforms/remove_comments_on_extensions"
8
7
  require_relative "activerecord-pg-format-db-structure/transforms/inline_serials"
9
8
  require_relative "activerecord-pg-format-db-structure/transforms/inline_primary_keys"
@@ -12,12 +11,9 @@ require_relative "activerecord-pg-format-db-structure/transforms/move_indices_af
12
11
  require_relative "activerecord-pg-format-db-structure/transforms/inline_constraints"
13
12
  require_relative "activerecord-pg-format-db-structure/transforms/group_alter_table_statements"
14
13
  require_relative "activerecord-pg-format-db-structure/transforms/sort_schema_migrations"
14
+ require_relative "activerecord-pg-format-db-structure/transforms/sort_table_columns"
15
15
 
16
16
  module ActiveRecordPgFormatDbStructure
17
- DEFAULT_PREPROCESSORS = [
18
- Preprocessors::RemoveWhitespaces
19
- ].freeze
20
-
21
17
  DEFAULT_TRANSFORMS = [
22
18
  Transforms::RemoveCommentsOnExtensions,
23
19
  Transforms::SortSchemaMigrations,
@@ -26,7 +22,8 @@ module ActiveRecordPgFormatDbStructure
26
22
  Transforms::InlineSerials,
27
23
  Transforms::InlineConstraints,
28
24
  Transforms::MoveIndicesAfterCreateTable,
29
- Transforms::GroupAlterTableStatements
25
+ Transforms::GroupAlterTableStatements,
26
+ Transforms::SortTableColumns
30
27
  ].freeze
31
28
 
32
29
  DEFAULT_DEPARSER = Deparser
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activerecord-pg-format-db-structure
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jell
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-02-08 00:00:00.000000000 Z
10
+ date: 2025-02-15 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: pg_query
@@ -42,7 +42,6 @@ files:
42
42
  - lib/activerecord-pg-format-db-structure/deparser.rb
43
43
  - lib/activerecord-pg-format-db-structure/formatter.rb
44
44
  - lib/activerecord-pg-format-db-structure/indenter.rb
45
- - lib/activerecord-pg-format-db-structure/preprocessors/remove_whitespaces.rb
46
45
  - lib/activerecord-pg-format-db-structure/railtie.rb
47
46
  - lib/activerecord-pg-format-db-structure/tasks/clean_db_structure.rake
48
47
  - lib/activerecord-pg-format-db-structure/transforms/base.rb
@@ -54,6 +53,7 @@ files:
54
53
  - lib/activerecord-pg-format-db-structure/transforms/move_indices_after_create_table.rb
55
54
  - lib/activerecord-pg-format-db-structure/transforms/remove_comments_on_extensions.rb
56
55
  - lib/activerecord-pg-format-db-structure/transforms/sort_schema_migrations.rb
56
+ - lib/activerecord-pg-format-db-structure/transforms/sort_table_columns.rb
57
57
  - lib/activerecord-pg-format-db-structure/version.rb
58
58
  homepage: https://github.com/ReifyAB/activerecord-pg-format-db-structure
59
59
  licenses:
@@ -1,27 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ActiveRecordPgFormatDbStructure
4
- module Preprocessors
5
- # Remove whitespace and SQL comments from an SQL string
6
- class RemoveWhitespaces
7
- attr_reader :source
8
-
9
- def initialize(source)
10
- @source = source
11
- end
12
-
13
- def preprocess!
14
- # Remove trailing whitespace
15
- source.gsub!(/[ \t]+$/, "")
16
- source.gsub!(/\A\n/, "")
17
- source.gsub!(/\n\n\z/, "\n")
18
-
19
- # Remove useless comment lines
20
- source.gsub!(/^--\n/, "")
21
-
22
- # Remove useless, version-specific parts of comments
23
- source.gsub!(/^-- (.*); Schema: ([\w.]+|-); Owner: -.*/, '-- \1')
24
- end
25
- end
26
- end
27
- end