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.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +24 -0
  3. data/lib/torque/postgresql/adapter/database_statements.rb +89 -7
  4. data/lib/torque/postgresql/adapter/inheritance_statements.rb +297 -0
  5. data/lib/torque/postgresql/adapter/oid/array.rb +17 -0
  6. data/lib/torque/postgresql/adapter/oid/box.rb +1 -1
  7. data/lib/torque/postgresql/adapter/oid/circle.rb +1 -1
  8. data/lib/torque/postgresql/adapter/oid/composite.rb +116 -0
  9. data/lib/torque/postgresql/adapter/oid/line.rb +1 -1
  10. data/lib/torque/postgresql/adapter/oid/lquery.rb +53 -0
  11. data/lib/torque/postgresql/adapter/oid/ltree.rb +53 -0
  12. data/lib/torque/postgresql/adapter/oid/segment.rb +1 -1
  13. data/lib/torque/postgresql/adapter/oid/struct.rb +150 -0
  14. data/lib/torque/postgresql/adapter/oid/struct_list.rb +52 -0
  15. data/lib/torque/postgresql/adapter/oid/struct_set.rb +38 -0
  16. data/lib/torque/postgresql/adapter/quoting.rb +11 -2
  17. data/lib/torque/postgresql/adapter/schema_definitions.rb +46 -0
  18. data/lib/torque/postgresql/adapter/schema_dumper.rb +26 -0
  19. data/lib/torque/postgresql/adapter/schema_statements.rb +207 -0
  20. data/lib/torque/postgresql/adapter.rb +2 -5
  21. data/lib/torque/postgresql/arel/nodes.rb +63 -1
  22. data/lib/torque/postgresql/arel/visitors.rb +19 -8
  23. data/lib/torque/postgresql/associations/join_dependency.rb +55 -0
  24. data/lib/torque/postgresql/associations.rb +3 -0
  25. data/lib/torque/postgresql/attributes/base.rb +94 -0
  26. data/lib/torque/postgresql/attributes/composite.rb +102 -0
  27. data/lib/torque/postgresql/attributes/enum.rb +13 -2
  28. data/lib/torque/postgresql/attributes/lquery.rb +180 -0
  29. data/lib/torque/postgresql/attributes/ltree.rb +169 -0
  30. data/lib/torque/postgresql/attributes/simple_enum.rb +72 -0
  31. data/lib/torque/postgresql/attributes/struct.rb +137 -0
  32. data/lib/torque/postgresql/base.rb +17 -14
  33. data/lib/torque/postgresql/config.rb +81 -14
  34. data/lib/torque/postgresql/inheritance/expander.rb +66 -0
  35. data/lib/torque/postgresql/inheritance/record.rb +52 -0
  36. data/lib/torque/postgresql/inheritance.rb +138 -65
  37. data/lib/torque/postgresql/migration/command_recorder.rb +76 -0
  38. data/lib/torque/postgresql/predicate_builder/composite_handler.rb +109 -0
  39. data/lib/torque/postgresql/predicate_builder/ltree_handler.rb +44 -0
  40. data/lib/torque/postgresql/predicate_builder/struct_handler.rb +68 -0
  41. data/lib/torque/postgresql/predicate_builder.rb +26 -0
  42. data/lib/torque/postgresql/predicate_table.rb +61 -0
  43. data/lib/torque/postgresql/railtie.rb +35 -0
  44. data/lib/torque/postgresql/relation/inheritance.rb +146 -28
  45. data/lib/torque/postgresql/relation/merger.rb +21 -5
  46. data/lib/torque/postgresql/relation.rb +12 -11
  47. data/lib/torque/postgresql/schema_cache.rb +1 -0
  48. data/lib/torque/postgresql/validations.rb +29 -0
  49. data/lib/torque/postgresql/version.rb +1 -1
  50. data/lib/torque/postgresql/versioned_commands/command_migration.rb +1 -1
  51. data/lib/torque/postgresql.rb +6 -0
  52. data/spec/initialize.rb +20 -2
  53. data/spec/mocks/cache_query.rb +12 -0
  54. data/spec/models/author.rb +1 -1
  55. data/spec/models/comment.rb +2 -0
  56. data/spec/models/place.rb +2 -0
  57. data/spec/models/profile.rb +31 -0
  58. data/spec/schema.rb +33 -4
  59. data/spec/tests/arel_spec.rb +5 -3
  60. data/spec/tests/composite_spec.rb +800 -0
  61. data/spec/tests/ltree_spec.rb +552 -0
  62. data/spec/tests/struct_spec.rb +968 -0
  63. data/spec/tests/table_inheritance_spec.rb +1027 -56
  64. metadata +47 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9e1560c305d73659df33b4dcc71dd57376ab72d898ef1848e54c8aaa5689584e
4
- data.tar.gz: 0fc8842730a9970f999b52c6a10feb728f16b79086b4389f114ccca12e24f5a9
3
+ metadata.gz: 386d035c1a009d6c67f130c50baa9e0db1c417aa79b6190834780855ca92d2ee
4
+ data.tar.gz: 468a10d17d4ac97b4e83914be5a539e1b2ca78cbb54ae35e9c80fbee41fda18f
5
5
  SHA512:
6
- metadata.gz: 9985681094dc57004a8d823f5f13d5383d965304336d1b9e4a886fa812e8b9dfca5af93504a7b46ed58e7e508fd0d3c8048d367b5b8c36f42f9dc293f2f556bf
7
- data.tar.gz: 4b23fe90238cb6f0f7c424631e381c27b15bc932e69d46d68dfba9f96a39081d40b80a0ff61266ab1122c24f10242c26a1728e5e737ef847a49c403e8b3e9c88
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
- query += " AND t.typtype IN ('e')"
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' && PostgreSQL.config.enum.enabled
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)
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Torque
4
4
  module PostgreSQL
5
- class Box < Struct.new(:x1, :y1, :x2, :y2)
5
+ class Box < ::Struct.new(:x1, :y1, :x2, :y2)
6
6
  def points
7
7
  klass = Torque::PostgreSQL.config.geometry.point_class
8
8
  [
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Torque
4
4
  module PostgreSQL
5
- class Circle < Struct.new(:x, :y, :r)
5
+ class Circle < ::Struct.new(:x, :y, :r)
6
6
  alias radius r
7
7
  alias radius= r=
8
8
 
@@ -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
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Torque
4
4
  module PostgreSQL
5
- class Line < Struct.new(:slope, :intercept)
5
+ class Line < ::Struct.new(:slope, :intercept)
6
6
  alias c intercept
7
7
  alias c= intercept=
8
8