pg_trunk 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.
Files changed (196) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ci.yml +87 -0
  3. data/.gitignore +9 -0
  4. data/.rspec +4 -0
  5. data/.rubocop.yml +92 -0
  6. data/.yardopts +4 -0
  7. data/CHANGELOG.md +31 -0
  8. data/CONTRIBUTING.md +17 -0
  9. data/Gemfile +22 -0
  10. data/LICENSE.txt +21 -0
  11. data/README.md +141 -0
  12. data/Rakefile +16 -0
  13. data/bin/console +8 -0
  14. data/bin/rake +19 -0
  15. data/bin/rspec +19 -0
  16. data/bin/setup +8 -0
  17. data/bin/yard +19 -0
  18. data/lib/pg_trunk/core/adapters/postgres.rb +80 -0
  19. data/lib/pg_trunk/core/dependencies_resolver.rb +101 -0
  20. data/lib/pg_trunk/core/generators.rb +140 -0
  21. data/lib/pg_trunk/core/operation/attributes.rb +78 -0
  22. data/lib/pg_trunk/core/operation/callbacks.rb +40 -0
  23. data/lib/pg_trunk/core/operation/generators.rb +51 -0
  24. data/lib/pg_trunk/core/operation/inversion.rb +70 -0
  25. data/lib/pg_trunk/core/operation/registration.rb +55 -0
  26. data/lib/pg_trunk/core/operation/ruby_builder.rb +112 -0
  27. data/lib/pg_trunk/core/operation/ruby_helpers.rb +99 -0
  28. data/lib/pg_trunk/core/operation/sql_helpers.rb +44 -0
  29. data/lib/pg_trunk/core/operation/validations.rb +21 -0
  30. data/lib/pg_trunk/core/operation.rb +78 -0
  31. data/lib/pg_trunk/core/qualified_name.rb +165 -0
  32. data/lib/pg_trunk/core/railtie/command_recorder.rb +30 -0
  33. data/lib/pg_trunk/core/railtie/custom_types.rb +37 -0
  34. data/lib/pg_trunk/core/railtie/migration.rb +50 -0
  35. data/lib/pg_trunk/core/railtie/migrator.rb +22 -0
  36. data/lib/pg_trunk/core/railtie/schema_dumper.rb +75 -0
  37. data/lib/pg_trunk/core/railtie/schema_migration.rb +22 -0
  38. data/lib/pg_trunk/core/railtie/statements.rb +21 -0
  39. data/lib/pg_trunk/core/railtie.rb +35 -0
  40. data/lib/pg_trunk/core/registry.rb +159 -0
  41. data/lib/pg_trunk/core/serializers/array_of_hashes_serializer.rb +28 -0
  42. data/lib/pg_trunk/core/serializers/array_of_strings_serializer.rb +29 -0
  43. data/lib/pg_trunk/core/serializers/array_of_symbols_serializer.rb +28 -0
  44. data/lib/pg_trunk/core/serializers/array_serializer.rb +22 -0
  45. data/lib/pg_trunk/core/serializers/lowercase_string_serializer.rb +21 -0
  46. data/lib/pg_trunk/core/serializers/multiline_text_serializer.rb +21 -0
  47. data/lib/pg_trunk/core/serializers/qualified_name_serializer.rb +27 -0
  48. data/lib/pg_trunk/core/serializers/symbol_serializer.rb +22 -0
  49. data/lib/pg_trunk/core/serializers.rb +16 -0
  50. data/lib/pg_trunk/core/validators/all_items_valid_validator.rb +15 -0
  51. data/lib/pg_trunk/core/validators/difference_validator.rb +19 -0
  52. data/lib/pg_trunk/core/validators.rb +10 -0
  53. data/lib/pg_trunk/core.rb +21 -0
  54. data/lib/pg_trunk/generators.rb +7 -0
  55. data/lib/pg_trunk/operations/check_constraints/add_check_constraint.rb +109 -0
  56. data/lib/pg_trunk/operations/check_constraints/base.rb +69 -0
  57. data/lib/pg_trunk/operations/check_constraints/drop_check_constraint.rb +60 -0
  58. data/lib/pg_trunk/operations/check_constraints/rename_check_constraint.rb +54 -0
  59. data/lib/pg_trunk/operations/check_constraints/validate_check_constraint.rb +39 -0
  60. data/lib/pg_trunk/operations/check_constraints.rb +14 -0
  61. data/lib/pg_trunk/operations/composite_types/base.rb +61 -0
  62. data/lib/pg_trunk/operations/composite_types/change_composite_type.rb +136 -0
  63. data/lib/pg_trunk/operations/composite_types/column.rb +118 -0
  64. data/lib/pg_trunk/operations/composite_types/create_composite_type.rb +99 -0
  65. data/lib/pg_trunk/operations/composite_types/drop_composite_type.rb +67 -0
  66. data/lib/pg_trunk/operations/composite_types/rename_composite_type.rb +44 -0
  67. data/lib/pg_trunk/operations/composite_types.rb +15 -0
  68. data/lib/pg_trunk/operations/domains/base.rb +46 -0
  69. data/lib/pg_trunk/operations/domains/change_domain.rb +140 -0
  70. data/lib/pg_trunk/operations/domains/constraint.rb +93 -0
  71. data/lib/pg_trunk/operations/domains/create_domain.rb +124 -0
  72. data/lib/pg_trunk/operations/domains/drop_domain.rb +65 -0
  73. data/lib/pg_trunk/operations/domains/rename_domain.rb +44 -0
  74. data/lib/pg_trunk/operations/domains.rb +15 -0
  75. data/lib/pg_trunk/operations/enums/base.rb +47 -0
  76. data/lib/pg_trunk/operations/enums/change.rb +55 -0
  77. data/lib/pg_trunk/operations/enums/change_enum.rb +119 -0
  78. data/lib/pg_trunk/operations/enums/create_enum.rb +83 -0
  79. data/lib/pg_trunk/operations/enums/drop_enum.rb +63 -0
  80. data/lib/pg_trunk/operations/enums/rename_enum.rb +44 -0
  81. data/lib/pg_trunk/operations/enums.rb +15 -0
  82. data/lib/pg_trunk/operations/foreign_keys/add_foreign_key.rb +174 -0
  83. data/lib/pg_trunk/operations/foreign_keys/base.rb +155 -0
  84. data/lib/pg_trunk/operations/foreign_keys/drop_foreign_key.rb +76 -0
  85. data/lib/pg_trunk/operations/foreign_keys/rename_foreign_key.rb +63 -0
  86. data/lib/pg_trunk/operations/foreign_keys.rb +16 -0
  87. data/lib/pg_trunk/operations/functions/base.rb +54 -0
  88. data/lib/pg_trunk/operations/functions/change_function.rb +108 -0
  89. data/lib/pg_trunk/operations/functions/create_function.rb +198 -0
  90. data/lib/pg_trunk/operations/functions/drop_function.rb +88 -0
  91. data/lib/pg_trunk/operations/functions/rename_function.rb +57 -0
  92. data/lib/pg_trunk/operations/functions.rb +14 -0
  93. data/lib/pg_trunk/operations/indexes/add_index.rb +68 -0
  94. data/lib/pg_trunk/operations/indexes.rb +10 -0
  95. data/lib/pg_trunk/operations/materialized_views/base.rb +79 -0
  96. data/lib/pg_trunk/operations/materialized_views/change_materialized_view.rb +139 -0
  97. data/lib/pg_trunk/operations/materialized_views/column.rb +94 -0
  98. data/lib/pg_trunk/operations/materialized_views/create_materialized_view.rb +170 -0
  99. data/lib/pg_trunk/operations/materialized_views/drop_materialized_view.rb +70 -0
  100. data/lib/pg_trunk/operations/materialized_views/refresh_materialized_view.rb +48 -0
  101. data/lib/pg_trunk/operations/materialized_views/rename_materialized_view.rb +61 -0
  102. data/lib/pg_trunk/operations/materialized_views.rb +17 -0
  103. data/lib/pg_trunk/operations/procedures/base.rb +42 -0
  104. data/lib/pg_trunk/operations/procedures/change_procedure.rb +107 -0
  105. data/lib/pg_trunk/operations/procedures/create_procedure.rb +146 -0
  106. data/lib/pg_trunk/operations/procedures/drop_procedure.rb +66 -0
  107. data/lib/pg_trunk/operations/procedures/rename_procedure.rb +57 -0
  108. data/lib/pg_trunk/operations/procedures.rb +14 -0
  109. data/lib/pg_trunk/operations/statistics/base.rb +94 -0
  110. data/lib/pg_trunk/operations/statistics/create_statistics.rb +181 -0
  111. data/lib/pg_trunk/operations/statistics/drop_statistics.rb +75 -0
  112. data/lib/pg_trunk/operations/statistics/rename_statistics.rb +48 -0
  113. data/lib/pg_trunk/operations/statistics.rb +13 -0
  114. data/lib/pg_trunk/operations/tables/create_table.rb +75 -0
  115. data/lib/pg_trunk/operations/tables.rb +10 -0
  116. data/lib/pg_trunk/operations/triggers/base.rb +119 -0
  117. data/lib/pg_trunk/operations/triggers/change_trigger.rb +82 -0
  118. data/lib/pg_trunk/operations/triggers/create_trigger.rb +208 -0
  119. data/lib/pg_trunk/operations/triggers/drop_trigger.rb +66 -0
  120. data/lib/pg_trunk/operations/triggers/rename_trigger.rb +71 -0
  121. data/lib/pg_trunk/operations/triggers.rb +14 -0
  122. data/lib/pg_trunk/operations/views/base.rb +38 -0
  123. data/lib/pg_trunk/operations/views/change_view.rb +90 -0
  124. data/lib/pg_trunk/operations/views/create_view.rb +115 -0
  125. data/lib/pg_trunk/operations/views/drop_view.rb +69 -0
  126. data/lib/pg_trunk/operations/views/rename_view.rb +58 -0
  127. data/lib/pg_trunk/operations/views.rb +14 -0
  128. data/lib/pg_trunk/operations.rb +23 -0
  129. data/lib/pg_trunk/version.rb +6 -0
  130. data/lib/pg_trunk.rb +27 -0
  131. data/pg_trunk.gemspec +34 -0
  132. data/spec/dummy/.gitignore +16 -0
  133. data/spec/dummy/Rakefile +15 -0
  134. data/spec/dummy/bin/bundle +6 -0
  135. data/spec/dummy/bin/rails +6 -0
  136. data/spec/dummy/bin/rake +6 -0
  137. data/spec/dummy/config/application.rb +18 -0
  138. data/spec/dummy/config/boot.rb +7 -0
  139. data/spec/dummy/config/database.yml +14 -0
  140. data/spec/dummy/config/environment.rb +7 -0
  141. data/spec/dummy/config.ru +6 -0
  142. data/spec/dummy/db/materialized_views/admin_users_v01.sql +1 -0
  143. data/spec/dummy/db/migrate/.keep +0 -0
  144. data/spec/dummy/db/schema.rb +18 -0
  145. data/spec/dummy/db/views/admin_users_v01.sql +1 -0
  146. data/spec/dummy/db/views/admin_users_v02.sql +1 -0
  147. data/spec/operations/check_constraints/add_check_constraint_spec.rb +85 -0
  148. data/spec/operations/check_constraints/drop_check_constraint_spec.rb +111 -0
  149. data/spec/operations/check_constraints/rename_check_constraint_spec.rb +90 -0
  150. data/spec/operations/composite_types/change_composite_type_spec.rb +257 -0
  151. data/spec/operations/composite_types/create_composite_type_spec.rb +55 -0
  152. data/spec/operations/composite_types/drop_composite_type_spec.rb +109 -0
  153. data/spec/operations/composite_types/rename_composite_type_spec.rb +74 -0
  154. data/spec/operations/dependency_resolver_spec.rb +177 -0
  155. data/spec/operations/domains/change_domain_spec.rb +287 -0
  156. data/spec/operations/domains/create_domain_spec.rb +69 -0
  157. data/spec/operations/domains/drop_domain_spec.rb +119 -0
  158. data/spec/operations/domains/rename_domain_spec.rb +70 -0
  159. data/spec/operations/enums/change_enum_spec.rb +157 -0
  160. data/spec/operations/enums/create_enum_spec.rb +40 -0
  161. data/spec/operations/enums/drop_enum_spec.rb +120 -0
  162. data/spec/operations/enums/rename_enum_spec.rb +72 -0
  163. data/spec/operations/foreign_keys/add_foreign_key_spec.rb +208 -0
  164. data/spec/operations/foreign_keys/drop_foreign_key_spec.rb +167 -0
  165. data/spec/operations/foreign_keys/rename_foreign_key_spec.rb +101 -0
  166. data/spec/operations/functions/change_function_spec.rb +166 -0
  167. data/spec/operations/functions/create_function_spec.rb +192 -0
  168. data/spec/operations/functions/drop_function_spec.rb +182 -0
  169. data/spec/operations/functions/rename_function_spec.rb +101 -0
  170. data/spec/operations/indexes/add_index_spec.rb +94 -0
  171. data/spec/operations/materialized_views/change_materialized_view_spec.rb +190 -0
  172. data/spec/operations/materialized_views/create_materialized_view_spec.rb +144 -0
  173. data/spec/operations/materialized_views/drop_materialized_view_spec.rb +145 -0
  174. data/spec/operations/materialized_views/refresh_materialized_view_spec.rb +79 -0
  175. data/spec/operations/materialized_views/rename_materialized_view_spec.rb +88 -0
  176. data/spec/operations/procedures/change_procedure_spec.rb +175 -0
  177. data/spec/operations/procedures/create_procedure_spec.rb +151 -0
  178. data/spec/operations/procedures/drop_procedure_spec.rb +159 -0
  179. data/spec/operations/procedures/rename_procedure_spec.rb +107 -0
  180. data/spec/operations/statistics/create_statistics_spec.rb +230 -0
  181. data/spec/operations/statistics/drop_statistics_spec.rb +106 -0
  182. data/spec/operations/statistics/rename_statistics_spec.rb +129 -0
  183. data/spec/operations/tables/create_table_spec.rb +53 -0
  184. data/spec/operations/tables/rename_table_spec.rb +37 -0
  185. data/spec/operations/triggers/change_trigger_spec.rb +195 -0
  186. data/spec/operations/triggers/create_trigger_spec.rb +104 -0
  187. data/spec/operations/triggers/drop_trigger_spec.rb +124 -0
  188. data/spec/operations/triggers/rename_trigger_spec.rb +160 -0
  189. data/spec/operations/views/change_view_spec.rb +144 -0
  190. data/spec/operations/views/create_view_spec.rb +134 -0
  191. data/spec/operations/views/drop_view_spec.rb +146 -0
  192. data/spec/operations/views/rename_view_spec.rb +85 -0
  193. data/spec/pg_trunk/dependencies_resolver_spec.rb +43 -0
  194. data/spec/spec_helper.rb +28 -0
  195. data/spec/support/migrations_helper.rb +376 -0
  196. metadata +348 -0
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ class PGTrunk::Operation
4
+ # @private
5
+ # Helpers to build ruby snippet from the operation definition
6
+ module RubyHelpers
7
+ extend ActiveSupport::Concern
8
+
9
+ class_methods do
10
+ # The name of the builder
11
+ # @return [Symbol]
12
+ def ruby_name
13
+ @ruby_name ||= name.split("::").last.underscore.to_sym
14
+ end
15
+
16
+ # The name of the inverted builder
17
+ # @return [Symbol]
18
+ def ruby_iname
19
+ "invert_#{ruby_name}".to_sym
20
+ end
21
+
22
+ # Get/set positional params of the ruby method
23
+ #
24
+ # @example Provide a method `add_check_constraint(table = nil, **opts)`
25
+ # class AddCheckConstraint < PGTrunk::Operation
26
+ # ruby_params :table
27
+ # # ...
28
+ # end
29
+ #
30
+ def ruby_params(*params)
31
+ @ruby_params = params.compact.map(&:to_sym) if params.any?
32
+ @ruby_params ||= []
33
+ end
34
+
35
+ # Gets or sets the block building a ruby snippet
36
+ #
37
+ # @yieldparam [PGTrunk::Operation::RubyBuilder]
38
+ #
39
+ # @example
40
+ # ruby_snippet do |s|
41
+ # s.ruby_param(comment: comment) if comment.present?
42
+ # values.each { |v| s.ruby_line(:value, v) }
43
+ # end
44
+ #
45
+ # # will build something like
46
+ #
47
+ # do_something "foo.bar", comment: "comment" do |s|
48
+ # s.value "baz"
49
+ # s.value "qux"
50
+ # end
51
+ def ruby_snippet(&block)
52
+ @ruby_snippet ||= block
53
+ end
54
+
55
+ # Build the operation from arguments sent to Ruby method
56
+ def from_ruby(*args, &block)
57
+ options = args.last.is_a?(Hash) ? args.pop.symbolize_keys : {}
58
+ params = ruby_params.zip(args).to_h
59
+ new(**params, **options, &block)
60
+ end
61
+
62
+ private
63
+
64
+ def inherited(klass)
65
+ # Use params from a parent class by default (can be overloaded).
66
+ klass.instance_variable_set(:@ruby_params, ruby_params)
67
+ klass.instance_variable_set(:@ruby_snippet, ruby_snippet)
68
+ super
69
+ end
70
+ end
71
+
72
+ # @private
73
+ # Ruby snippet to dump the creator
74
+ # @return [String]
75
+ def to_ruby
76
+ builder = RubyBuilder.new(self.class.ruby_name)
77
+ instance_exec(builder, &self.class.ruby_snippet)
78
+ builder.build.rstrip
79
+ end
80
+
81
+ # List of attributes assigned that are assigned
82
+ # via Ruby method parameters.
83
+ #
84
+ # We can use it to announce the operation to $stdout
85
+ # like `create_foreign_key("users", "roles")`.
86
+ def to_a
87
+ to_h.values_at(*self.class.ruby_params)
88
+ end
89
+
90
+ def to_opts
91
+ to_h.except(*self.class.ruby_params)
92
+ end
93
+
94
+ # @param [IO] stream
95
+ def dump(stream)
96
+ to_ruby&.rstrip&.lines&.each { |line| stream.print(line.indent(2)) }
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ class PGTrunk::Operation
4
+ # @private
5
+ # Add helpers for building SQL queries
6
+ module SQLHelpers
7
+ extend ActiveSupport::Concern
8
+
9
+ class_methods do
10
+ include Enumerable
11
+
12
+ # Get/set the block to extract operation definitions
13
+ # from the database.
14
+ # @yield [Proc] the block returning sql
15
+ # @yieldparam [#to_s] version The current version of the database
16
+ def from_sql(&block)
17
+ @from_sql = block if block
18
+ @from_sql ||= nil
19
+ end
20
+
21
+ # Iterate by sorted operation definitions
22
+ # extracted from the database
23
+ def each(&block)
24
+ return to_enum unless block_given?
25
+
26
+ fetch
27
+ .map { |item| new(**item.symbolize_keys) }
28
+ .sort
29
+ .each { |op| block.call(op) }
30
+ end
31
+
32
+ private
33
+
34
+ def fetch
35
+ query = from_sql&.call(PGTrunk.database.server_version)
36
+ query.blank? ? [] : PGTrunk.database.execute(query)
37
+ end
38
+ end
39
+
40
+ def quote(str)
41
+ PGTrunk.database.quote(str)
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ class PGTrunk::Operation
4
+ # @private
5
+ # Enable validation of the operation in the Rails way
6
+ module Validations
7
+ extend ActiveSupport::Concern
8
+
9
+ class_methods do
10
+ extend ActiveModel::Validations
11
+ end
12
+
13
+ def error_messages
14
+ errors.messages.flat_map do |k, v|
15
+ Array(v).map do |msg|
16
+ k == :base ? msg : [k, msg].join(" ")
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: false
2
+
3
+ require_relative "operation/callbacks"
4
+ require_relative "operation/attributes"
5
+ require_relative "operation/generators"
6
+ require_relative "operation/validations"
7
+ require_relative "operation/inversion"
8
+ require_relative "operation/ruby_builder"
9
+ require_relative "operation/ruby_helpers"
10
+ require_relative "operation/sql_helpers"
11
+ require_relative "operation/registration"
12
+
13
+ module PGTrunk
14
+ # @private
15
+ # Base class for operations.
16
+ # Inherit this class to define new operation.
17
+ class Operation
18
+ include Callbacks
19
+ include Attributes
20
+ include Generators
21
+ include Comparable
22
+ include Validations
23
+ include Inversion
24
+ include RubyHelpers
25
+ include SQLHelpers
26
+ include Registration
27
+
28
+ attribute :comment, :string, desc: \
29
+ "The comment to the object"
30
+ attribute :force, :pg_trunk_symbol, desc: \
31
+ "How to process dependent objects"
32
+ attribute :if_exists, :boolean, desc: \
33
+ "Don't fail if the object is absent"
34
+ attribute :if_not_exists, :boolean, desc: \
35
+ "Don't fail if the object is already present"
36
+ attribute :name, :pg_trunk_qualified_name, desc: \
37
+ "The qualified name of the object"
38
+ attribute :new_name, :pg_trunk_qualified_name, aliases: :to, desc: \
39
+ "The new name of the object to rename to"
40
+ attribute :oid, :integer, desc: \
41
+ "The oid of the database object"
42
+ attribute :version, :integer, aliases: :revert_to_version, desc: \
43
+ "The version of the SQL snippet"
44
+
45
+ validates :name, presence: true
46
+ validates :new_name, "PGTrunk/difference": { from: :name }, allow_nil: true
47
+ validates :force, inclusion: { in: %i[cascade restrict] }, allow_nil: true
48
+
49
+ # By default ruby methods take the object name as a positional argument.
50
+ ruby_params :name
51
+
52
+ protected
53
+
54
+ # Define the order of objects
55
+ # @param [PGTrunk::Definitions]
56
+ # @return [-1, 0, 1, nil]
57
+ def <=>(other)
58
+ name <=> other.name if other.is_a?(self.class)
59
+ end
60
+
61
+ private
62
+
63
+ # Helper to read a versioned snippet for a specific
64
+ # kind of objects
65
+ def read_snippet_from(kind)
66
+ return if kind.blank? || name.blank? || version.blank?
67
+
68
+ filename = format(
69
+ "db/%<kind>s/%<name>s_v%<version>02d.sql",
70
+ kind: kind.to_s.pluralize,
71
+ name: name.routine,
72
+ version: version,
73
+ )
74
+ filepath = Rails.root.join(filename)
75
+ File.read(filepath).sub(/;\s*$/, "")
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,165 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PGTrunk
4
+ # @private
5
+ # The qualified name of an object consists from schema (namespace) and name.
6
+ # The class contains several helper methods for ruby and sql snippets.
7
+ QualifiedName = Struct.new(:schema, :name) do
8
+ include Comparable
9
+
10
+ # Build qualified name structure from a string
11
+ #
12
+ # @example
13
+ # QualifiedName["bar"].to_h
14
+ # # => { schema: "public", name: "bar" }
15
+ #
16
+ # QualifiedName["foo.bar(a int, b int)"].to_h
17
+ # # => { schema: "foo", name: "bar(a int, b int)" }
18
+ #
19
+ def self.wrap(string)
20
+ schema = /^([^.(]+)[.]/.match(string).to_a[1]
21
+ name = string.sub(/^[^.(]*[.]/, "")
22
+ new(schema, name)
23
+ end
24
+
25
+ # Get the current schema
26
+ def current_schema
27
+ @current_schema ||= PGTrunk.database.current_schema
28
+ end
29
+
30
+ private def initialize(schema, name)
31
+ super(schema.to_s.presence || current_schema, name.to_s.presence)
32
+ end
33
+
34
+ # If the schema is current (which is usually 'public')
35
+ # This schema would be ignored in +lean+
36
+ def current_schema?
37
+ schema == current_schema
38
+ end
39
+
40
+ # If the qualified name is blank (not specified)
41
+ def blank?
42
+ routine.blank?
43
+ end
44
+
45
+ # If the name matches the regex
46
+ def match?(regex)
47
+ routine.match?(regex)
48
+ end
49
+
50
+ # If the name is present but not matches the regexp
51
+ # (used to check if it is not generated by some pattern)
52
+ def differs_from?(regex)
53
+ !routine.match?(regex) if present?
54
+ end
55
+
56
+ # Quoted representation of the name for SQL snippets
57
+ #
58
+ # QualifiedName.wrap("foo.bar(a int, OUT b int) record").to_sql
59
+ # # => '"foo"."bar" (a int)'
60
+ #
61
+ # qname = QualifiedName.wrap("public.foo")
62
+ # qname.to_sql
63
+ # # => '"foo"'
64
+ # qname.to_sql(true)
65
+ # # => '"foo"()'
66
+ def to_sql(with_args = nil)
67
+ @to_sql ||= begin
68
+ str = [
69
+ *(schema unless current_schema?),
70
+ routine,
71
+ ].map(&:inspect).join(".")
72
+ str = "#{str} (#{args})" if args.present? || with_args
73
+ str
74
+ end
75
+ end
76
+
77
+ # The qualified name with unquoted parts
78
+ # (to be used in ruby snippets)
79
+ #
80
+ # QualifiedName.wrap("bar.foo").full
81
+ # # => "bar.foo"
82
+ #
83
+ # QualifiedName.wrap("public.foo(int, int) int").full
84
+ # # => "foo(int, int) int"
85
+ def full
86
+ @full ||= [schema, name].join(".")
87
+ end
88
+
89
+ # Exclude current schema from a qualified name
90
+ #
91
+ # QualifiedName.wrap("public.foo").lean
92
+ # # => "foo"
93
+ #
94
+ # QualifiedName.wrap("bar.baz").lean
95
+ # # => "bar.baz"
96
+ def lean
97
+ @lean ||= [*(schema unless current_schema?), name].join(".")
98
+ end
99
+
100
+ # Name of the routine for function definition
101
+ # QualifiedName.wrap("foo.bar(a int, b int) int").routine
102
+ # # => "bar"
103
+ def routine
104
+ @routine ||= name[/^[^(]+/]&.strip
105
+ end
106
+
107
+ # Args from the function definition
108
+ # QualifiedName.wrap("foo.bar(a int, b int DEFAULT = 0) int").args
109
+ # # => "a int, b int DEFAULT = 0"
110
+ def args
111
+ @args ||= name&.match(/\(([^)]+)/).to_a.last&.gsub(/\s+/, " ")&.strip
112
+ end
113
+
114
+ # Arg types from the function definition
115
+ # QualifiedName.wrap("foo.bar(a int, b int DEFAULT = 0) int").args
116
+ # # => %w[int int]
117
+ def arg_types
118
+ @arg_types ||= args.to_s.split(",").map { |a| a.strip.split(/\s+/).last }
119
+ end
120
+
121
+ # Type of returned value of a routine
122
+ # QualifiedName.wrap("foo.concat(a int, b int) text").returns
123
+ # # => "text"
124
+ def returns
125
+ @returns ||= name&.match(/\)(.+)/).to_a.last&.strip
126
+ end
127
+
128
+ # Quote the name (to compare to text fields in the database)
129
+ # QualifiedName.wrap("foo.concat(a int, b int) text").quoted
130
+ # # => "'concat'"
131
+ def quoted
132
+ @quoted ||= PGTrunk.database.quote(routine)
133
+ end
134
+
135
+ # Transform the namespace (to compare to oid fields in the database)
136
+ # QualifiedName.wrap("foo.bar").namespace
137
+ # # => "'foo'::regnamespace"
138
+ def namespace
139
+ @namespace ||= "#{PGTrunk.database.quote(schema)}::regnamespace"
140
+ end
141
+
142
+ # Make qualified names comparable by their full representation
143
+ # @return [Integer, NilClass]
144
+ def <=>(other)
145
+ sort_order <=> other.sort_order if other.is_a?(self.class)
146
+ end
147
+
148
+ # Checks if partially qualified <function>
149
+ # have the same schema/name as another function
150
+ def maybe_eq?(other)
151
+ sort_order.first(2) == other.sort_order.first(2)
152
+ end
153
+
154
+ # Build the name with a changed part (either a name or a schema)
155
+ def merge(**args)
156
+ self.class.new(args.fetch(:schema, schema), args.fetch(:name, name))
157
+ end
158
+
159
+ protected
160
+
161
+ def sort_order
162
+ [schema.to_s, routine.to_s, *arg_types]
163
+ end
164
+ end
165
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PGTrunk
4
+ # @private
5
+ # The module record commands done during a migration.
6
+ module CommandRecorder
7
+ # @param [PGTrunk::Operation] klass
8
+ def self.register(klass)
9
+ define_method(klass.ruby_name) do |*args, &block|
10
+ record(klass.ruby_name, args, &block)
11
+ end
12
+ end
13
+
14
+ # @param [PGTrunk::Operation] klass
15
+ def self.register_inversion(klass)
16
+ define_method(klass.ruby_iname) do |args, &block|
17
+ original = klass.from_ruby(*args, &block)
18
+ inverted = original.invert!
19
+ # for example (skip_inversion(:validate_foreign_key))
20
+ return [:skip_inversion, [klass.ruby_name]] unless inverted
21
+
22
+ # list of attributes `to_a` is added for reporting to stdout
23
+ params = inverted.to_a
24
+ opts = inverted.to_opts
25
+ params << opts if opts.present?
26
+ [inverted.class.ruby_name, params]
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PGTrunk
4
+ # @private
5
+ # The module adds custom type casting
6
+ module CustomTypes
7
+ # All custom types are typecasted to strings in Rails
8
+ TYPE = ActiveRecord::ConnectionAdapters::PostgreSQL::OID::SpecializedString
9
+
10
+ def self.known
11
+ @known ||= Set.new([])
12
+ end
13
+
14
+ def enable_pg_trunk_types
15
+ execute(<<~SQL).each { |item| enable_pg_trunk_type item["name"] }
16
+ SELECT (
17
+ CASE
18
+ WHEN t.typnamespace = 'public'::regnamespace THEN t.typname
19
+ ELSE t.typnamespace::regnamespace || '.' || t.typname
20
+ END
21
+ ) AS name
22
+ FROM pg_trunk e JOIN pg_type t ON t.oid = e.oid
23
+ WHERE e.classid = 'pg_type'::regclass
24
+ SQL
25
+ end
26
+
27
+ def enable_pg_trunk_type(type)
28
+ type = type.to_s
29
+ CustomTypes.known << type
30
+ type_map.register_type(type, TYPE.new(type)) unless type_map.key?(type)
31
+ end
32
+
33
+ def valid_type?(type)
34
+ CustomTypes.known.include?(type.to_s) || super
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PGTrunk
4
+ # @private
5
+ # The module goes around the ActiveRecord::Migration's `method_missing`.
6
+ #
7
+ # This is necessary because +ActiveRecord::Migration#method_missing+
8
+ # forces the first argument to be a proper table name.
9
+ #
10
+ # In Rails migrations the first argument specifies the +table+,
11
+ # while the +name+ of the object can be specified by option:
12
+ #
13
+ # pg_trunk_create_index :users, %w[id name], name: 'users_idx'
14
+ #
15
+ # in PGTrunk the positional argument is always specify the name
16
+ # of the the current object (type, function, table etc.):
17
+ #
18
+ # pg_trunk_create_index 'users_ids', table: 'users', columns: %w[id name]
19
+ # create_enum 'currency', values: %w[USD EUR BTC]
20
+ #
21
+ # With this fix we can also use the options-only syntax like:
22
+ #
23
+ # pg_trunk_create_enum name: 'currency', values: %w[USD EUR BTC]
24
+ #
25
+ # or even skip any name when it can be generated from options:
26
+ #
27
+ # pg_trunk_create_index do |i|
28
+ # i.table 'users'
29
+ # i.column 'id'
30
+ # end
31
+ #
32
+ module Migration
33
+ # @param [PGTrunk::Operation] klass
34
+ def self.register(klass)
35
+ define_method(klass.ruby_name) do |*args, &block|
36
+ say_with_time "#{klass.ruby_name}(#{_pretty_args(*args)})" do
37
+ connection.send(klass.ruby_name, *args, &block)
38
+ end
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def _pretty_args(*args)
45
+ opts = args.last.is_a?(Hash) ? args.pop : {}
46
+ opts = opts.map { |k, v| "#{k}: #{v.inspect}" if v.present? }.compact
47
+ [*args.map(&:inspect), *opts].join(", ")
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PGTrunk
4
+ # @private
5
+ # This module extends the ActiveRecord::Migrator
6
+ # to clean the gem-specific registry `pg_trunk`:
7
+ #
8
+ # - set version to rows added by the current migration
9
+ # - delete rows that refer to objects deleted by the migration
10
+ #
11
+ # We need this because some objects (like check constraints,
12
+ # indexes etc.) can be dropped along with the table
13
+ # they refer to. This depencency is implicit-ish.
14
+ # That's why we have to check the presence of all objects in `pg_trunk`
15
+ # after every single migration.
16
+ module Migrator
17
+ def record_version_state_after_migrating(*)
18
+ super
19
+ PGTrunk::Registry.finalize
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PGTrunk
4
+ # @private
5
+ # Overloads methods defined in ActiveRecord::SchemaDumper
6
+ # to redefine how various objects must be dumped.
7
+ module SchemaDumper
8
+ class << self
9
+ def operations
10
+ @operations ||= []
11
+ end
12
+
13
+ def register(operation)
14
+ operations << operation unless operations.include?(operation)
15
+ end
16
+ end
17
+
18
+ # Here we totally redefining the way a schema is dumped.
19
+ #
20
+ # In Rails every table definition is dumped as an `add_table`
21
+ # creator including all its columns, indexes, type casts and foreign keys.
22
+ #
23
+ # In some circumstances, these objects can have inter-dependencies
24
+ # with others (like functions, custom types and constraints).
25
+ # For example, we could define a function getting table raw as an argument,
26
+ # and then use this function to define check constraint for the table.
27
+ # In this case we must insert the definition of the function between
28
+ # the table's and constraint's ones.
29
+ #
30
+ # That's why we can neither rely on the method, defined in ActiveRecord
31
+ # nor reuse it through fallback to `super` like both Scenic and F(x) do.
32
+ # Instead of it, we fetch object definitions from the database,
33
+ # and then resolve their inter-dependencies.
34
+ def dump(stream)
35
+ pg_trunk_register_custom_types
36
+ header(stream)
37
+ extensions(stream)
38
+ pg_trunk_objects(stream)
39
+ trailer(stream)
40
+ stream
41
+ end
42
+
43
+ private
44
+
45
+ # Before dumping the schema extract from SQL all custom types
46
+ # to enable their usage in table columns in the schema.
47
+ def pg_trunk_register_custom_types
48
+ @connection.enable_pg_trunk_types
49
+ end
50
+
51
+ def pg_trunk_objects(stream)
52
+ # Fetch operation definitions from the database.
53
+ #
54
+ # Operations of different kind are fetched
55
+ # in the order of their definitions (see `lib/pg_trunk/definitions.rb`).
56
+ # Operations of the same kind are sorted in a kind-specific order.
57
+ operations = SchemaDumper.operations.flat_map(&:to_a)
58
+ # Limit operations by oids known in `pg_trunk`
59
+ oids = PGTrunk::Registry.pluck(:oid)
60
+ operations = operations.select { |op| oids.include?(op.oid) }
61
+ # Resolve dependencies between fetched commands.
62
+ operations = PGTrunk::DependenciesResolver.resolve(operations)
63
+ # provide the content of the schema.
64
+ operations.each do |cmd|
65
+ cmd.dump(stream)
66
+ stream.puts
67
+ stream.puts
68
+ end
69
+ end
70
+
71
+ # Prevent indexes and check constraints from being added to the table
72
+ def indexes_in_create(_table, _stream); end
73
+ def check_constraints_in_create(_table, _stream); end
74
+ end
75
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PGTrunk
4
+ # @private
5
+ # The module makes `pg_trunk` gem-specific registry
6
+ # to be created and dropped along with the native schema.
7
+ module SchemaMigration
8
+ extend ActiveSupport::Concern
9
+
10
+ class_methods do
11
+ def create_table
12
+ super
13
+ PGTrunk::Registry.create_table
14
+ end
15
+
16
+ def drop_table
17
+ PGTrunk::Registry.drop_table
18
+ super
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PGTrunk
4
+ # @private
5
+ # The module adds commands to execute DDL operations in PostgreSQL.
6
+ module Statements
7
+ # @param [PGTrunk::Operation] klass
8
+ def self.register(klass)
9
+ define_method(klass.ruby_name) do |*args, &block|
10
+ operation = klass.from_ruby(*args, &block)
11
+ operation.validate!
12
+ PGTrunk.database.execute_operation(operation)
13
+ end
14
+ end
15
+
16
+ # A command does nothing when a unidirectional command is inverted
17
+ # (for example, when a foreign key validation is inverted).
18
+ # This case is different from those when an inversion cannot be made.
19
+ def skip_inversion(*); end
20
+ end
21
+ end