activerecord-pg-format-db-structure 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.
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pg_query"
4
+
5
+ module ActiveRecordPgFormatDbStructure
6
+ module Transforms
7
+ # Inline non-foreign key constraints into table declaration
8
+ class InlineConstraints
9
+ attr_reader :raw_statements
10
+
11
+ def initialize(raw_statements)
12
+ @raw_statements = raw_statements
13
+ @tables_with_constraint = {}
14
+ end
15
+
16
+ def transform!
17
+ extract_constraints_to_inline!
18
+ raw_statements.each do |raw_statement|
19
+ next unless raw_statement.stmt.to_h in create_stmt: { relation: { schemaname:, relname: }}
20
+
21
+ relation = { schemaname:, relname: }
22
+ next unless @tables_with_constraint.include?(relation)
23
+
24
+ @tables_with_constraint[relation].each do |constraint|
25
+ add_constraint!(raw_statement, constraint)
26
+ end
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def extract_constraints_to_inline!
33
+ raw_statements.delete_if do |raw_statement|
34
+ next unless match_alter_column_statement(raw_statement) in { table:, constraint: }
35
+
36
+ @tables_with_constraint[table] ||= []
37
+ @tables_with_constraint[table] << constraint
38
+
39
+ true
40
+ end
41
+ end
42
+
43
+ def match_alter_column_statement(raw_statement)
44
+ return unless raw_statement.stmt.to_h in {
45
+ alter_table_stmt: {
46
+ objtype: :OBJECT_TABLE,
47
+ relation: {
48
+ schemaname:,
49
+ relname:
50
+ },
51
+ cmds: [{
52
+ alter_table_cmd: {
53
+ subtype: :AT_AddConstraint,
54
+ def: { constraint: },
55
+ behavior: :DROP_RESTRICT
56
+ }
57
+ }]
58
+ }
59
+ }
60
+
61
+ # Skip foreign keys
62
+ return if constraint in contype: :CONSTR_FOREIGN
63
+
64
+ {
65
+ table: { schemaname:, relname: },
66
+ constraint:
67
+ }
68
+ end
69
+
70
+ def add_constraint!(raw_statement, constraint)
71
+ raw_statement.stmt.create_stmt.table_elts << PgQuery::Node.new(
72
+ constraint: PgQuery::Constraint.new(constraint)
73
+ )
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pg_query"
4
+
5
+ module ActiveRecordPgFormatDbStructure
6
+ module Transforms
7
+ # Inline foreign key constraints.
8
+ #
9
+ # Note: using this transform makes the structure file no longer
10
+ # loadable, since tables should be created before a foreign key
11
+ # can target it.
12
+ class InlineForeignKeys
13
+ attr_reader :raw_statements
14
+
15
+ def initialize(raw_statements)
16
+ @raw_statements = raw_statements
17
+ @columns_with_foreign_key = {}
18
+ end
19
+
20
+ def transform!
21
+ extract_foreign_keys_to_inline!
22
+ raw_statements.each do |raw_statement|
23
+ next unless raw_statement.stmt.to_h in create_stmt: { relation: { schemaname:, relname: }}
24
+
25
+ relation = { schemaname:, relname: }
26
+ next unless @columns_with_foreign_key.include?(relation)
27
+
28
+ @columns_with_foreign_key[relation].each do |column_name, constraint|
29
+ add_constraint!(raw_statement, column_name, constraint)
30
+ end
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def extract_foreign_keys_to_inline!
37
+ raw_statements.delete_if do |raw_statement|
38
+ next unless match_alter_column_statement(raw_statement) in { column:, constraint: }
39
+
40
+ table = column.except(:colname)
41
+ @columns_with_foreign_key[table] ||= {}
42
+ @columns_with_foreign_key[table][column[:colname]] = constraint
43
+
44
+ true
45
+ end
46
+ end
47
+
48
+ def match_alter_column_statement(raw_statement)
49
+ # Extracting statements of this shape:
50
+ #
51
+ # ALTER TABLE ONLY ts.tn ALTER COLUMN c SET DEFAULT nextval('ts.tn_c_seq'::regclass);
52
+ #
53
+ # Which corresponds to what we get when using a SERIAL column
54
+ if raw_statement.stmt.to_h in {
55
+ alter_table_stmt: {
56
+ objtype: :OBJECT_TABLE,
57
+ relation: {
58
+ schemaname:,
59
+ relname:
60
+ },
61
+ cmds: [{
62
+ alter_table_cmd: {
63
+ subtype: :AT_AddConstraint,
64
+ def: {
65
+ constraint: {
66
+ contype: :CONSTR_FOREIGN,
67
+ conname:,
68
+ initially_valid:,
69
+ pktable:,
70
+ fk_attrs: [{string: {sval: colname}}],
71
+ pk_attrs:,
72
+ fk_matchtype:,
73
+ fk_upd_action:,
74
+ fk_del_action:
75
+ }
76
+ },
77
+ behavior: :DROP_RESTRICT
78
+ }
79
+ }]
80
+ }
81
+ }
82
+ {
83
+ column: { schemaname:, relname:, colname: },
84
+ constraint: {
85
+ contype: :CONSTR_FOREIGN,
86
+ conname:,
87
+ initially_valid:,
88
+ pktable:,
89
+ pk_attrs:,
90
+ fk_matchtype:,
91
+ fk_upd_action:,
92
+ fk_del_action:
93
+ }
94
+ }
95
+ end
96
+ end
97
+
98
+ def add_constraint!(raw_statement, colname, constraint)
99
+ raw_statement.stmt.create_stmt.table_elts.each do |table_elt|
100
+ next unless table_elt.to_h in { column_def: { colname: ^colname } }
101
+
102
+ table_elt.column_def.constraints << PgQuery::Node.new(
103
+ constraint: PgQuery::Constraint.new(constraint)
104
+ )
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pg_query"
4
+
5
+ module ActiveRecordPgFormatDbStructure
6
+ module Transforms
7
+ # Inlines primary keys with the table declaration
8
+ class InlinePrimaryKeys
9
+ attr_reader :raw_statements
10
+
11
+ def initialize(raw_statements)
12
+ @raw_statements = raw_statements
13
+ @columns_with_primary_key = {}
14
+ end
15
+
16
+ def transform!
17
+ extract_primary_keys_to_inline!
18
+ raw_statements.each do |raw_statement|
19
+ next unless raw_statement.stmt.to_h in create_stmt: { relation: { schemaname:, relname: }}
20
+
21
+ relation = { schemaname:, relname: }
22
+ primary_key = @columns_with_primary_key[relation]
23
+ add_primary_key!(raw_statement, primary_key) if primary_key
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def extract_primary_keys_to_inline!
30
+ raw_statements.delete_if do |raw_statement|
31
+ next unless match_alter_column_statement(raw_statement) in { schemaname:, relname:, colname: }
32
+
33
+ column = { schemaname:, relname:, colname: }
34
+ table = column.except(:colname)
35
+ @columns_with_primary_key[table] = column[:colname]
36
+
37
+ true
38
+ end
39
+ end
40
+
41
+ def match_alter_column_statement(raw_statement)
42
+ return unless raw_statement.stmt.to_h in {
43
+ alter_table_stmt: {
44
+ objtype: :OBJECT_TABLE,
45
+ relation: {
46
+ schemaname:,
47
+ relname:
48
+ },
49
+ cmds: [{
50
+ alter_table_cmd: {
51
+ subtype: :AT_AddConstraint,
52
+ def: {
53
+ constraint: {
54
+ contype: :CONSTR_PRIMARY,
55
+ conname: _,
56
+ keys: [{string: {sval: colname}}]
57
+ }
58
+ },
59
+ behavior: :DROP_RESTRICT
60
+ }
61
+ }]
62
+ }
63
+ }
64
+
65
+ { schemaname:, relname:, colname: }
66
+ end
67
+
68
+ def add_primary_key!(raw_statement, colname)
69
+ raw_statement.stmt.create_stmt.table_elts.each do |table_elt|
70
+ next unless table_elt.to_h in { column_def: { colname: ^colname } }
71
+
72
+ table_elt.column_def.constraints.delete_if do |c|
73
+ c.to_h in { constraint: { contype: :CONSTR_NOTNULL } }
74
+ end
75
+
76
+ table_elt.column_def.constraints << PgQuery::Node.new(
77
+ constraint: PgQuery::Constraint.new(
78
+ contype: :CONSTR_PRIMARY
79
+ )
80
+ )
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pg_query"
4
+
5
+ module ActiveRecordPgFormatDbStructure
6
+ module Transforms
7
+ # Inline SERIAL declaration inside table declaration.
8
+ #
9
+ # Note: the logic looks for statements of this shape:
10
+ #
11
+ # ALTER TABLE ONLY ts.tn ALTER COLUMN c SET DEFAULT nextval('ts.tn_c_seq'::regclass);
12
+ #
13
+ # It also assumes that the associated sequence has default settings. A
14
+ # later version could try to be more strict / validate that the
15
+ # sequence indeed has default settings.
16
+ class InlineSerials
17
+ attr_reader :raw_statements
18
+
19
+ def initialize(raw_statements)
20
+ @raw_statements = raw_statements
21
+ @columns_to_replace_with_serial = {}
22
+ @sequences_to_remove = Set.new
23
+ end
24
+
25
+ def transform!
26
+ extract_serials_to_inline!
27
+ delete_redundant_statements!
28
+ raw_statements.each do |raw_statement|
29
+ next unless raw_statement.stmt.to_h in create_stmt: { relation: { schemaname:, relname: }}
30
+
31
+ relation = { schemaname:, relname: }
32
+ next unless @columns_to_replace_with_serial.include?(relation)
33
+
34
+ @columns_to_replace_with_serial[relation].each do |colname|
35
+ replace_id_with_serial!(raw_statement, colname)
36
+ end
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ def extract_serials_to_inline!
43
+ raw_statements.delete_if do |raw_statement|
44
+ next unless match_alter_column_statement(raw_statement) in { column:, sequence: }
45
+
46
+ table = column.except(:column_name)
47
+ @columns_to_replace_with_serial[table] ||= []
48
+ @columns_to_replace_with_serial[table] << column[:column_name]
49
+ @sequences_to_remove << sequence
50
+
51
+ true
52
+ end
53
+ end
54
+
55
+ def match_alter_column_statement(raw_statement)
56
+ # Extracting statements of this shape:
57
+ #
58
+ # ALTER TABLE ONLY ts.tn ALTER COLUMN c SET DEFAULT nextval('ts.tn_c_seq'::regclass);
59
+ #
60
+ # Which corresponds to what we get when using a SERIAL column
61
+ return unless raw_statement.stmt.to_h in {
62
+ alter_table_stmt: {
63
+ objtype: :OBJECT_TABLE,
64
+ relation: {
65
+ schemaname:,
66
+ relname:
67
+ },
68
+ cmds: [{
69
+ alter_table_cmd: {
70
+ subtype: :AT_ColumnDefault,
71
+ name: column_name,
72
+ def: {
73
+ func_call: {
74
+ funcname: [{string: {sval: "nextval"}}],
75
+ args: [{ type_cast: {arg: {a_const: {sval: {sval: sequence_qualified_name}}},
76
+ type_name: {names: [{string: {sval: "regclass"}}]}}}],
77
+ funcformat: :COERCE_EXPLICIT_CALL
78
+ }
79
+ },
80
+ behavior: :DROP_RESTRICT
81
+ }
82
+ }]
83
+ }
84
+ }
85
+ return unless sequence_qualified_name == "#{schemaname}.#{relname}_#{column_name}_seq"
86
+
87
+ {
88
+ column: { schemaname:, relname:, column_name: },
89
+ sequence: { schemaname:, relname: "#{relname}_#{column_name}_seq" }
90
+ }
91
+ end
92
+
93
+ COLUMN_TYPE_TO_SERIAL_TYPE = {
94
+ "int2" => "smallserial",
95
+ "int4" => "serial",
96
+ "int8" => "bigserial"
97
+ }.freeze
98
+
99
+ def replace_id_with_serial!(raw_statement, colname)
100
+ raw_statement.stmt.create_stmt.table_elts.each do |table_elt|
101
+ next unless table_elt.to_h in {
102
+ column_def: {
103
+ colname: ^colname,
104
+ type_name: { names: [{string: {sval: "pg_catalog"}},
105
+ {string: {sval: "int2" | "int4" | "int8" => integer_type}}] }}
106
+ }
107
+
108
+ table_elt.column_def.type_name = PgQuery::TypeName.new(
109
+ names: [
110
+ PgQuery::Node.new(string: PgQuery::String.new(
111
+ sval: COLUMN_TYPE_TO_SERIAL_TYPE.fetch(integer_type)
112
+ ))
113
+ ]
114
+ )
115
+ end
116
+ end
117
+
118
+ def delete_redundant_statements!
119
+ raw_statements.delete_if do |raw_statement|
120
+ case raw_statement.stmt.to_h
121
+ in create_seq_stmt: { sequence: { schemaname:, relname: }}
122
+ @sequences_to_remove.include?({ schemaname:, relname: })
123
+ in alter_seq_stmt: {sequence: { schemaname:, relname: }}
124
+ @sequences_to_remove.include?({ schemaname:, relname: })
125
+ else
126
+ false
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pg_query"
4
+
5
+ module ActiveRecordPgFormatDbStructure
6
+ module Transforms
7
+ # Move indice declaration just below the table they index
8
+ class MoveIndicesAfterCreateTable
9
+ attr_reader :raw_statements
10
+
11
+ def initialize(raw_statements)
12
+ @raw_statements = raw_statements
13
+ end
14
+
15
+ def transform!
16
+ extract_table_indices!.each do |table, indices|
17
+ insert_index = find_insert_index(**table)
18
+ sort_indices(indices).reverse.each do |index|
19
+ raw_statements.insert(insert_index + 1, index)
20
+ end
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def find_insert_index(schemaname:, relname:)
27
+ raw_statements.find_index do |s|
28
+ s.stmt.to_h in {
29
+ create_stmt: { relation: { schemaname: ^schemaname, relname: ^relname } }
30
+ } | {
31
+ create_table_as_stmt: { into: { rel: { schemaname: ^schemaname, relname: ^relname } }}
32
+ }
33
+ end
34
+ end
35
+
36
+ def sort_indices(indices)
37
+ indices.sort_by do |s|
38
+ [
39
+ s.stmt.index_stmt.unique ? 0 : 1, # unique indices first
40
+ s.stmt.index_stmt.idxname
41
+ ]
42
+ end
43
+ end
44
+
45
+ def extract_table_indices!
46
+ indices = raw_statements.select { |s| s.stmt.to_h in index_stmt: _ }
47
+ raw_statements.delete_if { |s| s.stmt.to_h in index_stmt: _ }
48
+ indices.group_by do |s|
49
+ {
50
+ schemaname: s.stmt.index_stmt.relation.schemaname,
51
+ relname: s.stmt.index_stmt.relation.relname
52
+ }
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pg_query"
4
+
5
+ module ActiveRecordPgFormatDbStructure
6
+ module Transforms
7
+ # Remove COMMENT statement applied to extensions
8
+ class RemoveCommentsOnExtensions
9
+ attr_reader :raw_statements
10
+
11
+ def initialize(raw_statements)
12
+ @raw_statements = raw_statements
13
+ end
14
+
15
+ def transform!
16
+ raw_statements.delete_if do |raw_statement|
17
+ raw_statement.stmt.to_h in {
18
+ comment_stmt: { objtype: :OBJECT_EXTENSION }
19
+ }
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordPgFormatDbStructure
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "activerecord-pg-format-db-structure/version"
4
+
5
+ require_relative "activerecord-pg-format-db-structure/deparser"
6
+ require_relative "activerecord-pg-format-db-structure/preprocessors/remove_whitespaces"
7
+ require_relative "activerecord-pg-format-db-structure/transforms/remove_comments_on_extensions"
8
+ require_relative "activerecord-pg-format-db-structure/transforms/inline_serials"
9
+ require_relative "activerecord-pg-format-db-structure/transforms/inline_primary_keys"
10
+ require_relative "activerecord-pg-format-db-structure/transforms/inline_foreign_keys"
11
+ require_relative "activerecord-pg-format-db-structure/transforms/move_indices_after_create_table"
12
+ require_relative "activerecord-pg-format-db-structure/transforms/inline_constraints"
13
+ require_relative "activerecord-pg-format-db-structure/transforms/group_alter_table_statements"
14
+
15
+ module ActiveRecordPgFormatDbStructure
16
+ DEFAULT_PREPROCESSORS = [
17
+ Preprocessors::RemoveWhitespaces
18
+ ].freeze
19
+
20
+ DEFAULT_TRANSFORMS = [
21
+ Transforms::RemoveCommentsOnExtensions,
22
+ Transforms::InlinePrimaryKeys,
23
+ # Transforms::InlineForeignKeys,
24
+ Transforms::InlineSerials,
25
+ Transforms::InlineConstraints,
26
+ Transforms::MoveIndicesAfterCreateTable,
27
+ Transforms::GroupAlterTableStatements
28
+ ].freeze
29
+
30
+ DEFAULT_DEPARSER = Deparser
31
+ end
32
+
33
+ # :nocov:
34
+ require_relative "activerecord-pg-format-db-structure/railtie" if defined?(Rails)
35
+ # :nocov:
metadata ADDED
@@ -0,0 +1,80 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: activerecord-pg-format-db-structure
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Jell
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 2025-01-26 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: pg_query
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '6.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '6.0'
26
+ description: automatically runs after each db:schema:dump and formats the output
27
+ email:
28
+ - rubygems@reify.se
29
+ executables: []
30
+ extensions: []
31
+ extra_rdoc_files: []
32
+ files:
33
+ - ".rspec"
34
+ - ".rubocop.yml"
35
+ - ".ruby-version"
36
+ - CHANGELOG.md
37
+ - CODE_OF_CONDUCT.md
38
+ - LICENSE.txt
39
+ - README.md
40
+ - Rakefile
41
+ - lib/activerecord-pg-format-db-structure.rb
42
+ - lib/activerecord-pg-format-db-structure/deparser.rb
43
+ - lib/activerecord-pg-format-db-structure/formatter.rb
44
+ - lib/activerecord-pg-format-db-structure/preprocessors/remove_whitespaces.rb
45
+ - lib/activerecord-pg-format-db-structure/railtie.rb
46
+ - lib/activerecord-pg-format-db-structure/tasks/clean_db_structure.rake
47
+ - lib/activerecord-pg-format-db-structure/transforms/group_alter_table_statements.rb
48
+ - lib/activerecord-pg-format-db-structure/transforms/inline_constraints.rb
49
+ - lib/activerecord-pg-format-db-structure/transforms/inline_foreign_keys.rb
50
+ - lib/activerecord-pg-format-db-structure/transforms/inline_primary_keys.rb
51
+ - lib/activerecord-pg-format-db-structure/transforms/inline_serials.rb
52
+ - lib/activerecord-pg-format-db-structure/transforms/move_indices_after_create_table.rb
53
+ - lib/activerecord-pg-format-db-structure/transforms/remove_comments_on_extensions.rb
54
+ - lib/activerecord-pg-format-db-structure/version.rb
55
+ homepage: https://github.com/ReifyAB/activerecord-pg-format-db-structure
56
+ licenses:
57
+ - MIT
58
+ metadata:
59
+ homepage_uri: https://github.com/ReifyAB/activerecord-pg-format-db-structure
60
+ source_code_uri: https://github.com/ReifyAB/activerecord-pg-format-db-structure
61
+ changelog_uri: https://github.com/ReifyAB/activerecord-pg-format-db-structure/blob/main/CHANGELOG.md
62
+ rubygems_mfa_required: 'true'
63
+ rdoc_options: []
64
+ require_paths:
65
+ - lib
66
+ required_ruby_version: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: 3.1.0
71
+ required_rubygems_version: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ requirements: []
77
+ rubygems_version: 3.6.2
78
+ specification_version: 4
79
+ summary: Automatic formatting of Rails db/structure.sql file using pg_query
80
+ test_files: []