pg_trunk 0.1.0

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