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,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ # nodoc
4
+ module PGTrunk
5
+ # @private
6
+ # Turn in PGTrunk-relates stuff in the Rails app
7
+ class Railtie < Rails::Railtie
8
+ require_relative "railtie/command_recorder"
9
+ require_relative "railtie/custom_types"
10
+ require_relative "railtie/migration"
11
+ require_relative "railtie/migrator"
12
+ require_relative "railtie/schema_dumper"
13
+ require_relative "railtie/schema_migration"
14
+ require_relative "railtie/statements"
15
+
16
+ initializer("pg_trunk.load") do
17
+ ActiveSupport.on_load(:active_record) do
18
+ # overload schema dumper to use gem-specific object fetchers
19
+ ActiveRecord::SchemaDumper.prepend PGTrunk::SchemaDumper
20
+ # add custom type casting
21
+ ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.prepend PGTrunk::CustomTypes
22
+ # add migration methods
23
+ ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.prepend PGTrunk::Statements
24
+ # register those methods for migration directions
25
+ ActiveRecord::Migration::CommandRecorder.include PGTrunk::CommandRecorder
26
+ # support the registry table `pg_trunk` in addition to `schema_migrations`
27
+ ActiveRecord::SchemaMigration.prepend PGTrunk::SchemaMigration
28
+ # fix migration to enable different syntax without the name of the table
29
+ ActiveRecord::Migration.prepend PGTrunk::Migration
30
+ # make the migrator to remove stale records from `pg_trunk`
31
+ ActiveRecord::Migrator.prepend PGTrunk::Migrator
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PGTrunk
4
+ # @private
5
+ # The internal model to represent the gem-specific registry
6
+ # where we store information about objects added by migrations.
7
+ #
8
+ # Every time when an object is created, we should record it
9
+ # in the table, setting its `oid` along with the reference
10
+ # to the system table (`classid::oid`).
11
+ #
12
+ # The third column `version::text` keeps the current version
13
+ # where the object has been added.
14
+ #
15
+ # rubocop: disable Metrics/ClassLength
16
+ class Registry < ActiveRecord::Base
17
+ class << self
18
+ def _internal?
19
+ true
20
+ end
21
+
22
+ def primary_key
23
+ "oid"
24
+ end
25
+
26
+ def table_name
27
+ "pg_trunk"
28
+ end
29
+
30
+ # rubocop: disable Metrics/MethodLength
31
+ def create_table
32
+ return if connection.table_exists?(table_name)
33
+
34
+ connection.create_table(
35
+ table_name,
36
+ id: false,
37
+ if_not_exists: true,
38
+ comment: "Objects added by migrations",
39
+ ) do |t|
40
+ t.column :oid, :oid, primary_key: true, comment: "Object identifier"
41
+ t.column :classid, :oid, null: false, comment: \
42
+ "ID of the systems catalog in pg_class"
43
+ t.column :version, :string, index: true, comment: \
44
+ "Version of the migration that added the object"
45
+ t.foreign_key ActiveRecord::Base.schema_migrations_table_name,
46
+ column: :version, primary_key: :version,
47
+ on_update: :cascade, on_delete: :cascade
48
+ end
49
+ end
50
+ # rubocop: enable Metrics/MethodLength
51
+
52
+ # This method is called by a migrator after applying
53
+ # all migrations in whatever direction.
54
+ def finalize
55
+ connection.execute [
56
+ *create_table,
57
+ *forget_dropped_objects,
58
+ *remember_tables,
59
+ *fill_missed_version,
60
+ ].join(";")
61
+ end
62
+
63
+ def drop_table
64
+ connection.drop_table table_name, if_exists: true
65
+ end
66
+
67
+ private
68
+
69
+ # List of service tables that shouldn't get into the registry.
70
+ SERVICE_TABLES = [
71
+ ActiveRecord::Base.schema_migrations_table_name,
72
+ ActiveRecord::Base.internal_metadata_table_name,
73
+ "pg_trunk",
74
+ ].freeze
75
+
76
+ def catalogs
77
+ connection
78
+ .execute("SELECT DISTINCT classid::regclass FROM #{table_name}")
79
+ .map { |item| item["classid"] }
80
+ end
81
+
82
+ # Delete all objects which are absent in system catalogs
83
+ # (they could be deleted either explicitly, or through
84
+ # the cascade dependencies clearance).
85
+ def forget_dropped_objects
86
+ catalogs.map do |tbl|
87
+ <<~SQL.squish
88
+ DELETE FROM #{table_name}
89
+ WHERE classid = '#{tbl}'::regclass
90
+ AND oid NOT IN (SELECT oid FROM #{tbl});
91
+ SQL
92
+ end
93
+ end
94
+
95
+ # Register all tables known to Rails
96
+ # along with their indexes, check constraints and foreign keys.
97
+ # This would let us fetch those objects even though
98
+ # they were created by native methods of +ActiveRecord+ like
99
+ # `create_table` etc.
100
+ def remember_tables
101
+ names_and_schemas = names_and_schemas_sql
102
+ return unless names_and_schemas
103
+
104
+ <<~SQL.squish
105
+ WITH
106
+ tbl AS (
107
+ SELECT oid FROM pg_class
108
+ WHERE #{names_and_schemas} AND relkind IN ('r', 'p')
109
+ ),
110
+ idx AS (
111
+ SELECT r.oid
112
+ FROM pg_class r
113
+ JOIN pg_index i ON r.oid = i.indexrelid
114
+ JOIN tbl ON i.indrelid = tbl.oid
115
+ ),
116
+ con AS (
117
+ SELECT c.oid AS oid
118
+ FROM pg_constraint c
119
+ JOIN tbl ON c.conrelid = tbl.oid
120
+ WHERE c.contype IN ('c', 'f')
121
+ ),
122
+ obj (oid, classid) AS (
123
+ SELECT oid, 'pg_class'::regclass FROM tbl
124
+ UNION
125
+ SELECT oid, 'pg_class'::regclass FROM idx
126
+ UNION
127
+ SELECT oid, 'pg_constraint'::regclass FROM con
128
+ )
129
+ INSERT INTO #{table_name} (oid, classid)
130
+ SELECT oid, classid FROM obj
131
+ ON CONFLICT DO NOTHING;
132
+ SQL
133
+ end
134
+
135
+ # Assign the most recent version to new records in `pg_trunk`.
136
+ def fill_missed_version
137
+ <<~SQL
138
+ UPDATE #{table_name} SET version = list.version
139
+ FROM (
140
+ SELECT max(version) AS version
141
+ FROM "#{ActiveRecord::Base.schema_migrations_table_name}"
142
+ ) list
143
+ WHERE #{table_name}.version IS NULL;
144
+ SQL
145
+ end
146
+
147
+ def names_and_schemas_sql
148
+ (connection.tables - SERVICE_TABLES)
149
+ .map { |table| QualifiedName.wrap(table) }
150
+ .group_by(&:namespace)
151
+ .transform_values { |list| list.map(&:quoted).join(",") }
152
+ .map { |nsp, tbl| "relnamespace = #{nsp} AND relname IN (#{tbl})" }
153
+ .join("OR")
154
+ .presence
155
+ end
156
+ end
157
+ end
158
+ # rubocop: enable Metrics/ClassLength
159
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @private
4
+ module PGTrunk::Serializers
5
+ # @private
6
+ # Cast the attribute value as an array of strings.
7
+ # It knows how to cast arrays returned by PostgreSQL
8
+ # as a string like '{USD,EUR,GBP}' into ['USD', 'EUR', 'GBP'].
9
+ class ArrayOfHashesSerializer < ActiveRecord::Type::Value
10
+ def cast(value)
11
+ case value
12
+ when ::String then JSON.parse(value).map(&:symbolize_keys)
13
+ when ::NilClass then []
14
+ when ::Array then value.map(&:to_h)
15
+ else [value.to_h]
16
+ end
17
+ end
18
+
19
+ def serialize(value)
20
+ Array.wrap(value).map { |item| item.to_h.symbolize_keys }
21
+ end
22
+ end
23
+
24
+ ActiveModel::Type.register(
25
+ :pg_trunk_array_of_hashes,
26
+ ArrayOfHashesSerializer,
27
+ )
28
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @private
4
+ module PGTrunk::Serializers
5
+ # @private
6
+ # Cast the attribute value as an array of strings.
7
+ # It knows how to cast arrays returned by PostgreSQL
8
+ # as a string like '{USD,EUR,GBP}' into ['USD', 'EUR', 'GBP'].
9
+ class ArrayOfStringsSerializer < ActiveRecord::Type::Value
10
+ def cast(value)
11
+ case value
12
+ when ::String
13
+ value.gsub(/^\{|\}$/, "").split(",")
14
+ when ::NilClass then []
15
+ when ::Array then value.map(&:to_s)
16
+ else [value.to_s]
17
+ end
18
+ end
19
+
20
+ def serialize(value)
21
+ value
22
+ end
23
+ end
24
+
25
+ ActiveModel::Type.register(
26
+ :pg_trunk_array_of_strings,
27
+ ArrayOfStringsSerializer,
28
+ )
29
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @private
4
+ module PGTrunk::Serializers
5
+ # @private
6
+ # The same as the array of strings with symbolization at the end
7
+ class ArrayOfSymbolsSerializer < ActiveRecord::Type::Value
8
+ def cast(value)
9
+ case value
10
+ when ::NilClass then []
11
+ when ::Symbol then [value]
12
+ when ::String
13
+ value.gsub(/^\{|\}$/, "").split(",").map(&:to_sym)
14
+ when ::Array then value.map { |i| i.to_s.to_sym }
15
+ else [value.to_s.to_sym]
16
+ end
17
+ end
18
+
19
+ def serialize(value)
20
+ value
21
+ end
22
+ end
23
+
24
+ ActiveModel::Type.register(
25
+ :pg_trunk_array_of_symbols,
26
+ ArrayOfSymbolsSerializer,
27
+ )
28
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @private
4
+ module PGTrunk::Serializers
5
+ # @private
6
+ # Cast the attribute value as an array, not caring about its content.
7
+ class ArraySerializer < ActiveRecord::Type::Value
8
+ def cast(value)
9
+ case value
10
+ when ::NilClass then []
11
+ when ::Array then value
12
+ else [value]
13
+ end
14
+ end
15
+
16
+ def serialize(value)
17
+ value
18
+ end
19
+ end
20
+
21
+ ActiveModel::Type.register(:pg_trunk_array, ArraySerializer)
22
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @private
4
+ module PGTrunk::Serializers
5
+ # @private
6
+ # Cast the attribute value as a non-empty stripped string in lowercase
7
+ class LowercaseStringSerializer < ActiveRecord::Type::Value
8
+ def cast(value)
9
+ value.to_s.presence&.downcase&.strip
10
+ end
11
+
12
+ def serialize(value)
13
+ value.to_s
14
+ end
15
+ end
16
+
17
+ ActiveModel::Type.register(
18
+ :pg_trunk_lowercase_string,
19
+ LowercaseStringSerializer,
20
+ )
21
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @private
4
+ module PGTrunk::Serializers
5
+ # @private
6
+ # Cast the attribute value as a multiline text
7
+ # with right-stripped lines and without empty lines.
8
+ class MultilineTextSerializer < ActiveRecord::Type::Value
9
+ def cast(value)
10
+ return if value.blank?
11
+
12
+ value.to_s.lines.map(&:strip).reject(&:blank?).join("\n")
13
+ end
14
+
15
+ def serialize(value)
16
+ value&.to_s
17
+ end
18
+ end
19
+
20
+ ActiveModel::Type.register(:pg_trunk_multiline_text, MultilineTextSerializer)
21
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @private
4
+ module PGTrunk::Serializers
5
+ # @private
6
+ # Cast the attribute value as a qualified name.
7
+ class QualifiedNameSerializer < ActiveRecord::Type::Value
8
+ TYPE = ::PGTrunk::QualifiedName
9
+
10
+ def cast(value)
11
+ case value
12
+ when NilClass then nil
13
+ when TYPE then value
14
+ else TYPE.wrap(value.to_s)
15
+ end
16
+ end
17
+
18
+ def serialize(value)
19
+ value.is_a?(TYPE) ? value.lean : value&.to_s
20
+ end
21
+ end
22
+
23
+ ActiveModel::Type.register(
24
+ :pg_trunk_qualified_name,
25
+ PGTrunk::Serializers::QualifiedNameSerializer,
26
+ )
27
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @private
4
+ module PGTrunk::Serializers
5
+ # @private
6
+ # Cast the attribute value as a symbol.
7
+ class SymbolSerializer < ActiveRecord::Type::Value
8
+ def cast(value)
9
+ return if value.blank?
10
+ return value if value.is_a?(Symbol)
11
+ return value.to_sym if value.respond_to?(:to_sym)
12
+
13
+ value.to_s.to_sym
14
+ end
15
+
16
+ def serialize(value)
17
+ value
18
+ end
19
+ end
20
+
21
+ ActiveModel::Type.register(:pg_trunk_symbol, SymbolSerializer)
22
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PGTrunk
4
+ # @private
5
+ # Namespace for the gem-specific activemodel serializers
6
+ module Serializers
7
+ require_relative "serializers/array_serializer"
8
+ require_relative "serializers/array_of_hashes_serializer"
9
+ require_relative "serializers/array_of_strings_serializer"
10
+ require_relative "serializers/array_of_symbols_serializer"
11
+ require_relative "serializers/lowercase_string_serializer"
12
+ require_relative "serializers/multiline_text_serializer"
13
+ require_relative "serializers/qualified_name_serializer"
14
+ require_relative "serializers/symbol_serializer"
15
+ end
16
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @private
4
+ # Ensure that all items in the array are valid
5
+ class PGTrunk::AllItemsValidValidator < ActiveModel::EachValidator
6
+ def validate_each(record, attribute, value)
7
+ Array.wrap(value).each.with_index.map do |item, index|
8
+ item.errors.messages.each do |name, list|
9
+ list.each do |message|
10
+ record.errors.add :base, "#{attribute}[#{index}]: #{name} #{message}"
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @private
4
+ # Ensure that an attribute is different from another one
5
+ class PGTrunk::DifferenceValidator < ActiveModel::EachValidator
6
+ def validate_each(record, attribute, value)
7
+ another_name = options.fetch(:from)
8
+ another_value = record.send(another_name).presence
9
+
10
+ case another_value
11
+ when PGTrunk::QualifiedName
12
+ return unless value.maybe_eq?(another_value)
13
+ else
14
+ return unless value == another_value
15
+ end
16
+
17
+ record.errors.add attribute, "must be different from the #{another_name}"
18
+ end
19
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PGTrunk
4
+ # @private
5
+ # Namespace for the gem-specific activemodel validators
6
+ module Validators
7
+ require_relative "validators/all_items_valid_validator"
8
+ require_relative "validators/difference_validator"
9
+ end
10
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This class loads the base mechanics of the gem
4
+ # isolated in the corresponding folder.
5
+
6
+ # nodoc
7
+ module PGTrunk
8
+ require_relative "core/adapters/postgres"
9
+ require_relative "core/railtie"
10
+ require_relative "core/qualified_name"
11
+ require_relative "core/registry"
12
+ require_relative "core/serializers"
13
+ require_relative "core/validators"
14
+ require_relative "core/operation"
15
+ require_relative "core/dependencies_resolver"
16
+
17
+ # @private
18
+ def database
19
+ Adapters::Postgres.new
20
+ end
21
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "version"
4
+ require_relative "core/serializers"
5
+ require_relative "core/operation"
6
+ require_relative "core/generators"
7
+ require_relative "operations"
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: false
2
+
3
+ # @!method ActiveRecord::Migration#add_check_constraint(table, expression = nil, **options, &block)
4
+ # Add a check constraint to the table
5
+ #
6
+ # @param [#to_s] table (nil) The qualified name of the table
7
+ # @param [#to_s] expression (nil) The SQL expression
8
+ # @option [#to_s] :name (nil) The optional name of the constraint
9
+ # @option [Boolean] :inherit (true) If the constraint should be inherited by subtables
10
+ # @option [#to_s] :comment (nil) The comment describing the constraint
11
+ # @yield [Proc] the block with the constraint's definition
12
+ # @yieldparam The receiver of methods specifying the constraint
13
+ #
14
+ # The name of the new constraint can be set explicitly
15
+ #
16
+ # add_check_constraint :users, "length(phone) > 10",
17
+ # name: "phone_is_long_enough",
18
+ # inherit: false,
19
+ # comment: "Phone is 10+ chars long"
20
+ #
21
+ # The name can also be skipped (it will be generated by default):
22
+ #
23
+ # add_check_constraint :users, "length(phone) > 1"
24
+ #
25
+ # The block syntax can be used for any argument as usual:
26
+ #
27
+ # add_check_constraint do |c|
28
+ # c.table "users"
29
+ # c.expression "length(phone) > 10"
30
+ # c.name "phone_is_long_enough"
31
+ # c.inherit false
32
+ # c.comment "Phone is 10+ chars long"
33
+ # end
34
+ #
35
+ # The operation is always reversible.
36
+
37
+ module PGTrunk::Operations::CheckConstraints
38
+ # @private
39
+ class AddCheckConstraint < Base
40
+ # The operation is used by the generator `rails g check_constraint`
41
+ generates_object :check_constraint
42
+
43
+ validates :expression, presence: true
44
+ validates :if_exists, :new_name, :force, absence: true
45
+
46
+ from_sql do
47
+ <<~SQL
48
+ SELECT
49
+ c.oid,
50
+ c.conname AS name,
51
+ c.connamespace::regnamespace AS schema,
52
+ r.relnamespace::regnamespace || '.' || r.relname AS "table",
53
+ (
54
+ NOT c.connoinherit
55
+ ) AS inherit,
56
+ (
57
+ regexp_match(
58
+ pg_get_constraintdef(c.oid),
59
+ '^CHECK [(][(](.+)[)][)]( NO INHERIT)?$'
60
+ )
61
+ )[1] AS expression,
62
+ d.description AS comment
63
+ FROM pg_constraint c
64
+ JOIN pg_class r ON r.oid = c.conrelid
65
+ LEFT JOIN pg_description d ON c.oid = d.objoid
66
+ WHERE c.contype = 'c';
67
+ SQL
68
+ end
69
+
70
+ def to_sql(_version)
71
+ [add_constraint, *add_comment, register_constraint].compact.join(" ")
72
+ end
73
+
74
+ def invert
75
+ DropCheckConstraint.new(**to_h)
76
+ end
77
+
78
+ private
79
+
80
+ def add_constraint
81
+ sql = "ALTER TABLE #{table.to_sql} ADD CONSTRAINT #{name.name.inspect}"
82
+ sql << " CHECK (#{expression})"
83
+ sql << " NO INHERIT" unless inherit
84
+ sql << ";"
85
+ end
86
+
87
+ def add_comment
88
+ return if comment.blank?
89
+
90
+ <<~SQL
91
+ COMMENT ON CONSTRAINT #{name.lean.inspect} ON #{table.to_sql}
92
+ IS $comment$#{comment}$comment$;
93
+ SQL
94
+ end
95
+
96
+ # Rely on the fact the (schema.table, schema.name) is unique
97
+ def register_constraint
98
+ <<~SQL
99
+ INSERT INTO pg_trunk (oid, classid)
100
+ SELECT c.oid, 'pg_constraint'::regclass
101
+ FROM pg_constraint c JOIN pg_class r ON r.oid = c.conrelid
102
+ WHERE r.relname = #{table.quoted}
103
+ AND r.relnamespace = #{table.namespace}
104
+ AND c.conname = #{name.quoted}
105
+ ON CONFLICT DO NOTHING;
106
+ SQL
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: false
2
+
3
+ module PGTrunk::Operations::CheckConstraints
4
+ # @abstract
5
+ # @private
6
+ # Base class for operations with check constraints
7
+ class Base < PGTrunk::Operation
8
+ # All attributes that can be used by check-related commands
9
+ attribute :expression, :string
10
+ attribute :inherit, :boolean, default: true
11
+ attribute :table, :pg_trunk_qualified_name
12
+
13
+ # Generate missed name from table & expression
14
+ after_initialize { self.name = generated_name if name.blank? }
15
+
16
+ # Ensure correctness of present values
17
+ # The table must be defined because the name only
18
+ # is not enough to identify the constraint.
19
+ validates :if_not_exists, absence: true
20
+ validates :table, presence: true
21
+
22
+ # By default foreign keys are sorted by tables and names.
23
+ def <=>(other)
24
+ return unless other.is_a?(self.class)
25
+
26
+ result = table <=> other.table
27
+ result.zero? ? super : result
28
+ end
29
+
30
+ # Support `table` and `expression` in positional arguments
31
+ # @example
32
+ # add_check_constraint :users, "length(phone) == 10", **opts
33
+ ruby_params :table, :expression
34
+
35
+ # Snippet to be used in all operations with check constraints
36
+ ruby_snippet do |s|
37
+ s.ruby_param(table.lean) if table.present?
38
+ s.ruby_param(expression) if expression.present?
39
+ s.ruby_param(if_exists: true) if if_exists
40
+ s.ruby_param(inherit: false) if inherit&.== false
41
+ s.ruby_param(name: name.lean) if custom_name?
42
+ s.ruby_param(to: new_name.lean) if custom_name?(new_name)
43
+ s.ruby_param(comment: comment) if comment.present?
44
+ end
45
+
46
+ private
47
+
48
+ # *************************************************************************
49
+ # Helpers for operation definitions
50
+ # *************************************************************************
51
+
52
+ def generated_name
53
+ return @generated_name if instance_variable_defined?(:@generated_name)
54
+
55
+ @generated_name = begin
56
+ return if table.blank? || expression.blank?
57
+
58
+ PGTrunk::QualifiedName.new(
59
+ nil,
60
+ PGTrunk.database.check_constraint_name(table.lean, expression),
61
+ )
62
+ end
63
+ end
64
+
65
+ def custom_name?(qname = name)
66
+ qname&.differs_from?(/^chk_rails_\w+$/)
67
+ end
68
+ end
69
+ end