torque-postgresql 4.0.0 → 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 +28 -2
- 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/join_series.rb +8 -6
- data/lib/torque/postgresql/relation/merger.rb +23 -7
- 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 +34 -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 +49 -7
|
@@ -25,6 +25,100 @@ module Torque
|
|
|
25
25
|
SQL
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
+
# Creates a new composite type, with the columns being defined using
|
|
29
|
+
# the same DSL as +create_table+, although limited to the type itself,
|
|
30
|
+
# the size-related options, and no indexes or constraints
|
|
31
|
+
#
|
|
32
|
+
# Example:
|
|
33
|
+
# create_composite_type :address do |t|
|
|
34
|
+
# t.string "street"
|
|
35
|
+
# t.integer "number"
|
|
36
|
+
# end
|
|
37
|
+
def create_composite_type(name, options = {})
|
|
38
|
+
td = create_composite_type_definition(name)
|
|
39
|
+
yield td if block_given?
|
|
40
|
+
|
|
41
|
+
validate_composite_type!(name, td)
|
|
42
|
+
drop_type(name, force: options[:force], check: true, schema: options[:schema]) \
|
|
43
|
+
if options[:force]
|
|
44
|
+
|
|
45
|
+
columns = td.columns.map { |column| composite_column_definition(name, column) }
|
|
46
|
+
type_name = sanitize_name_with_schema(name, options.dup)
|
|
47
|
+
|
|
48
|
+
internal_exec_query(<<~SQL.squish, 'SCHEMA').tap { reload_type_map }
|
|
49
|
+
CREATE TYPE #{quote_type_name(type_name)} AS (#{columns.join(', ')})
|
|
50
|
+
SQL
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Changes an existing composite type, using the same DSL as
|
|
54
|
+
# +change_table+, although limited to what a type supports
|
|
55
|
+
#
|
|
56
|
+
# Example:
|
|
57
|
+
# change_composite_type :address do |t|
|
|
58
|
+
# t.string 'zipcode'
|
|
59
|
+
# t.change 'number', :bigint
|
|
60
|
+
# t.remove 'city'
|
|
61
|
+
# t.rename 'street', 'road'
|
|
62
|
+
# end
|
|
63
|
+
def change_composite_type(name, options = {})
|
|
64
|
+
td = create_composite_type_definition(name)
|
|
65
|
+
yield td
|
|
66
|
+
|
|
67
|
+
validate_composite_type!(name, td)
|
|
68
|
+
type_name = quote_type_name(sanitize_name_with_schema(name, options.dup))
|
|
69
|
+
actions = composite_type_actions(name, td)
|
|
70
|
+
|
|
71
|
+
execute("ALTER TYPE #{type_name} #{actions.join(', ')}") if actions.any?
|
|
72
|
+
|
|
73
|
+
td.renames.each do |from, to|
|
|
74
|
+
execute <<~SQL.squish
|
|
75
|
+
ALTER TYPE #{type_name}
|
|
76
|
+
RENAME ATTRIBUTE #{quote_column_name(from)} TO #{quote_column_name(to)}
|
|
77
|
+
SQL
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
reset_composite_type(name)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Adds a single column to an existing composite type
|
|
84
|
+
def add_composite_column(type_name, column_name, type, options = {})
|
|
85
|
+
options = options.dup
|
|
86
|
+
schema = options.delete(:schema)
|
|
87
|
+
|
|
88
|
+
td = create_composite_type_definition(type_name)
|
|
89
|
+
td.column(column_name, type, **options)
|
|
90
|
+
|
|
91
|
+
alter_composite_type(type_name, schema, composite_type_actions(type_name, td))
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Removes a single column of an existing composite type. The +type+ is
|
|
95
|
+
# only needed to make the migration reversible
|
|
96
|
+
def remove_composite_column(type_name, column_name, type = nil, options = {})
|
|
97
|
+
action = "DROP ATTRIBUTE #{quote_column_name(column_name)}"
|
|
98
|
+
alter_composite_type(type_name, options[:schema], [action])
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Changes the type of a single column of an existing composite type
|
|
102
|
+
def change_composite_column(type_name, column_name, type, options = {})
|
|
103
|
+
options = options.dup
|
|
104
|
+
schema = options.delete(:schema)
|
|
105
|
+
|
|
106
|
+
td = create_composite_type_definition(type_name)
|
|
107
|
+
td.change(column_name, type, **options)
|
|
108
|
+
|
|
109
|
+
alter_composite_type(type_name, schema, composite_type_actions(type_name, td))
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Renames a single column of an existing composite type
|
|
113
|
+
def rename_composite_column(type_name, column_name, new_name, options = {})
|
|
114
|
+
action = <<~SQL.squish
|
|
115
|
+
RENAME ATTRIBUTE #{quote_column_name(column_name)}
|
|
116
|
+
TO #{quote_column_name(new_name)}
|
|
117
|
+
SQL
|
|
118
|
+
|
|
119
|
+
alter_composite_type(type_name, options[:schema], [action])
|
|
120
|
+
end
|
|
121
|
+
|
|
28
122
|
# Creates a column that stores the underlying language of the record so
|
|
29
123
|
# that a search vector can be created dynamically based on it. It uses
|
|
30
124
|
# a `regconfig` type, so string conversions are mandatory
|
|
@@ -92,6 +186,28 @@ module Torque
|
|
|
92
186
|
end
|
|
93
187
|
|
|
94
188
|
|
|
189
|
+
# Spread the features of the parent tables into the one being created,
|
|
190
|
+
# since PostgreSQL only propagates columns and their check constraints
|
|
191
|
+
def create_table(table_name, **options, &block)
|
|
192
|
+
sync = options.delete(:sync)
|
|
193
|
+
result = super
|
|
194
|
+
return result if sync.blank?
|
|
195
|
+
|
|
196
|
+
parents = Array.wrap(options[:inherits]).flatten.compact.map(&:to_s)
|
|
197
|
+
return result if parents.empty?
|
|
198
|
+
|
|
199
|
+
sync_inheritance_into(table_name.to_s, parents, sync_inheritance_selection(sync), false)
|
|
200
|
+
result
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# A primary key that came from a parent table is described by the +sync+
|
|
204
|
+
# option while dumping, otherwise the dumper would describe the inherited
|
|
205
|
+
# column all over again and the load would break on the duplicate
|
|
206
|
+
def primary_key(table_name)
|
|
207
|
+
return super unless @_dump_mode
|
|
208
|
+
sync_inheritance_parent_primary_key?(table_name) ? nil : super
|
|
209
|
+
end
|
|
210
|
+
|
|
95
211
|
# Add the schema option when extracting table options
|
|
96
212
|
def table_options(table_name)
|
|
97
213
|
options = super
|
|
@@ -108,6 +224,7 @@ module Torque
|
|
|
108
224
|
|
|
109
225
|
tables = inherited_table_names(table_name)
|
|
110
226
|
options[:inherits] = tables.one? ? tables.first : tables
|
|
227
|
+
options[:sync] = { primary_key: true } if sync_inheritance_parent_primary_key?(table_name)
|
|
111
228
|
end
|
|
112
229
|
|
|
113
230
|
options
|
|
@@ -137,6 +254,25 @@ module Torque
|
|
|
137
254
|
super + [:schema, :inherits]
|
|
138
255
|
end
|
|
139
256
|
|
|
257
|
+
# Add composite_type as one of the valid options for column definition
|
|
258
|
+
def valid_column_definition_options
|
|
259
|
+
super + [:composite_type]
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# Maps the composite type through its required option, mirroring how
|
|
263
|
+
# enums are handled
|
|
264
|
+
def type_to_sql(type, composite_type: nil, **options)
|
|
265
|
+
return super(type, **options) unless type.to_s == 'composite'
|
|
266
|
+
|
|
267
|
+
raise ArgumentError, <<~MSG.squish if composite_type.nil?
|
|
268
|
+
composite_type is required for composite columns
|
|
269
|
+
MSG
|
|
270
|
+
|
|
271
|
+
sql = composite_type.to_s
|
|
272
|
+
sql = "#{sql}[]" if options[:array]
|
|
273
|
+
sql
|
|
274
|
+
end
|
|
275
|
+
|
|
140
276
|
# Add proper support for schema load when using versioned commands
|
|
141
277
|
def assume_migrated_upto_version(version)
|
|
142
278
|
return super unless PostgreSQL.config.versioned_commands.enabled
|
|
@@ -195,6 +331,77 @@ module Torque
|
|
|
195
331
|
Quoting::Name.new(schema.to_s, name.to_s)
|
|
196
332
|
end
|
|
197
333
|
|
|
334
|
+
# A composite type is defined with the same DSL as a table, but only
|
|
335
|
+
# its columns are taken into account
|
|
336
|
+
def create_composite_type_definition(name)
|
|
337
|
+
CompositeTypeDefinition.new(self, name)
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
# Runs a single set of changes over an existing composite type
|
|
341
|
+
def alter_composite_type(name, schema, actions)
|
|
342
|
+
type_name = quote_type_name(sanitize_name_with_schema(name, schema: schema))
|
|
343
|
+
execute("ALTER TYPE #{type_name} #{actions.join(', ')}")
|
|
344
|
+
reset_composite_type(name)
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
# Every addition, change, and removal that a composite type can batch
|
|
348
|
+
# into a single statement. Renames are left out because PostgreSQL
|
|
349
|
+
# does not allow them to be combined with other actions
|
|
350
|
+
def composite_type_actions(name, td)
|
|
351
|
+
adds = td.columns.map do |column|
|
|
352
|
+
"ADD ATTRIBUTE #{composite_column_definition(name, column)}"
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
changes = td.changes.map do |column|
|
|
356
|
+
quoted = quote_column_name(column.name)
|
|
357
|
+
"ALTER ATTRIBUTE #{quoted} TYPE #{composite_column_type(name, column)}"
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
drops = td.removals.map { |name| "DROP ATTRIBUTE #{quote_column_name(name)}" }
|
|
361
|
+
|
|
362
|
+
adds + changes + drops
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
# A composite type only holds columns, so anything else that the DSL
|
|
366
|
+
# can produce is rejected
|
|
367
|
+
def validate_composite_type!(name, td)
|
|
368
|
+
raise ArgumentError, <<~MSG.squish if td.indexes.present?
|
|
369
|
+
Unable to define the composite type "#{name}" because it does
|
|
370
|
+
not support indexes.
|
|
371
|
+
MSG
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
# Build a single column of a composite type
|
|
375
|
+
def composite_column_definition(name, column)
|
|
376
|
+
"#{quote_column_name(column.name)} #{composite_column_type(name, column)}"
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
# Resolve the type of a column, making sure that only supported
|
|
380
|
+
# options were provided
|
|
381
|
+
def composite_column_type(name, column)
|
|
382
|
+
supported = %i[limit precision scale array enum_type composite_type]
|
|
383
|
+
options = column.options.dup
|
|
384
|
+
options.delete(:primary_key) unless options[:primary_key]
|
|
385
|
+
|
|
386
|
+
unsupported = options.keys - supported
|
|
387
|
+
raise ArgumentError, <<~MSG.squish if unsupported.present?
|
|
388
|
+
Unable to define the composite type "#{name}" because the
|
|
389
|
+
column "#{column.name}" contains unsupported options:
|
|
390
|
+
#{unsupported.map(&:inspect).join(', ')}.
|
|
391
|
+
MSG
|
|
392
|
+
|
|
393
|
+
type_to_sql(column.type, **options.slice(*supported))
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
# Members loaded by the class that handles the type are no longer
|
|
397
|
+
# valid once the type itself has changed
|
|
398
|
+
def reset_composite_type(name)
|
|
399
|
+
reload_type_map
|
|
400
|
+
return unless PostgreSQL.config.composite.enabled
|
|
401
|
+
|
|
402
|
+
Attributes::Composite.lookup(name.to_s.split('.').last).reset_columns!
|
|
403
|
+
end
|
|
404
|
+
|
|
198
405
|
def quote_enum_values(name, values, options)
|
|
199
406
|
prefix = options[:prefix]
|
|
200
407
|
prefix = name if prefix === true
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative 'adapter/database_statements'
|
|
4
|
+
require_relative 'adapter/inheritance_statements'
|
|
4
5
|
require_relative 'adapter/oid'
|
|
5
6
|
require_relative 'adapter/quoting'
|
|
6
7
|
require_relative 'adapter/schema_creation'
|
|
@@ -13,6 +14,7 @@ module Torque
|
|
|
13
14
|
module Adapter
|
|
14
15
|
include Quoting
|
|
15
16
|
include DatabaseStatements
|
|
17
|
+
include InheritanceStatements
|
|
16
18
|
include SchemaStatements
|
|
17
19
|
|
|
18
20
|
# :nodoc:
|
|
@@ -31,11 +33,6 @@ module Torque
|
|
|
31
33
|
)
|
|
32
34
|
end
|
|
33
35
|
|
|
34
|
-
# Add `inherits` and `schema` to the list of extracted table options
|
|
35
|
-
def extract_table_options!(options)
|
|
36
|
-
super.merge(options.extract!(:inherits, :schema))
|
|
37
|
-
end
|
|
38
|
-
|
|
39
36
|
# Allow filtered bulk insert by adding the where clause. This method is
|
|
40
37
|
# only used by +InsertAll+, so it somewhat safe to override it
|
|
41
38
|
def build_insert_sql(insert)
|
|
@@ -13,12 +13,74 @@ module Torque
|
|
|
13
13
|
include ::Arel::Math
|
|
14
14
|
|
|
15
15
|
def initialize(left, right, array = false)
|
|
16
|
-
right =
|
|
16
|
+
right = right.to_s.dup
|
|
17
17
|
right << '[]' if array
|
|
18
18
|
super left, right
|
|
19
19
|
end
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
+
# Accessing a single column of a composite value, which behaves like an
|
|
23
|
+
# attribute of the composite type it belongs to
|
|
24
|
+
class Column < ::Arel::Nodes::Binary
|
|
25
|
+
include ::Arel::Expressions
|
|
26
|
+
include ::Arel::Predications
|
|
27
|
+
include ::Arel::AliasPredication
|
|
28
|
+
include ::Arel::OrderPredications
|
|
29
|
+
include ::Arel::Math
|
|
30
|
+
|
|
31
|
+
attr_reader :type_caster
|
|
32
|
+
alias name right
|
|
33
|
+
|
|
34
|
+
def initialize(source, name, type_caster)
|
|
35
|
+
@type_caster = type_caster
|
|
36
|
+
super(source, name.to_s)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def able_to_type_cast?
|
|
40
|
+
true
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def type_cast_for_database(value)
|
|
44
|
+
type_caster.serialize(value)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Accessing a single property of a document, at any depth, casted to
|
|
49
|
+
# the type that the class declares for it
|
|
50
|
+
class Property < ::Arel::Nodes::Binary
|
|
51
|
+
include ::Arel::Expressions
|
|
52
|
+
include ::Arel::Predications
|
|
53
|
+
include ::Arel::AliasPredication
|
|
54
|
+
include ::Arel::OrderPredications
|
|
55
|
+
include ::Arel::Math
|
|
56
|
+
|
|
57
|
+
attr_reader :type_caster, :cast
|
|
58
|
+
|
|
59
|
+
alias path right
|
|
60
|
+
|
|
61
|
+
# Reaching a property of another one simply adds up to the path, so
|
|
62
|
+
# that a single node always holds the whole way to it
|
|
63
|
+
def initialize(source, key, type_caster, cast = nil)
|
|
64
|
+
@type_caster = type_caster
|
|
65
|
+
@cast = cast
|
|
66
|
+
|
|
67
|
+
return super(source.left, source.path + [key.to_s]) if source.is_a?(Property)
|
|
68
|
+
super(source, [key.to_s])
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def name
|
|
72
|
+
right.last
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def able_to_type_cast?
|
|
76
|
+
true
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def type_cast_for_database(value)
|
|
80
|
+
type_caster.serialize(value)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
22
84
|
class Ref < ::Arel::Nodes::Unary
|
|
23
85
|
attr_reader :reference
|
|
24
86
|
alias to_s expr
|
|
@@ -16,19 +16,30 @@ module Torque
|
|
|
16
16
|
quote_array(o.expr, collector)
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
# Allow quoted arrays to get here
|
|
20
|
-
def visit_Arel_Nodes_Casted(o, collector)
|
|
21
|
-
value = o.value_for_database
|
|
22
|
-
klass = ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array::Data
|
|
23
|
-
return super unless value.is_a?(klass)
|
|
24
|
-
quote_array(value.values, collector)
|
|
25
|
-
end
|
|
26
|
-
|
|
27
19
|
## TORQUE VISITORS
|
|
28
20
|
def visit_Torque_PostgreSQL_Arel_Nodes_Ref(o, collector)
|
|
29
21
|
collector << quote_table_name(o.expr)
|
|
30
22
|
end
|
|
31
23
|
|
|
24
|
+
# Access a single column of a composite value
|
|
25
|
+
def visit_Torque_PostgreSQL_Arel_Nodes_Column(o, collector)
|
|
26
|
+
collector << '('
|
|
27
|
+
visit(o.left, collector)
|
|
28
|
+
collector << ').' << quote_column_name(o.right)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Access a single property of a document, as text, so that it can then
|
|
32
|
+
# be casted to whatever the property is supposed to be
|
|
33
|
+
def visit_Torque_PostgreSQL_Arel_Nodes_Property(o, collector)
|
|
34
|
+
collector << '('
|
|
35
|
+
visit(o.left, collector)
|
|
36
|
+
collector << ' #>> '
|
|
37
|
+
visit(::Arel.array(o.path), collector)
|
|
38
|
+
collector << ')'
|
|
39
|
+
collector << '::' << o.cast if o.cast
|
|
40
|
+
collector
|
|
41
|
+
end
|
|
42
|
+
|
|
32
43
|
# Allow casting any node
|
|
33
44
|
def visit_Torque_PostgreSQL_Arel_Nodes_Cast(o, collector)
|
|
34
45
|
visit(o.left, collector) << '::' << o.right
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Torque
|
|
4
|
+
module PostgreSQL
|
|
5
|
+
module Associations
|
|
6
|
+
module JoinDependency
|
|
7
|
+
def apply_column_aliases(relation)
|
|
8
|
+
result = super
|
|
9
|
+
return result unless relation.from_clause.empty?
|
|
10
|
+
|
|
11
|
+
relation._select!(root_record_class_marker) if cast_join_root?(relation)
|
|
12
|
+
relation._select!(-> { joined_record_class_markers })
|
|
13
|
+
result
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def cast_join_root?(relation)
|
|
19
|
+
return false if relation.itself_only_value === true
|
|
20
|
+
return false unless join_root_alias
|
|
21
|
+
|
|
22
|
+
join_root.base_klass.physically_inheritances?
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def root_record_class_marker
|
|
26
|
+
table = join_root.base_klass.arel_table
|
|
27
|
+
table['tableoid'].pg_cast(:regclass).as(record_class_column_name)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# The root is projected under its own name and picked up by the
|
|
31
|
+
# column sweep in #instantiate, which only ever feeds the root. Every
|
|
32
|
+
# other join part has to carry the marker inside its own set of
|
|
33
|
+
# aliases, since that is all #extract_record ever reads
|
|
34
|
+
def joined_record_class_markers
|
|
35
|
+
markers = join_root.each_with_index.map do |join_part, index|
|
|
36
|
+
next if index.zero? || !join_part.base_klass.physically_inheritances?
|
|
37
|
+
next if (columns = aliases.column_aliases(join_part)).blank?
|
|
38
|
+
|
|
39
|
+
column_alias = "#{columns.first.alias[/\At\d+/]}_r#{columns.size}"
|
|
40
|
+
columns << ::ActiveRecord::Associations::JoinDependency::Aliases::Column
|
|
41
|
+
.new(record_class_column_name, column_alias)
|
|
42
|
+
|
|
43
|
+
join_part.table['tableoid'].pg_cast(:regclass).as(column_alias)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
markers.compact
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def record_class_column_name
|
|
50
|
+
::ActiveRecord::Relation._record_class_column_name
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -4,7 +4,10 @@ require_relative 'associations/foreign_association'
|
|
|
4
4
|
|
|
5
5
|
require_relative 'associations/builder'
|
|
6
6
|
require_relative 'associations/preloader'
|
|
7
|
+
require_relative 'associations/join_dependency'
|
|
7
8
|
|
|
8
9
|
association_mod = Torque::PostgreSQL::Associations::ForeignAssociation
|
|
9
10
|
::ActiveRecord::Associations::HasManyAssociation.prepend(association_mod)
|
|
10
11
|
::ActiveRecord::Associations::BelongsToManyAssociation.prepend(association_mod)
|
|
12
|
+
|
|
13
|
+
::ActiveRecord::Associations::JoinDependency.prepend(Torque::PostgreSQL::Associations::JoinDependency)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'simple_enum'
|
|
4
|
+
|
|
5
|
+
module Torque
|
|
6
|
+
module PostgreSQL
|
|
7
|
+
module Attributes
|
|
8
|
+
# The common ground between the classes that back a column with a set of
|
|
9
|
+
# typed properties, like structs and composite types. It carries enough of
|
|
10
|
+
# the ActiveRecord attribute surface for its own features, and for the
|
|
11
|
+
# ActiveRecord concerns that build on top of it, to work
|
|
12
|
+
class Base
|
|
13
|
+
include ActiveModel::Model
|
|
14
|
+
include ActiveModel::Attributes
|
|
15
|
+
include ActiveModel::Serialization
|
|
16
|
+
include ActiveModel::Serializers::JSON
|
|
17
|
+
include ActiveModel::Dirty
|
|
18
|
+
include ActiveModel::Validations::Callbacks
|
|
19
|
+
|
|
20
|
+
include SimpleEnum
|
|
21
|
+
include ActiveRecord::Store
|
|
22
|
+
# Rails 8.1 moved normalization to Active Model, which is where these
|
|
23
|
+
# classes belong anyway
|
|
24
|
+
include(AR810 ? ActiveModel::Attributes::Normalization : ActiveRecord::Normalization)
|
|
25
|
+
include ActiveRecord::Encryption::EncryptableRecord
|
|
26
|
+
|
|
27
|
+
class << self
|
|
28
|
+
# These classes are not backed by a table, so nothing here has the
|
|
29
|
+
# extra information that a column would provide
|
|
30
|
+
def columns_hash
|
|
31
|
+
{}
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Encrypt individual attributes, so their values are stored encrypted
|
|
35
|
+
# inside the column that holds them
|
|
36
|
+
def encrypts(*names, **options)
|
|
37
|
+
names.each do |name|
|
|
38
|
+
raise ArgumentError, <<~MSG.squish unless attribute_names.include?(name.to_s)
|
|
39
|
+
Unable to encrypt "#{name}" because it is not a declared
|
|
40
|
+
attribute of #{self.name}.
|
|
41
|
+
MSG
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
super
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Plain access to any property, declared or not
|
|
49
|
+
def [](key)
|
|
50
|
+
key = key.to_s
|
|
51
|
+
@attributes.key?(key) ? @attributes.fetch_value(key) : nil
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def []=(key, value)
|
|
55
|
+
assign_attributes(key => value)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def ==(other)
|
|
59
|
+
other.class == self.class && other.attributes == attributes
|
|
60
|
+
end
|
|
61
|
+
alias eql? ==
|
|
62
|
+
|
|
63
|
+
def empty?
|
|
64
|
+
@attributes.keys.empty?
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def hash
|
|
68
|
+
[self.class, attributes].hash
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def to_h
|
|
72
|
+
attributes.symbolize_keys
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def inspect
|
|
76
|
+
entries = attributes.map { |key, value| "#{key}: #{value.inspect}" }
|
|
77
|
+
"#<#{self.class.name} #{entries.join(', ')}>"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def type_for_attribute(name, &block)
|
|
81
|
+
self.class.type_for_attribute(name, &block)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def read_attribute_before_type_cast(name)
|
|
85
|
+
@attributes[name.to_s].value_before_type_cast
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def read_attribute_for_database(name)
|
|
89
|
+
@attributes[name.to_s].value_for_database
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
|
|
5
|
+
module Torque
|
|
6
|
+
module PostgreSQL
|
|
7
|
+
module Attributes
|
|
8
|
+
class Composite < Base
|
|
9
|
+
class << self
|
|
10
|
+
attr_writer :type_name
|
|
11
|
+
|
|
12
|
+
# Find or create the class that will handle the composite type. It
|
|
13
|
+
# first checks the irregular types mapping, then the namespace
|
|
14
|
+
def lookup(name)
|
|
15
|
+
name = name.to_s.underscore
|
|
16
|
+
mapping = PostgreSQL.config.composite.irregular_types
|
|
17
|
+
return resolve(mapping[name].constantize, name) if mapping.key?(name)
|
|
18
|
+
|
|
19
|
+
const = name.camelize
|
|
20
|
+
namespace = PostgreSQL.config.composite.namespace
|
|
21
|
+
|
|
22
|
+
return resolve(namespace.const_get(const), name) if namespace.const_defined?(const)
|
|
23
|
+
resolve(namespace.const_set(const, Class.new(Composite)), name)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def inherited(subclass)
|
|
27
|
+
super
|
|
28
|
+
subclass.instance_variable_set(:@load_columns_monitor, ::Monitor.new)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# The name of the composite type on the database
|
|
32
|
+
def type_name
|
|
33
|
+
@type_name ||= name.demodulize.underscore
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# The ordered list of columns of the composite type, as a hash of
|
|
37
|
+
# name and type, loaded in a lazy way
|
|
38
|
+
def columns
|
|
39
|
+
load_columns
|
|
40
|
+
@columns
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Load the columns from the database and define any attribute that
|
|
44
|
+
# was not manually declared
|
|
45
|
+
def load_columns
|
|
46
|
+
return if self == Composite || defined?(@columns)
|
|
47
|
+
|
|
48
|
+
@load_columns_monitor.synchronize do
|
|
49
|
+
next if defined?(@columns)
|
|
50
|
+
|
|
51
|
+
columns = connection.composite_column_types(type_name)
|
|
52
|
+
columns.each do |attr_name, attr_type|
|
|
53
|
+
attribute(attr_name, attr_type) unless attribute_names.include?(attr_name)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
@columns = columns
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Drop the loaded columns, so that they are collected again on the
|
|
61
|
+
# next use, which happens after the type is changed
|
|
62
|
+
def reset_columns!
|
|
63
|
+
@load_columns_monitor.synchronize do
|
|
64
|
+
remove_instance_variable(:@columns) if defined?(@columns)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def new(*args, **xargs, &block)
|
|
69
|
+
load_columns
|
|
70
|
+
super
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Columns have to be in place before any of them can be decorated
|
|
74
|
+
def encrypts(*names, **options)
|
|
75
|
+
load_columns
|
|
76
|
+
super
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def enum(*args, **xargs)
|
|
80
|
+
load_columns
|
|
81
|
+
super
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
# Make sure that the class knows the name of the type it handles,
|
|
87
|
+
# since it can be resolved from an irregular mapping
|
|
88
|
+
def resolve(klass, name)
|
|
89
|
+
klass.type_name = name
|
|
90
|
+
klass
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def connection
|
|
94
|
+
ActiveRecord::Base.connection
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
Composite = Attributes::Composite
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -39,11 +39,22 @@ module Torque
|
|
|
39
39
|
super
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
+
def inherited(subclass)
|
|
43
|
+
super
|
|
44
|
+
subclass.instance_variable_set(:@load_values_monitor, ::Monitor.new)
|
|
45
|
+
end
|
|
46
|
+
|
|
42
47
|
# Load the list of values in a lazy way
|
|
43
48
|
def values
|
|
44
|
-
|
|
45
|
-
|
|
49
|
+
return if self == Enum
|
|
50
|
+
return @values if defined?(@values)
|
|
51
|
+
|
|
52
|
+
@load_values_monitor.synchronize do
|
|
53
|
+
next if defined?(@values)
|
|
54
|
+
@values = connection.enum_values(type_name).freeze
|
|
46
55
|
end
|
|
56
|
+
|
|
57
|
+
@values
|
|
47
58
|
end
|
|
48
59
|
|
|
49
60
|
# List of values as symbols
|