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