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,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