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.
- checksums.yaml +7 -0
- data/.github/workflows/ci.yml +87 -0
- data/.gitignore +9 -0
- data/.rspec +4 -0
- data/.rubocop.yml +92 -0
- data/.yardopts +4 -0
- data/CHANGELOG.md +31 -0
- data/CONTRIBUTING.md +17 -0
- data/Gemfile +22 -0
- data/LICENSE.txt +21 -0
- data/README.md +141 -0
- data/Rakefile +16 -0
- data/bin/console +8 -0
- data/bin/rake +19 -0
- data/bin/rspec +19 -0
- data/bin/setup +8 -0
- data/bin/yard +19 -0
- data/lib/pg_trunk/core/adapters/postgres.rb +80 -0
- data/lib/pg_trunk/core/dependencies_resolver.rb +101 -0
- data/lib/pg_trunk/core/generators.rb +140 -0
- data/lib/pg_trunk/core/operation/attributes.rb +78 -0
- data/lib/pg_trunk/core/operation/callbacks.rb +40 -0
- data/lib/pg_trunk/core/operation/generators.rb +51 -0
- data/lib/pg_trunk/core/operation/inversion.rb +70 -0
- data/lib/pg_trunk/core/operation/registration.rb +55 -0
- data/lib/pg_trunk/core/operation/ruby_builder.rb +112 -0
- data/lib/pg_trunk/core/operation/ruby_helpers.rb +99 -0
- data/lib/pg_trunk/core/operation/sql_helpers.rb +44 -0
- data/lib/pg_trunk/core/operation/validations.rb +21 -0
- data/lib/pg_trunk/core/operation.rb +78 -0
- data/lib/pg_trunk/core/qualified_name.rb +165 -0
- data/lib/pg_trunk/core/railtie/command_recorder.rb +30 -0
- data/lib/pg_trunk/core/railtie/custom_types.rb +37 -0
- data/lib/pg_trunk/core/railtie/migration.rb +50 -0
- data/lib/pg_trunk/core/railtie/migrator.rb +22 -0
- data/lib/pg_trunk/core/railtie/schema_dumper.rb +75 -0
- data/lib/pg_trunk/core/railtie/schema_migration.rb +22 -0
- data/lib/pg_trunk/core/railtie/statements.rb +21 -0
- data/lib/pg_trunk/core/railtie.rb +35 -0
- data/lib/pg_trunk/core/registry.rb +159 -0
- data/lib/pg_trunk/core/serializers/array_of_hashes_serializer.rb +28 -0
- data/lib/pg_trunk/core/serializers/array_of_strings_serializer.rb +29 -0
- data/lib/pg_trunk/core/serializers/array_of_symbols_serializer.rb +28 -0
- data/lib/pg_trunk/core/serializers/array_serializer.rb +22 -0
- data/lib/pg_trunk/core/serializers/lowercase_string_serializer.rb +21 -0
- data/lib/pg_trunk/core/serializers/multiline_text_serializer.rb +21 -0
- data/lib/pg_trunk/core/serializers/qualified_name_serializer.rb +27 -0
- data/lib/pg_trunk/core/serializers/symbol_serializer.rb +22 -0
- data/lib/pg_trunk/core/serializers.rb +16 -0
- data/lib/pg_trunk/core/validators/all_items_valid_validator.rb +15 -0
- data/lib/pg_trunk/core/validators/difference_validator.rb +19 -0
- data/lib/pg_trunk/core/validators.rb +10 -0
- data/lib/pg_trunk/core.rb +21 -0
- data/lib/pg_trunk/generators.rb +7 -0
- data/lib/pg_trunk/operations/check_constraints/add_check_constraint.rb +109 -0
- data/lib/pg_trunk/operations/check_constraints/base.rb +69 -0
- data/lib/pg_trunk/operations/check_constraints/drop_check_constraint.rb +60 -0
- data/lib/pg_trunk/operations/check_constraints/rename_check_constraint.rb +54 -0
- data/lib/pg_trunk/operations/check_constraints/validate_check_constraint.rb +39 -0
- data/lib/pg_trunk/operations/check_constraints.rb +14 -0
- data/lib/pg_trunk/operations/composite_types/base.rb +61 -0
- data/lib/pg_trunk/operations/composite_types/change_composite_type.rb +136 -0
- data/lib/pg_trunk/operations/composite_types/column.rb +118 -0
- data/lib/pg_trunk/operations/composite_types/create_composite_type.rb +99 -0
- data/lib/pg_trunk/operations/composite_types/drop_composite_type.rb +67 -0
- data/lib/pg_trunk/operations/composite_types/rename_composite_type.rb +44 -0
- data/lib/pg_trunk/operations/composite_types.rb +15 -0
- data/lib/pg_trunk/operations/domains/base.rb +46 -0
- data/lib/pg_trunk/operations/domains/change_domain.rb +140 -0
- data/lib/pg_trunk/operations/domains/constraint.rb +93 -0
- data/lib/pg_trunk/operations/domains/create_domain.rb +124 -0
- data/lib/pg_trunk/operations/domains/drop_domain.rb +65 -0
- data/lib/pg_trunk/operations/domains/rename_domain.rb +44 -0
- data/lib/pg_trunk/operations/domains.rb +15 -0
- data/lib/pg_trunk/operations/enums/base.rb +47 -0
- data/lib/pg_trunk/operations/enums/change.rb +55 -0
- data/lib/pg_trunk/operations/enums/change_enum.rb +119 -0
- data/lib/pg_trunk/operations/enums/create_enum.rb +83 -0
- data/lib/pg_trunk/operations/enums/drop_enum.rb +63 -0
- data/lib/pg_trunk/operations/enums/rename_enum.rb +44 -0
- data/lib/pg_trunk/operations/enums.rb +15 -0
- data/lib/pg_trunk/operations/foreign_keys/add_foreign_key.rb +174 -0
- data/lib/pg_trunk/operations/foreign_keys/base.rb +155 -0
- data/lib/pg_trunk/operations/foreign_keys/drop_foreign_key.rb +76 -0
- data/lib/pg_trunk/operations/foreign_keys/rename_foreign_key.rb +63 -0
- data/lib/pg_trunk/operations/foreign_keys.rb +16 -0
- data/lib/pg_trunk/operations/functions/base.rb +54 -0
- data/lib/pg_trunk/operations/functions/change_function.rb +108 -0
- data/lib/pg_trunk/operations/functions/create_function.rb +198 -0
- data/lib/pg_trunk/operations/functions/drop_function.rb +88 -0
- data/lib/pg_trunk/operations/functions/rename_function.rb +57 -0
- data/lib/pg_trunk/operations/functions.rb +14 -0
- data/lib/pg_trunk/operations/indexes/add_index.rb +68 -0
- data/lib/pg_trunk/operations/indexes.rb +10 -0
- data/lib/pg_trunk/operations/materialized_views/base.rb +79 -0
- data/lib/pg_trunk/operations/materialized_views/change_materialized_view.rb +139 -0
- data/lib/pg_trunk/operations/materialized_views/column.rb +94 -0
- data/lib/pg_trunk/operations/materialized_views/create_materialized_view.rb +170 -0
- data/lib/pg_trunk/operations/materialized_views/drop_materialized_view.rb +70 -0
- data/lib/pg_trunk/operations/materialized_views/refresh_materialized_view.rb +48 -0
- data/lib/pg_trunk/operations/materialized_views/rename_materialized_view.rb +61 -0
- data/lib/pg_trunk/operations/materialized_views.rb +17 -0
- data/lib/pg_trunk/operations/procedures/base.rb +42 -0
- data/lib/pg_trunk/operations/procedures/change_procedure.rb +107 -0
- data/lib/pg_trunk/operations/procedures/create_procedure.rb +146 -0
- data/lib/pg_trunk/operations/procedures/drop_procedure.rb +66 -0
- data/lib/pg_trunk/operations/procedures/rename_procedure.rb +57 -0
- data/lib/pg_trunk/operations/procedures.rb +14 -0
- data/lib/pg_trunk/operations/statistics/base.rb +94 -0
- data/lib/pg_trunk/operations/statistics/create_statistics.rb +181 -0
- data/lib/pg_trunk/operations/statistics/drop_statistics.rb +75 -0
- data/lib/pg_trunk/operations/statistics/rename_statistics.rb +48 -0
- data/lib/pg_trunk/operations/statistics.rb +13 -0
- data/lib/pg_trunk/operations/tables/create_table.rb +75 -0
- data/lib/pg_trunk/operations/tables.rb +10 -0
- data/lib/pg_trunk/operations/triggers/base.rb +119 -0
- data/lib/pg_trunk/operations/triggers/change_trigger.rb +82 -0
- data/lib/pg_trunk/operations/triggers/create_trigger.rb +208 -0
- data/lib/pg_trunk/operations/triggers/drop_trigger.rb +66 -0
- data/lib/pg_trunk/operations/triggers/rename_trigger.rb +71 -0
- data/lib/pg_trunk/operations/triggers.rb +14 -0
- data/lib/pg_trunk/operations/views/base.rb +38 -0
- data/lib/pg_trunk/operations/views/change_view.rb +90 -0
- data/lib/pg_trunk/operations/views/create_view.rb +115 -0
- data/lib/pg_trunk/operations/views/drop_view.rb +69 -0
- data/lib/pg_trunk/operations/views/rename_view.rb +58 -0
- data/lib/pg_trunk/operations/views.rb +14 -0
- data/lib/pg_trunk/operations.rb +23 -0
- data/lib/pg_trunk/version.rb +6 -0
- data/lib/pg_trunk.rb +27 -0
- data/pg_trunk.gemspec +34 -0
- data/spec/dummy/.gitignore +16 -0
- data/spec/dummy/Rakefile +15 -0
- data/spec/dummy/bin/bundle +6 -0
- data/spec/dummy/bin/rails +6 -0
- data/spec/dummy/bin/rake +6 -0
- data/spec/dummy/config/application.rb +18 -0
- data/spec/dummy/config/boot.rb +7 -0
- data/spec/dummy/config/database.yml +14 -0
- data/spec/dummy/config/environment.rb +7 -0
- data/spec/dummy/config.ru +6 -0
- data/spec/dummy/db/materialized_views/admin_users_v01.sql +1 -0
- data/spec/dummy/db/migrate/.keep +0 -0
- data/spec/dummy/db/schema.rb +18 -0
- data/spec/dummy/db/views/admin_users_v01.sql +1 -0
- data/spec/dummy/db/views/admin_users_v02.sql +1 -0
- data/spec/operations/check_constraints/add_check_constraint_spec.rb +85 -0
- data/spec/operations/check_constraints/drop_check_constraint_spec.rb +111 -0
- data/spec/operations/check_constraints/rename_check_constraint_spec.rb +90 -0
- data/spec/operations/composite_types/change_composite_type_spec.rb +257 -0
- data/spec/operations/composite_types/create_composite_type_spec.rb +55 -0
- data/spec/operations/composite_types/drop_composite_type_spec.rb +109 -0
- data/spec/operations/composite_types/rename_composite_type_spec.rb +74 -0
- data/spec/operations/dependency_resolver_spec.rb +177 -0
- data/spec/operations/domains/change_domain_spec.rb +287 -0
- data/spec/operations/domains/create_domain_spec.rb +69 -0
- data/spec/operations/domains/drop_domain_spec.rb +119 -0
- data/spec/operations/domains/rename_domain_spec.rb +70 -0
- data/spec/operations/enums/change_enum_spec.rb +157 -0
- data/spec/operations/enums/create_enum_spec.rb +40 -0
- data/spec/operations/enums/drop_enum_spec.rb +120 -0
- data/spec/operations/enums/rename_enum_spec.rb +72 -0
- data/spec/operations/foreign_keys/add_foreign_key_spec.rb +208 -0
- data/spec/operations/foreign_keys/drop_foreign_key_spec.rb +167 -0
- data/spec/operations/foreign_keys/rename_foreign_key_spec.rb +101 -0
- data/spec/operations/functions/change_function_spec.rb +166 -0
- data/spec/operations/functions/create_function_spec.rb +192 -0
- data/spec/operations/functions/drop_function_spec.rb +182 -0
- data/spec/operations/functions/rename_function_spec.rb +101 -0
- data/spec/operations/indexes/add_index_spec.rb +94 -0
- data/spec/operations/materialized_views/change_materialized_view_spec.rb +190 -0
- data/spec/operations/materialized_views/create_materialized_view_spec.rb +144 -0
- data/spec/operations/materialized_views/drop_materialized_view_spec.rb +145 -0
- data/spec/operations/materialized_views/refresh_materialized_view_spec.rb +79 -0
- data/spec/operations/materialized_views/rename_materialized_view_spec.rb +88 -0
- data/spec/operations/procedures/change_procedure_spec.rb +175 -0
- data/spec/operations/procedures/create_procedure_spec.rb +151 -0
- data/spec/operations/procedures/drop_procedure_spec.rb +159 -0
- data/spec/operations/procedures/rename_procedure_spec.rb +107 -0
- data/spec/operations/statistics/create_statistics_spec.rb +230 -0
- data/spec/operations/statistics/drop_statistics_spec.rb +106 -0
- data/spec/operations/statistics/rename_statistics_spec.rb +129 -0
- data/spec/operations/tables/create_table_spec.rb +53 -0
- data/spec/operations/tables/rename_table_spec.rb +37 -0
- data/spec/operations/triggers/change_trigger_spec.rb +195 -0
- data/spec/operations/triggers/create_trigger_spec.rb +104 -0
- data/spec/operations/triggers/drop_trigger_spec.rb +124 -0
- data/spec/operations/triggers/rename_trigger_spec.rb +160 -0
- data/spec/operations/views/change_view_spec.rb +144 -0
- data/spec/operations/views/create_view_spec.rb +134 -0
- data/spec/operations/views/drop_view_spec.rb +146 -0
- data/spec/operations/views/rename_view_spec.rb +85 -0
- data/spec/pg_trunk/dependencies_resolver_spec.rb +43 -0
- data/spec/spec_helper.rb +28 -0
- data/spec/support/migrations_helper.rb +376 -0
- 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,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
|