activerecord-pg-format-db-structure 0.1.4 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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