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,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PGTrunk
4
+ # @private
5
+ # Resolve dependencies between inter-dependent objects,
6
+ # identified by database `#oid` and comparable to each other.
7
+ #
8
+ # The method builds the sorted list:
9
+ # - parent objects moved before their dependants.
10
+ # - independent objects keeps their original order.
11
+ #
12
+ # We have no expectations about the natural order here.
13
+ # @see [PGTrunk::ObjectDefinitions::Operation]
14
+ class DependenciesResolver
15
+ class << self
16
+ # Resolve dependencies between objects
17
+ # @param [Array<Enumerable, #oid>] objects The list of objects
18
+ # @return [Array<#oid>] The sorted list of objects
19
+ # @raise [Dependencies::CycleError] if dependencies contain a cycle
20
+ def resolve(objects)
21
+ new(objects, dependencies).send(:sorted)
22
+ end
23
+
24
+ private
25
+
26
+ def query
27
+ <<~SQL.squish
28
+ SELECT child, array_agg(parent) AS parents
29
+ FROM (
30
+ SELECT
31
+ d.objid AS child,
32
+ d.refobjid AS parent
33
+ FROM pg_depend d
34
+ JOIN pg_trunk e1 ON d.objid = e1.oid
35
+ JOIN pg_trunk e2 ON d.refobjid = e2.oid
36
+ WHERE d.objsubid IS NULL
37
+ ) dependencies
38
+ GROUP BY child;
39
+ SQL
40
+ end
41
+
42
+ # Extract dependencies between given oids from the database
43
+ def dependencies
44
+ ActiveRecord::Base
45
+ .connection
46
+ .execute(query)
47
+ .each_with_object({}) do |i, obj|
48
+ child = i["child"].to_i
49
+ parents = i["parents"].scan(/\d+/).map(&:to_i)
50
+ obj[child] = parents.uniq
51
+ end
52
+ end
53
+ end
54
+
55
+ private
56
+
57
+ # @param [Array<Comparable, #oid>] objects Objects to sort
58
+ # @param [Hash<{Integer => Array<Integer>}] parents Dependencies between oid-s
59
+ def initialize(objects, parents)
60
+ @objects = objects
61
+ @parents = parents.transform_values do |oids|
62
+ # preserve the original order of objects
63
+ objects.each_with_object([]) do |obj, list|
64
+ list << obj if oids.include?(obj.oid)
65
+ end
66
+ end
67
+ end
68
+
69
+ attr_reader :objects, :parents
70
+
71
+ def visited
72
+ @visited ||= {}
73
+ end
74
+
75
+ def index
76
+ @index ||= objects.each_with_object({}) { |obj, idx| idx[obj.oid] = obj }
77
+ end
78
+
79
+ # Use topological sorting algorithm to resolve dependencies
80
+ # @return [Array<#oid>]
81
+ def sorted
82
+ @sorted ||= [].tap do |output|
83
+ while (object = next_unvisited)
84
+ visit(object, output)
85
+ end
86
+ end
87
+ end
88
+
89
+ def next_unvisited
90
+ objects.find { |object| !visited[object.oid] }
91
+ end
92
+
93
+ def visit(object, output)
94
+ return if visited[object.oid]
95
+
96
+ parents[object.oid]&.each { |parent| visit(parent, output) }
97
+ visited[object.oid] = true
98
+ output << object
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: false
2
+
3
+ require "rails/generators"
4
+ require "rails/generators/active_record"
5
+
6
+ # @private
7
+ # @abstract
8
+ # Module to build object-specific generators
9
+ module PGTrunk::Generators
10
+ extend ActiveSupport::Concern
11
+
12
+ class << self
13
+ # Add new generator for given operation
14
+ # @param [Class < PGTrunk::Operation] operation
15
+ def register(operation)
16
+ klass = build_generator(operation)
17
+ class_name = klass.name.split("::").last.to_sym
18
+ remove_const(class_name) if const_defined?(class_name)
19
+ const_set(class_name, klass)
20
+ end
21
+
22
+ # rubocop: disable Metrics/MethodLength
23
+ def build_generator(operation)
24
+ Class.new(Rails::Generators::NamedBase) do
25
+ include Rails::Generators::Migration
26
+ include PGTrunk::Generators
27
+
28
+ # Add the same arguments as in ruby method
29
+ operation.ruby_params.each do |name|
30
+ argument(name, **operation.attributes[name])
31
+ end
32
+
33
+ # Add the same options as in ruby method
34
+ operation.attributes.except(*operation.ruby_params).each do |name, opts|
35
+ class_option(name, **opts)
36
+ end
37
+
38
+ # The only command of the generator is to create a migration file
39
+ create_command(:create_migration_file)
40
+ end
41
+ end
42
+ # rubocop: enable Metrics/MethodLength
43
+ end
44
+
45
+ class_methods do
46
+ # @!attribute [r] fetcher The module including +PGTrunk::BaseFetcher+
47
+ attr_accessor :operation
48
+
49
+ # The name of the generated object like `foreign_key`
50
+ # for the `add_foreign_key` operation so that the command
51
+ #
52
+ # rails g foreign_key 'users', 'roles'
53
+ #
54
+ # to build the migration containing
55
+ #
56
+ # def change
57
+ # add_foreign_key 'users', 'roles'
58
+ # end
59
+ #
60
+ def object_name
61
+ @object_name ||= operation.object.singularize
62
+ end
63
+
64
+ # Use the name of the object as a name of the generator class
65
+ def name
66
+ @name ||= "PGTrunk::Generators::#{object_name.camelize}"
67
+ end
68
+
69
+ # The name of the operation to be added to the migration
70
+ def operation_name
71
+ @operation_name ||= operation.ruby_name
72
+ end
73
+
74
+ # Ruby handler to add positional arguments to options
75
+ def handle(*arguments, **options)
76
+ options.ruby_params.zip(arguments).merge(options)
77
+ end
78
+
79
+ def next_migration_number(dir)
80
+ ::ActiveRecord::Generators::Base.next_migration_number(dir)
81
+ end
82
+ end
83
+
84
+ def create_migration_file
85
+ file_name = "db/migrate/#{migration_number}_#{migration_name}.rb"
86
+ file = create_migration(file_name, nil, {}) do
87
+ <<~RUBY
88
+ # frozen_string_literal: true
89
+
90
+ class #{migration_name.camelize} < #{migration_base}
91
+ def change
92
+ #{command}
93
+ end
94
+ end
95
+ RUBY
96
+ end
97
+ Rails::Generators.add_generated_file(file)
98
+ end
99
+
100
+ private
101
+
102
+ def current_version
103
+ return @current_version if instance_variable_defined?(:@current_version)
104
+
105
+ @current_version = nil
106
+ return unless ActiveRecord::Migration.respond_to?(:current_version)
107
+
108
+ @current_version = ActiveRecord::Migration.current_version
109
+ end
110
+
111
+ # Build the name of the migration from given params like:
112
+ #
113
+ # rails g foreign_key 'users', 'roles'
114
+ #
115
+ # to generate the migration named as:
116
+ #
117
+ # class AddForeignKeyUsersRoles < ::ActiveRecord::Migration[6.2]
118
+ # # ...
119
+ # end
120
+ def migration_name
121
+ @migration_name ||= [
122
+ self.class.operation_name,
123
+ *(self.class.operation.ruby_params.map { |p| send(p) }),
124
+ ].join("_")
125
+ end
126
+
127
+ def migration_base
128
+ @migration_base ||= "::ActiveRecord::Migration".tap do |mb|
129
+ next if Rails::Version::MAJOR < "5"
130
+
131
+ mb << "[#{current_version}]" if current_version.present?
132
+ end
133
+ end
134
+
135
+ def command
136
+ opts = self.class.handle(*arguments, **options.symbolize_keys)
137
+ operation = self.class.operation.new(opts)
138
+ operation.to_ruby.indent(4).strip
139
+ end
140
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ class PGTrunk::Operation
4
+ # @private
5
+ # Define getters/setters for the operation attributes
6
+ module Attributes
7
+ extend ActiveSupport::Concern
8
+ include ActiveModel::Model
9
+ include ActiveModel::Attributes
10
+
11
+ # The special undefined value for getters/setters
12
+ # to distinct it from the explicitly provided `nil`.
13
+ UNDEFINED = Object.new.freeze
14
+ private_constant :UNDEFINED
15
+
16
+ class_methods do
17
+ # list of aliases for attributes
18
+ def attr_aliases
19
+ @attr_aliases ||= {}
20
+ end
21
+
22
+ # Add an attribute to the operation
23
+ def attribute(name, type, default: nil, aliases: nil)
24
+ # prevent mutation of the default arrays, hashes etc.
25
+ default = default.freeze if default.respond_to?(:freeze)
26
+ super(name, type, default: default, &nil)
27
+ # add the private attribute for the previous value
28
+ attr_reader :"from_#{name}"
29
+
30
+ redefine_getter(name)
31
+ Array(aliases).each { |key| attr_aliases[key.to_sym] = name.to_sym }
32
+ name.to_sym
33
+ end
34
+
35
+ private
36
+
37
+ def redefine_getter(name)
38
+ getter = instance_method(name)
39
+ define_method(name) do |value = UNDEFINED, *args, from: nil|
40
+ # fallback to the original getter w/o arguments
41
+ return getter.bind(self).call if value == UNDEFINED
42
+
43
+ # set a previous value to return to
44
+ instance_variable_set("@from_#{name}", from)
45
+
46
+ # arrays can be assigned as lists
47
+ value = [value, *args] if args.any?
48
+
49
+ send(:"#{name}=", value)
50
+ end
51
+ end
52
+
53
+ def inherited(klass)
54
+ klass.instance_variable_set(:@attr_aliases, attr_aliases)
55
+ super
56
+ end
57
+ end
58
+
59
+ def initialize(**opts)
60
+ # enable aliases during the initialization
61
+ self.class.attr_aliases.each do |a, n|
62
+ opts[n] = opts.delete(a) if opts.key?(a)
63
+ end
64
+ # ignore unknown attributes (to simplify calls of `#invert`)
65
+ opts = opts.slice(*self.class.attribute_names.map(&:to_sym))
66
+
67
+ super(**opts)
68
+ end
69
+
70
+ # The hash of the operation's serialized attributes
71
+ def attributes
72
+ super.to_h do |k, v|
73
+ [k.to_sym, self.class.attribute_types[k].serialize(v)]
74
+ end
75
+ end
76
+ alias to_h attributes
77
+ end
78
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ class PGTrunk::Operation
4
+ # @private
5
+ # Enable to fulfill/generate missed attributes
6
+ # using the `after_initialize` callback.
7
+ #
8
+ # The callback is invoked after the end of the normal
9
+ # initialization and applying a block with explicit settings.
10
+ module Callbacks
11
+ extend ActiveSupport::Concern
12
+
13
+ class_methods do
14
+ def callbacks
15
+ @callbacks ||= []
16
+ end
17
+
18
+ # Get or set the callback
19
+ def after_initialize(&block)
20
+ callbacks << block if block
21
+ end
22
+
23
+ private
24
+
25
+ def inherited(klass)
26
+ klass.instance_variable_set(:@callbacks, callbacks.dup)
27
+ super
28
+ end
29
+ end
30
+
31
+ private def initialize(*, **, &block)
32
+ # Explicitly assign all attributes from params/options.
33
+ super
34
+ # Explicitly assign attributes using a block.
35
+ block&.call(self)
36
+ # Apply +callback+ at the very end after all explicit assignments.
37
+ self.class.callbacks.each { |callback| instance_exec(&callback) }
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ class PGTrunk::Operation
4
+ # @private
5
+ # Register attributes definition for later usage by generators
6
+ module Generators
7
+ extend ActiveSupport::Concern
8
+
9
+ class_methods do
10
+ # Gets or sets object name for the generator
11
+ def generates_object(name = nil)
12
+ @generates_object = name if name
13
+ @generates_object ||= nil
14
+ end
15
+
16
+ # The definitions of the attributes
17
+ # @return [Hash{Symbol => Hash{type:, default:, desc:}}]
18
+ def attributes
19
+ @attributes ||= {}
20
+ end
21
+
22
+ def attribute(name, type, default: nil, desc: nil, **opts)
23
+ name = name.to_sym
24
+ attributes[name] = {
25
+ type: gen_type(type),
26
+ default: default,
27
+ desc: desc,
28
+ }
29
+ super(name, type.to_sym, default: default, **opts)
30
+ end
31
+
32
+ private
33
+
34
+ def inherited(klass)
35
+ klass.instance_variable_set(:@attributes, attributes.dup)
36
+ super
37
+ end
38
+
39
+ # Convert the type to the acceptable by Rails::Generator
40
+ def gen_type(type)
41
+ case type.to_s
42
+ when "bool", "boolean" then :boolean
43
+ when "integer", "float" then :numeric
44
+ when /^pg_trunk_array/ then :array
45
+ when /^pg_trunk_hash/ then :hash
46
+ else :string
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ class PGTrunk::Operation
4
+ # @private
5
+ # The exception to be thrown when reversed migration isn't valid
6
+ class IrreversibleMigration < ActiveRecord::IrreversibleMigration
7
+ private
8
+
9
+ def initialize(operation, inversion, *messages)
10
+ msg = "#{header(operation)}#{inverted(inversion)} #{footer(messages)}"
11
+ super(msg.strip)
12
+ end
13
+
14
+ def header(operation)
15
+ <<~MSG
16
+ This migration uses the operation:
17
+
18
+ #{operation.to_ruby.indent(2).strip}
19
+
20
+ MSG
21
+ end
22
+
23
+ def inverted(inversion)
24
+ return "which is not automatically reversible" unless inversion
25
+
26
+ <<~MSG.strip
27
+ whose inversion would be like:
28
+
29
+ #{inversion.to_ruby.indent(2).strip}
30
+
31
+ which is invalid
32
+ MSG
33
+ end
34
+
35
+ def footer(messages)
36
+ reasons = <<~REASONS.strip if messages.any?
37
+ for the following reasons:
38
+
39
+ #{messages.map { |m| "- #{m}" }.join("\n")}
40
+ REASONS
41
+
42
+ <<~MSG.strip
43
+ #{reasons}
44
+
45
+ To make the migration reversible you can either:
46
+ 1. Define #up and #down methods in place of the #change method.
47
+ 2. Use the #reversible method to define reversible behavior.
48
+ MSG
49
+ end
50
+ end
51
+
52
+ # @private
53
+ # Enable operations to be invertible
54
+ module Inversion
55
+ # @private
56
+ def invert!
57
+ invert&.tap do |i|
58
+ i.valid? || raise(IrreversibleMigration.new(self, i, *i.error_messages))
59
+ end
60
+ end
61
+
62
+ # @private
63
+ def irreversible!(option)
64
+ raise IrreversibleMigration.new(self, nil, <<~MSG.squish)
65
+ The operation with the `#{option}` option cannot be reversed
66
+ due to uncertainty of the previous state of the database.
67
+ MSG
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ class PGTrunk::Operation
4
+ # @private
5
+ # Invoke all the necessary definitions
6
+ # in the modules included to Rails via Railtie
7
+ module Registration
8
+ extend ActiveSupport::Concern
9
+
10
+ class_methods do
11
+ def from_sql(&block)
12
+ super.tap { register_dumper if block }
13
+ end
14
+
15
+ def generates_object(name = nil)
16
+ super.tap { register_generator if name }
17
+ end
18
+
19
+ def method_added(name)
20
+ super
21
+ ensure
22
+ register_operation if name == :to_sql
23
+ register_inversion if name == :invert
24
+ end
25
+
26
+ private
27
+
28
+ def register_operation
29
+ # Add the method to statements as an entry point
30
+ PGTrunk::Statements.register(self)
31
+ # Add the shortcut to migration go get away with checking
32
+ # of the first parameter which could be NOT a table name.
33
+ PGTrunk::Migration.register(self)
34
+ # Record the direct operation
35
+ PGTrunk::CommandRecorder.register(self)
36
+ end
37
+
38
+ def register_inversion
39
+ # Record the inversion of the operation
40
+ PGTrunk::CommandRecorder.register_inversion(self)
41
+ end
42
+
43
+ def register_dumper
44
+ PGTrunk::SchemaDumper.register(self)
45
+ end
46
+
47
+ def register_generator
48
+ # skip registration in the runtime
49
+ return unless const_defined?("PGTrunk::Generators")
50
+
51
+ PGTrunk::Generators.register(self)
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ class PGTrunk::Operation
4
+ # @private
5
+ # Build ruby snippet
6
+ class RubyBuilder
7
+ private def initialize(name, shortage: nil)
8
+ @args = []
9
+ @lines = []
10
+ @name = name&.to_s
11
+ @opts = []
12
+ @shortage = shortage
13
+ end
14
+
15
+ # Add parameters to the method call
16
+ def ruby_param(*args, **opts)
17
+ @args = [*@args, *params(*args)]
18
+ @opts = [*@opts, *params(**opts)]
19
+ end
20
+
21
+ # Add line into a block
22
+ def ruby_line(meth, *args, **opts)
23
+ return if meth.blank?
24
+ return if args.first.nil?
25
+
26
+ @lines << build_line(meth, *args, **opts)
27
+ end
28
+
29
+ # Build the snippet
30
+ # @return [String]
31
+ def build
32
+ [header, *block].join(" ")
33
+ end
34
+
35
+ private
36
+
37
+ # Pattern to split lines by heredocs
38
+ HEREDOC = /<<~'?(?<head>[A-Z]+)'?.+(?<body>\n .+)*\n\k<head>/.freeze
39
+
40
+ def build_line(meth, *args, **opts)
41
+ method_name = [shortage, meth].join(".")
42
+ method_params = params(*args, **opts)
43
+ line = [method_name, *method_params].join(" ")
44
+ return single_line(line).indent(2) unless block_given?
45
+
46
+ builder = self.class.new(line, shortage: "f")
47
+ yield(builder)
48
+ builder.build.indent(2)
49
+ end
50
+
51
+ # Finalize line containing a heredoc args
52
+ # "body <<~'SQL'.chomp\n foo\nSQL, from: <<~'SQL'.chomp\n bar\nSQL"
53
+ # "body <<~'SQL'.chomp, from: <<~'SQL'.chomp\n foo\nSQL\n bar\nSQL"
54
+ def single_line(text)
55
+ parts = text.partition(HEREDOC)
56
+ (
57
+ parts.map { |p| p[/^.+/] } + parts.map { |p| p[/\n(\n|.)*$/] }
58
+ ).compact.join
59
+ end
60
+
61
+ def shortage
62
+ @shortage ||= @name.split("_").last.first
63
+ end
64
+
65
+ def format(value)
66
+ case value
67
+ when Hash then value
68
+ when String then format_text(value)
69
+ when Array then format_list(value)
70
+ else value.inspect
71
+ end
72
+ end
73
+
74
+ def format_text(text)
75
+ text = text.chomp
76
+ # prevent quoting interpolations and heredocs
77
+ return text if text[/^<<~|^%[A-Za-z][(]/]
78
+
79
+ long_text = text.size > 50 || text.include?("\n")
80
+ return "<<~'Q'.chomp\n#{text.indent(2)}\nQ" if long_text && text["\\"]
81
+ return "<<~Q.chomp\n#{text.indent(2)}\nQ" if long_text
82
+ return "%q(#{text})" if /\\|"/.match?(text)
83
+
84
+ text.inspect
85
+ end
86
+
87
+ def format_list(list)
88
+ case list.map(&:class).uniq
89
+ when [::String] then "%w[#{list.join(' ')}]"
90
+ when [::Symbol] then "%i[#{list.join(' ')}]"
91
+ else list
92
+ end
93
+ end
94
+
95
+ def params(*values, **options)
96
+ vals = values.map { |val| format(val) }
97
+ opts = options.compact.map { |key, val| "#{key}: #{format(val)}" }
98
+ [*vals, *opts].join(", ").presence
99
+ end
100
+
101
+ def header
102
+ method_params = [*@args, *@opts].join(", ").presence
103
+ line = [@name, *method_params].join(" ")
104
+ line << " do |#{shortage}|" if @lines.any?
105
+ single_line(line)
106
+ end
107
+
108
+ def block
109
+ [nil, *@lines, "end"].join("\n") if @lines.any?
110
+ end
111
+ end
112
+ end