torque-postgresql 4.0.1 → 4.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 +4 -4
- data/Rakefile +24 -0
- data/lib/torque/postgresql/adapter/database_statements.rb +89 -7
- data/lib/torque/postgresql/adapter/inheritance_statements.rb +297 -0
- data/lib/torque/postgresql/adapter/oid/array.rb +17 -0
- data/lib/torque/postgresql/adapter/oid/box.rb +1 -1
- data/lib/torque/postgresql/adapter/oid/circle.rb +1 -1
- data/lib/torque/postgresql/adapter/oid/composite.rb +116 -0
- data/lib/torque/postgresql/adapter/oid/line.rb +1 -1
- data/lib/torque/postgresql/adapter/oid/lquery.rb +53 -0
- data/lib/torque/postgresql/adapter/oid/ltree.rb +53 -0
- data/lib/torque/postgresql/adapter/oid/segment.rb +1 -1
- data/lib/torque/postgresql/adapter/oid/struct.rb +150 -0
- data/lib/torque/postgresql/adapter/oid/struct_list.rb +52 -0
- data/lib/torque/postgresql/adapter/oid/struct_set.rb +38 -0
- data/lib/torque/postgresql/adapter/quoting.rb +11 -2
- data/lib/torque/postgresql/adapter/schema_definitions.rb +46 -0
- data/lib/torque/postgresql/adapter/schema_dumper.rb +26 -0
- data/lib/torque/postgresql/adapter/schema_statements.rb +207 -0
- data/lib/torque/postgresql/adapter.rb +2 -5
- data/lib/torque/postgresql/arel/nodes.rb +63 -1
- data/lib/torque/postgresql/arel/visitors.rb +19 -8
- data/lib/torque/postgresql/associations/join_dependency.rb +55 -0
- data/lib/torque/postgresql/associations.rb +3 -0
- data/lib/torque/postgresql/attributes/base.rb +94 -0
- data/lib/torque/postgresql/attributes/composite.rb +102 -0
- data/lib/torque/postgresql/attributes/enum.rb +13 -2
- data/lib/torque/postgresql/attributes/lquery.rb +180 -0
- data/lib/torque/postgresql/attributes/ltree.rb +169 -0
- data/lib/torque/postgresql/attributes/simple_enum.rb +72 -0
- data/lib/torque/postgresql/attributes/struct.rb +137 -0
- data/lib/torque/postgresql/base.rb +17 -14
- data/lib/torque/postgresql/config.rb +81 -14
- data/lib/torque/postgresql/inheritance/expander.rb +66 -0
- data/lib/torque/postgresql/inheritance/record.rb +52 -0
- data/lib/torque/postgresql/inheritance.rb +138 -65
- data/lib/torque/postgresql/migration/command_recorder.rb +76 -0
- data/lib/torque/postgresql/predicate_builder/composite_handler.rb +109 -0
- data/lib/torque/postgresql/predicate_builder/ltree_handler.rb +44 -0
- data/lib/torque/postgresql/predicate_builder/struct_handler.rb +68 -0
- data/lib/torque/postgresql/predicate_builder.rb +26 -0
- data/lib/torque/postgresql/predicate_table.rb +61 -0
- data/lib/torque/postgresql/railtie.rb +35 -0
- data/lib/torque/postgresql/relation/inheritance.rb +146 -28
- data/lib/torque/postgresql/relation/merger.rb +21 -5
- data/lib/torque/postgresql/relation.rb +12 -11
- data/lib/torque/postgresql/schema_cache.rb +1 -0
- data/lib/torque/postgresql/validations.rb +29 -0
- data/lib/torque/postgresql/version.rb +1 -1
- data/lib/torque/postgresql/versioned_commands/command_migration.rb +1 -1
- data/lib/torque/postgresql.rb +6 -0
- data/spec/initialize.rb +20 -2
- data/spec/mocks/cache_query.rb +12 -0
- data/spec/models/author.rb +1 -1
- data/spec/models/comment.rb +2 -0
- data/spec/models/place.rb +2 -0
- data/spec/models/profile.rb +31 -0
- data/spec/schema.rb +33 -4
- data/spec/tests/arel_spec.rb +5 -3
- data/spec/tests/composite_spec.rb +800 -0
- data/spec/tests/ltree_spec.rb +552 -0
- data/spec/tests/struct_spec.rb +968 -0
- data/spec/tests/table_inheritance_spec.rb +1027 -56
- metadata +47 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 386d035c1a009d6c67f130c50baa9e0db1c417aa79b6190834780855ca92d2ee
|
|
4
|
+
data.tar.gz: 468a10d17d4ac97b4e83914be5a539e1b2ca78cbb54ae35e9c80fbee41fda18f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 503443151d99b82e2e76f1d6ed34b6a314e0b8cdd9cd576933879a7a807d33cecd926c48d3809a01fd785d8777d5ae0135c46fb0d9b4c4c347b4457e807cb6ba
|
|
7
|
+
data.tar.gz: a7bce5728938390a5bb285185c592dfcaf44f5322fb30b46fbc08ba5edeaf7a30d932177f91a685afa35153ff2ed8c5200edeb88058d571f2402f8c9a204b32e
|
data/Rakefile
CHANGED
|
@@ -4,8 +4,32 @@ rescue LoadError
|
|
|
4
4
|
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
|
5
5
|
end
|
|
6
6
|
|
|
7
|
+
require 'bundler/gem_tasks'
|
|
7
8
|
require 'rdoc/task'
|
|
8
9
|
|
|
10
|
+
DOCS_PATH = File.expand_path('docs', __dir__)
|
|
11
|
+
|
|
12
|
+
desc 'Compile the documentation site into docs/_site'
|
|
13
|
+
task :docs do
|
|
14
|
+
version = File.read(File.expand_path('lib/torque/postgresql/version.rb', __dir__))[/VERSION = '([^']+)'/, 1]
|
|
15
|
+
published = File.read(File.join(DOCS_PATH, '_config.yml'))[/^gem_version:\s*(\S+)/, 1]
|
|
16
|
+
|
|
17
|
+
unless published == version
|
|
18
|
+
abort <<~MESSAGE
|
|
19
|
+
docs/_config.yml says gem_version: #{published}, but the gem is #{version}.
|
|
20
|
+
The site prints that number in its header, so update _config.yml before building.
|
|
21
|
+
MESSAGE
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
env = { 'BUNDLE_GEMFILE' => File.join(DOCS_PATH, 'Gemfile') }
|
|
25
|
+
Dir.chdir(DOCS_PATH) { sh(env, 'bundle', 'exec', 'jekyll', 'build') }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# A gem cut from master carries freshly compiled docs; every other branch skips
|
|
29
|
+
# the cost. Publishing docs/_site stays a manual step.
|
|
30
|
+
branch = `git rev-parse --abbrev-ref HEAD 2>/dev/null`.strip
|
|
31
|
+
Rake::Task[:build].enhance([:docs]) if branch == 'master'
|
|
32
|
+
|
|
9
33
|
RDoc::Task.new(:rdoc) do |rdoc|
|
|
10
34
|
rdoc.rdoc_dir = 'rdoc'
|
|
11
35
|
rdoc.title = 'Torque::Postgresql'
|
|
@@ -5,7 +5,7 @@ module Torque
|
|
|
5
5
|
module Adapter
|
|
6
6
|
module DatabaseStatements
|
|
7
7
|
|
|
8
|
-
EXTENDED_DATABASE_TYPES = %i[enum enum_set interval]
|
|
8
|
+
EXTENDED_DATABASE_TYPES = %i[enum enum_set interval composite ltree lquery]
|
|
9
9
|
|
|
10
10
|
# Switch between dump mode or not
|
|
11
11
|
def dump_mode!
|
|
@@ -73,6 +73,11 @@ module Torque
|
|
|
73
73
|
if PostgreSQL.config.interval.enabled
|
|
74
74
|
m.register_type 'interval', OID::Interval.new
|
|
75
75
|
end
|
|
76
|
+
|
|
77
|
+
if PostgreSQL.config.ltree.enabled
|
|
78
|
+
m.register_type 'ltree', OID::Ltree.new
|
|
79
|
+
m.register_type 'lquery', OID::Lquery.new
|
|
80
|
+
end
|
|
76
81
|
end
|
|
77
82
|
|
|
78
83
|
# :nodoc:
|
|
@@ -92,9 +97,10 @@ module Torque
|
|
|
92
97
|
|
|
93
98
|
query = <<~SQL
|
|
94
99
|
SELECT t.oid, t.typname, t.typelem, t.typdelim, t.typinput,
|
|
95
|
-
r.rngsubtype, t.typtype, t.typbasetype, t.typarray
|
|
100
|
+
r.rngsubtype, t.typtype, t.typbasetype, t.typarray, c.relkind
|
|
96
101
|
FROM pg_type as t
|
|
97
|
-
LEFT JOIN pg_range as r ON oid = rngtypid
|
|
102
|
+
LEFT JOIN pg_range as r ON t.oid = rngtypid
|
|
103
|
+
LEFT JOIN pg_class as c ON c.oid = t.typrelid
|
|
98
104
|
LEFT JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace
|
|
99
105
|
WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')
|
|
100
106
|
SQL
|
|
@@ -102,19 +108,55 @@ module Torque
|
|
|
102
108
|
if oids
|
|
103
109
|
query += " AND t.oid IN (%s)" % oids.join(", ")
|
|
104
110
|
else
|
|
105
|
-
|
|
111
|
+
conditions = []
|
|
112
|
+
conditions << "t.typtype IN ('e')" if PostgreSQL.config.enum.enabled
|
|
113
|
+
conditions << "(t.typtype = 'c' AND c.relkind = 'c')" \
|
|
114
|
+
if PostgreSQL.config.composite.enabled
|
|
115
|
+
|
|
116
|
+
query += " AND (#{conditions.join(' OR ')})"
|
|
106
117
|
end
|
|
107
118
|
|
|
108
119
|
options = { allow_retry: true, materialize_transactions: false }
|
|
109
120
|
internal_execute(query, 'SCHEMA', **options).each do |row|
|
|
110
|
-
if row['typtype'] == 'e'
|
|
111
|
-
OID::Enum.create(row, type_map)
|
|
121
|
+
if row['typtype'] == 'e'
|
|
122
|
+
OID::Enum.create(row, type_map) if PostgreSQL.config.enum.enabled
|
|
123
|
+
elsif row['typtype'] == 'c' && row['relkind'] == 'c'
|
|
124
|
+
OID::Composite.create(row, type_map) if PostgreSQL.config.composite.enabled
|
|
112
125
|
end
|
|
113
126
|
end
|
|
114
127
|
end
|
|
115
128
|
|
|
116
129
|
def torque_load_additional_types?
|
|
117
|
-
PostgreSQL.config.enum.enabled
|
|
130
|
+
PostgreSQL.config.enum.enabled || PostgreSQL.config.composite.enabled
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Get the ordered list of columns of a composite type, as a hash of
|
|
134
|
+
# column name and its proper cast type
|
|
135
|
+
def composite_column_types(name)
|
|
136
|
+
rows = query(<<~SQL, 'SCHEMA')
|
|
137
|
+
SELECT a.attname, a.atttypid, a.atttypmod
|
|
138
|
+
FROM pg_attribute a
|
|
139
|
+
WHERE a.attrelid = #{quote(quote_type_name(name).to_s)}::regclass
|
|
140
|
+
AND a.attnum > 0 AND NOT a.attisdropped
|
|
141
|
+
ORDER BY a.attnum
|
|
142
|
+
SQL
|
|
143
|
+
|
|
144
|
+
rows.each_with_object({}) do |(attr_name, oid, fmod), hash|
|
|
145
|
+
hash[attr_name] = get_oid_type(oid.to_i, fmod.to_i, attr_name)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Get the list of user-defined composite types, sorted so that types
|
|
150
|
+
# that depend on other composite types come later
|
|
151
|
+
def composite_types
|
|
152
|
+
dependencies = composite_type_dependencies
|
|
153
|
+
result = []
|
|
154
|
+
|
|
155
|
+
dependencies.each_key do |name|
|
|
156
|
+
sort_composite_type(name, dependencies, result)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
result
|
|
118
160
|
end
|
|
119
161
|
|
|
120
162
|
# Gets a list of user defined types.
|
|
@@ -229,6 +271,46 @@ module Torque
|
|
|
229
271
|
conditions
|
|
230
272
|
end
|
|
231
273
|
|
|
274
|
+
private
|
|
275
|
+
|
|
276
|
+
# Every user-defined composite type associated with the list of other
|
|
277
|
+
# composite types that its columns rely on
|
|
278
|
+
def composite_type_dependencies
|
|
279
|
+
rows = select_rows(<<~SQL, 'SCHEMA')
|
|
280
|
+
SELECT t.typname, t.oid::int, COALESCE(elem.oid, mt.oid)::int AS column_oid
|
|
281
|
+
FROM pg_type t
|
|
282
|
+
JOIN pg_class c ON c.oid = t.typrelid AND c.relkind = 'c'
|
|
283
|
+
JOIN pg_namespace n ON n.oid = t.typnamespace
|
|
284
|
+
LEFT JOIN pg_attribute a ON a.attrelid = c.oid AND a.attnum > 0
|
|
285
|
+
AND NOT a.attisdropped
|
|
286
|
+
LEFT JOIN pg_type mt ON mt.oid = a.atttypid
|
|
287
|
+
LEFT JOIN pg_type elem ON elem.oid = mt.typelem AND mt.typelem <> 0
|
|
288
|
+
WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')
|
|
289
|
+
ORDER BY t.oid
|
|
290
|
+
SQL
|
|
291
|
+
|
|
292
|
+
names = rows.to_h { |name, oid, _| [oid, name] }
|
|
293
|
+
result = names.values.uniq.to_h { |name| [name, []] }
|
|
294
|
+
|
|
295
|
+
rows.each do |name, _oid, column_oid|
|
|
296
|
+
dependency = names[column_oid]
|
|
297
|
+
result[name] << dependency unless dependency.nil? || dependency == name
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
result
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# Add the type to the list, but only after every type it depends on
|
|
304
|
+
def sort_composite_type(name, dependencies, result)
|
|
305
|
+
return if result.include?(name)
|
|
306
|
+
|
|
307
|
+
dependencies[name].each do |dependency|
|
|
308
|
+
sort_composite_type(dependency, dependencies, result)
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
result << name
|
|
312
|
+
end
|
|
313
|
+
|
|
232
314
|
end
|
|
233
315
|
end
|
|
234
316
|
end
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Torque
|
|
4
|
+
module PostgreSQL
|
|
5
|
+
module Adapter
|
|
6
|
+
module InheritanceStatements
|
|
7
|
+
|
|
8
|
+
# Copies created by a sync are named after this, which is how they are
|
|
9
|
+
# told apart from anything written by hand
|
|
10
|
+
SYNC_MARKER = 'sync_inh_'
|
|
11
|
+
|
|
12
|
+
SYNC_FEATURES = %i[
|
|
13
|
+
primary_key indexes unique_constraints exclusion_constraints foreign_keys
|
|
14
|
+
].freeze
|
|
15
|
+
|
|
16
|
+
# PostgreSQL only propagates columns, their defaults and their check
|
|
17
|
+
# constraints to inherited tables. This copies over everything else that
|
|
18
|
+
# Rails has a DSL for, from +table_name+ down to its children
|
|
19
|
+
#
|
|
20
|
+
# Features default to +true+, so passing one as +false+ excludes it.
|
|
21
|
+
# Passing any of them as +true+ instead selects only those
|
|
22
|
+
#
|
|
23
|
+
# Example:
|
|
24
|
+
# sync_inheritance_features :activities
|
|
25
|
+
# sync_inheritance_features :activities, %i[activity_books]
|
|
26
|
+
# sync_inheritance_features :activities, primary_key: false
|
|
27
|
+
# sync_inheritance_features :activities, prune: true
|
|
28
|
+
def sync_inheritance_features(table_name, children = nil, prune: false, **features)
|
|
29
|
+
features = sync_inheritance_selection(features)
|
|
30
|
+
|
|
31
|
+
sync_inheritance_tree(table_name, children).each do |child, parents|
|
|
32
|
+
sync_inheritance_into(child, parents, features, prune)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
# Resolves which tables to write to, and where each of them pulls from,
|
|
39
|
+
# with ancestors always coming before their own descendants
|
|
40
|
+
def sync_inheritance_tree(table_name, children)
|
|
41
|
+
table_name = table_name.to_s
|
|
42
|
+
dependencies = inherited_tables
|
|
43
|
+
descendants = sync_inheritance_descendants(dependencies, table_name)
|
|
44
|
+
|
|
45
|
+
targets = descendants
|
|
46
|
+
unless children.nil?
|
|
47
|
+
children = Array.wrap(children).map(&:to_s)
|
|
48
|
+
unknown = children - descendants
|
|
49
|
+
raise ArgumentError, <<~MSG.squish if unknown.any?
|
|
50
|
+
#{unknown.to_sentence} #{unknown.one? ? 'does' : 'do'} not inherit
|
|
51
|
+
from #{table_name}
|
|
52
|
+
MSG
|
|
53
|
+
|
|
54
|
+
targets = descendants & children
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
scope = [table_name] + descendants
|
|
58
|
+
targets.map { |child| [child, dependencies[child] & scope] }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Walks the inheritance graph breadth-first, so that every table shows
|
|
62
|
+
# up only after all of its own ancestors
|
|
63
|
+
def sync_inheritance_descendants(dependencies, table_name)
|
|
64
|
+
children = {}
|
|
65
|
+
dependencies.each do |child, parents|
|
|
66
|
+
parents.each { |parent| (children[parent] ||= []) << child }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
result = []
|
|
70
|
+
queue = (children[table_name] || []).dup
|
|
71
|
+
|
|
72
|
+
while (item = queue.shift)
|
|
73
|
+
next if result.include?(item)
|
|
74
|
+
|
|
75
|
+
result << item
|
|
76
|
+
queue.concat(children[item] || [])
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
result
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Turns what was asked for into a decision for every single feature
|
|
83
|
+
def sync_inheritance_selection(features)
|
|
84
|
+
features = {} if features == true
|
|
85
|
+
unknown = features.keys - SYNC_FEATURES
|
|
86
|
+
raise ArgumentError, <<~MSG.squish if unknown.any?
|
|
87
|
+
Unknown inheritance #{'feature'.pluralize(unknown.size)} #{unknown.to_sentence}
|
|
88
|
+
MSG
|
|
89
|
+
|
|
90
|
+
return SYNC_FEATURES.index_with(true) if features.blank?
|
|
91
|
+
return SYNC_FEATURES.index_with { |name| features.fetch(name, false) } \
|
|
92
|
+
if features.value?(true)
|
|
93
|
+
|
|
94
|
+
SYNC_FEATURES.index_with { |name| features.fetch(name, true) }
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def sync_inheritance_into(child, parents, features, prune)
|
|
98
|
+
return if parents.blank?
|
|
99
|
+
|
|
100
|
+
sync_inheritance_primary_key(child, parents) if features[:primary_key]
|
|
101
|
+
sync_inheritance_unique_constraints(child, parents, prune) \
|
|
102
|
+
if features[:unique_constraints]
|
|
103
|
+
sync_inheritance_exclusion_constraints(child, parents, prune) \
|
|
104
|
+
if features[:exclusion_constraints]
|
|
105
|
+
sync_inheritance_indexes(child, parents, features, prune) if features[:indexes]
|
|
106
|
+
sync_inheritance_foreign_keys(child, parents, prune) if features[:foreign_keys]
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# The only copy that cannot carry the marker, since PostgreSQL names it
|
|
110
|
+
# after the table and Rails has no way to dump that name back. That is
|
|
111
|
+
# why it is also the only one that is never pruned
|
|
112
|
+
def sync_inheritance_primary_key(child, parents)
|
|
113
|
+
return if primary_keys(child).present?
|
|
114
|
+
|
|
115
|
+
parent = parents.find { |item| primary_keys(item).present? }
|
|
116
|
+
return if parent.nil?
|
|
117
|
+
|
|
118
|
+
columns = primary_keys(parent).map { |column| quote_column_name(column) }
|
|
119
|
+
execute(<<~SQL.squish)
|
|
120
|
+
ALTER TABLE #{quote_table_name(child)} ADD PRIMARY KEY (#{columns.join(', ')})
|
|
121
|
+
SQL
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def sync_inheritance_indexes(child, parents, features, prune)
|
|
125
|
+
existing = indexes(child)
|
|
126
|
+
expected = {}
|
|
127
|
+
|
|
128
|
+
parents.each do |parent|
|
|
129
|
+
skip = sync_inheritance_constraint_indexes(parent, features)
|
|
130
|
+
indexes(parent).each do |index|
|
|
131
|
+
next if skip.include?(index.name)
|
|
132
|
+
|
|
133
|
+
expected[sync_inheritance_name(child, parent, index.name)] ||= index
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
expected.each do |name, index|
|
|
138
|
+
next if existing.any? { |item| item.name == name || sync_inheritance_index?(item, index) }
|
|
139
|
+
|
|
140
|
+
add_index(child, index.columns, name: name, **sync_inheritance_index_options(index))
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
return unless prune
|
|
144
|
+
|
|
145
|
+
# An index that backs a constraint can only leave with it
|
|
146
|
+
guarded = sync_inheritance_constraint_indexes(child)
|
|
147
|
+
sync_inheritance_stale(existing, expected).each do |name|
|
|
148
|
+
remove_index(child, name: name) unless guarded.include?(name)
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def sync_inheritance_unique_constraints(child, parents, prune)
|
|
153
|
+
existing = unique_constraints(child)
|
|
154
|
+
expected = {}
|
|
155
|
+
|
|
156
|
+
parents.each do |parent|
|
|
157
|
+
unique_constraints(parent).each do |item|
|
|
158
|
+
expected[sync_inheritance_name(child, parent, item.name)] ||= item
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
expected.each do |name, item|
|
|
163
|
+
next if existing.any? { |other| other.name == name || other.column == item.column }
|
|
164
|
+
|
|
165
|
+
add_unique_constraint(child, item.column, name: name,
|
|
166
|
+
nulls_not_distinct: item.nulls_not_distinct, deferrable: item.deferrable)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
return unless prune
|
|
170
|
+
sync_inheritance_stale(existing, expected).each do |name|
|
|
171
|
+
remove_constraint(child, name)
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def sync_inheritance_exclusion_constraints(child, parents, prune)
|
|
176
|
+
existing = exclusion_constraints(child)
|
|
177
|
+
expected = {}
|
|
178
|
+
|
|
179
|
+
parents.each do |parent|
|
|
180
|
+
exclusion_constraints(parent).each do |item|
|
|
181
|
+
expected[sync_inheritance_name(child, parent, item.name)] ||= item
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
expected.each do |name, item|
|
|
186
|
+
next if existing.any? { |other| other.name == name || sync_inheritance_exclusion?(other, item) }
|
|
187
|
+
|
|
188
|
+
add_exclusion_constraint(child, item.expression, name: name,
|
|
189
|
+
using: item.using, where: item.where, deferrable: item.deferrable)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
return unless prune
|
|
193
|
+
sync_inheritance_stale(existing, expected).each do |name|
|
|
194
|
+
remove_constraint(child, name)
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def sync_inheritance_foreign_keys(child, parents, prune)
|
|
199
|
+
existing = foreign_keys(child)
|
|
200
|
+
expected = {}
|
|
201
|
+
|
|
202
|
+
parents.each do |parent|
|
|
203
|
+
foreign_keys(parent).each do |item|
|
|
204
|
+
expected[sync_inheritance_name(child, parent, item.name)] ||= item
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
expected.each do |name, item|
|
|
209
|
+
next if existing.any? { |other| other.name == name || sync_inheritance_foreign_key?(other, item) }
|
|
210
|
+
|
|
211
|
+
options = item.options.slice(:column, :primary_key, :on_delete, :on_update, :deferrable)
|
|
212
|
+
add_foreign_key(child, item.to_table, name: name, **options)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
return unless prune
|
|
216
|
+
sync_inheritance_stale(existing, expected).each do |name|
|
|
217
|
+
remove_constraint(child, name)
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Every copy is named after the marker plus a digest of where it came
|
|
222
|
+
# from and where it went, which keeps it identical across databases and
|
|
223
|
+
# always well below the identifier limit
|
|
224
|
+
def sync_inheritance_name(child, parent, source)
|
|
225
|
+
digest = OpenSSL::Digest::SHA256.hexdigest("#{child}/#{parent}/#{source}")
|
|
226
|
+
"#{SYNC_MARKER}#{digest.first(10)}"
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def sync_inheritance_copy?(name)
|
|
230
|
+
name.to_s.start_with?(SYNC_MARKER)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Copies whose source is gone from the parent, which is all that a
|
|
234
|
+
# prune is ever allowed to drop
|
|
235
|
+
def sync_inheritance_stale(existing, expected)
|
|
236
|
+
existing.filter_map do |item|
|
|
237
|
+
item.name if sync_inheritance_copy?(item.name) && !expected.key?(item.name)
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# PostgreSQL names the index that backs a constraint after the
|
|
242
|
+
# constraint itself, and +indexes+ hands those back along with the
|
|
243
|
+
# regular ones. Adding the constraint recreates them, so they are only
|
|
244
|
+
# skipped when that constraint is being copied as well. Without any
|
|
245
|
+
# feature given, every one of them is listed
|
|
246
|
+
def sync_inheritance_constraint_indexes(table_name, features = nil)
|
|
247
|
+
names = []
|
|
248
|
+
names.concat(unique_constraints(table_name).map(&:name)) \
|
|
249
|
+
if features.nil? || features[:unique_constraints]
|
|
250
|
+
names.concat(exclusion_constraints(table_name).map(&:name)) \
|
|
251
|
+
if features.nil? || features[:exclusion_constraints]
|
|
252
|
+
names
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def sync_inheritance_index_options(index)
|
|
256
|
+
{
|
|
257
|
+
unique: index.unique,
|
|
258
|
+
where: index.where,
|
|
259
|
+
using: index.using,
|
|
260
|
+
include: index.include,
|
|
261
|
+
nulls_not_distinct: index.nulls_not_distinct,
|
|
262
|
+
order: index.orders,
|
|
263
|
+
opclass: index.opclasses,
|
|
264
|
+
}.compact_blank
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def sync_inheritance_index?(current, source)
|
|
268
|
+
current.columns == source.columns && current.unique == source.unique &&
|
|
269
|
+
current.where == source.where && current.using == source.using
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def sync_inheritance_exclusion?(current, source)
|
|
273
|
+
current.expression == source.expression && current.using == source.using &&
|
|
274
|
+
current.where == source.where
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def sync_inheritance_foreign_key?(current, source)
|
|
278
|
+
current.to_table == source.to_table &&
|
|
279
|
+
current.options[:column] == source.options[:column] &&
|
|
280
|
+
current.options[:primary_key] == source.options[:primary_key]
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
# Tells whether the primary key of the given table is the one it got
|
|
284
|
+
# from a parent, which is the only case the dumper has to describe
|
|
285
|
+
# through the +sync+ option
|
|
286
|
+
def sync_inheritance_parent_primary_key?(table_name)
|
|
287
|
+
keys = primary_keys(table_name)
|
|
288
|
+
return false if keys.empty?
|
|
289
|
+
|
|
290
|
+
parents = inherited_table_names(table_name)
|
|
291
|
+
parents.any? { |parent| primary_keys(parent) == keys }
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
end
|
|
@@ -8,6 +8,23 @@ module Torque
|
|
|
8
8
|
def force_equality?(value)
|
|
9
9
|
PostgreSQL.config.predicate_builder.handle_array_attributes ? false : super
|
|
10
10
|
end
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
# A label path is an Array of labels, so the regular recursion would
|
|
15
|
+
# take each of its labels as another dimension of the column. Paths
|
|
16
|
+
# are a single value to PostgreSQL, which means that the entries of
|
|
17
|
+
# the array are never a dimension of their own
|
|
18
|
+
def type_cast_array(value, method)
|
|
19
|
+
return super unless path_subtype?
|
|
20
|
+
return subtype.public_send(method, value) unless value.is_a?(::Array)
|
|
21
|
+
|
|
22
|
+
value.map { |item| subtype.public_send(method, item) }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def path_subtype?
|
|
26
|
+
PostgreSQL.config.ltree.enabled && subtype.is_a?(Ltree)
|
|
27
|
+
end
|
|
11
28
|
end
|
|
12
29
|
|
|
13
30
|
::ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array.prepend(Array)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Torque
|
|
4
|
+
module PostgreSQL
|
|
5
|
+
module Adapter
|
|
6
|
+
module OID
|
|
7
|
+
class Composite < ActiveModel::Type::Value
|
|
8
|
+
ARRAY = ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array
|
|
9
|
+
DATA = ARRAY::Data
|
|
10
|
+
ENCODER = PG::TextEncoder::Record.new
|
|
11
|
+
DECODER = PG::TextDecoder::Record.new
|
|
12
|
+
|
|
13
|
+
attr_reader :name
|
|
14
|
+
|
|
15
|
+
# The composite type behind the given type, if there is one, wrapped
|
|
16
|
+
# or not by an array
|
|
17
|
+
def self.from(type)
|
|
18
|
+
type = type.subtype if type.is_a?(ARRAY)
|
|
19
|
+
type if type.is_a?(self)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.create(row, type_map)
|
|
23
|
+
oid_klass = new(row['typname'])
|
|
24
|
+
type_map.register_type(row['oid'].to_i, oid_klass)
|
|
25
|
+
type_map.register_type(row['typarray'].to_i, ARRAY.new(oid_klass, row['typdelim']))
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def initialize(name)
|
|
29
|
+
@name = name.to_s.freeze
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# The class that represents the composite type, loaded in a lazy way.
|
|
33
|
+
# It cannot be memoized here because Rails freezes the types it
|
|
34
|
+
# resolves through the type map, so the lookup owns the cache
|
|
35
|
+
def klass
|
|
36
|
+
Attributes::Composite.lookup(name)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# The ordered list of columns of the composite type, as a hash of
|
|
40
|
+
# name and type
|
|
41
|
+
def columns
|
|
42
|
+
klass.columns
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def type
|
|
46
|
+
:composite
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def cast(value)
|
|
50
|
+
case value
|
|
51
|
+
when klass then value
|
|
52
|
+
when ::Hash then build(value)
|
|
53
|
+
when DATA then deserialize(value)
|
|
54
|
+
when ::String then deserialize(value)
|
|
55
|
+
when ::Array then build(columns.keys.zip(value).to_h)
|
|
56
|
+
else value
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# A record that was already serialized comes back as the encoder and
|
|
61
|
+
# its values, which is how it is kept around for dirty tracking
|
|
62
|
+
def deserialize(value)
|
|
63
|
+
return if value.nil?
|
|
64
|
+
|
|
65
|
+
fields = value.is_a?(DATA) ? value.values : DECODER.decode(value)
|
|
66
|
+
instance = klass.new
|
|
67
|
+
set = attributes_of(instance)
|
|
68
|
+
|
|
69
|
+
columns.each_key.with_index do |attr_name, idx|
|
|
70
|
+
set.write_from_database(attr_name, fields[idx])
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
instance
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# The record is handed over to the adapter as an encoder and its
|
|
77
|
+
# values, the same way arrays are, so that columns are type casted by
|
|
78
|
+
# the connection right before the record literal is built. Values come
|
|
79
|
+
# from the record itself, so that types declared by the class, like
|
|
80
|
+
# encrypted ones, are the ones being applied
|
|
81
|
+
def serialize(value)
|
|
82
|
+
return if value.nil?
|
|
83
|
+
|
|
84
|
+
set = attributes_of(cast(value))
|
|
85
|
+
values = columns.each_key.map { |attr_name| set[attr_name].value_for_database }
|
|
86
|
+
|
|
87
|
+
DATA.new(ENCODER, values)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def changed_in_place?(raw_old_value, new_value)
|
|
91
|
+
deserialize(raw_old_value) != new_value
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def ==(other)
|
|
95
|
+
other.class == self.class && other.name == name
|
|
96
|
+
end
|
|
97
|
+
alias eql? ==
|
|
98
|
+
|
|
99
|
+
def hash
|
|
100
|
+
[self.class, name].hash
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
private
|
|
104
|
+
|
|
105
|
+
def build(attrs)
|
|
106
|
+
klass.new(**attrs.symbolize_keys)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def attributes_of(record)
|
|
110
|
+
record.instance_variable_get(:@attributes)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|