torque-postgresql 4.0.1 → 4.1.1
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 +52 -0
- data/lib/generators/torque/skill_generator.rb +17 -0
- data/lib/generators/torque/templates/trigger.sql.erb +3 -0
- data/lib/generators/torque/trigger_generator.rb +13 -0
- data/lib/torque/postgresql/adapter/database_statements.rb +98 -7
- data/lib/torque/postgresql/adapter/inheritance_statements.rb +297 -0
- data/lib/torque/postgresql/adapter/oid/array.rb +13 -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 +117 -0
- data/lib/torque/postgresql/adapter/oid/enum.rb +1 -1
- data/lib/torque/postgresql/adapter/oid/enum_set.rb +2 -2
- data/lib/torque/postgresql/adapter/oid/interval.rb +1 -1
- data/lib/torque/postgresql/adapter/oid/line.rb +1 -1
- data/lib/torque/postgresql/adapter/oid/lquery.rb +70 -0
- data/lib/torque/postgresql/adapter/oid/ltree.rb +111 -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/oid.rb +13 -0
- data/lib/torque/postgresql/adapter/quoting.rb +11 -2
- data/lib/torque/postgresql/adapter/schema_definitions.rb +51 -0
- data/lib/torque/postgresql/adapter/schema_dumper.rb +27 -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 +114 -1
- data/lib/torque/postgresql/arel/visitors.rb +65 -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 +97 -0
- data/lib/torque/postgresql/attributes/composite.rb +98 -0
- data/lib/torque/postgresql/attributes/enum.rb +13 -7
- data/lib/torque/postgresql/attributes/lquery.rb +120 -0
- data/lib/torque/postgresql/attributes/ltree.rb +121 -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 +26 -15
- data/lib/torque/postgresql/config.rb +100 -15
- data/lib/torque/postgresql/geometry_builder.rb +4 -10
- data/lib/torque/postgresql/inheritance/expander.rb +66 -0
- data/lib/torque/postgresql/inheritance/record.rb +52 -0
- data/lib/torque/postgresql/inheritance.rb +141 -68
- data/lib/torque/postgresql/migration/command_recorder.rb +76 -0
- data/lib/torque/postgresql/predicate_builder/arel_attribute_handler.rb +1 -1
- data/lib/torque/postgresql/predicate_builder/array_handler.rb +16 -8
- data/lib/torque/postgresql/predicate_builder/composite_handler.rb +122 -0
- data/lib/torque/postgresql/predicate_builder/ltree_handler.rb +112 -0
- data/lib/torque/postgresql/predicate_builder/struct_handler.rb +88 -0
- data/lib/torque/postgresql/predicate_builder.rb +53 -0
- data/lib/torque/postgresql/predicate_table.rb +61 -0
- data/lib/torque/postgresql/railtie.rb +52 -0
- data/lib/torque/postgresql/relation/auxiliary_statement.rb +7 -3
- data/lib/torque/postgresql/relation/buckets.rb +2 -1
- data/lib/torque/postgresql/relation/cross_join.rb +193 -0
- data/lib/torque/postgresql/relation/inheritance.rb +145 -28
- data/lib/torque/postgresql/relation/join_series.rb +1 -0
- data/lib/torque/postgresql/relation/merger.rb +30 -5
- data/lib/torque/postgresql/relation/segments.rb +140 -0
- data/lib/torque/postgresql/relation.rb +32 -12
- 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 +24 -1
- data/lib/torque/postgresql/versioned_commands.rb +39 -0
- data/lib/torque/postgresql.rb +4 -0
- data/sig/generators/torque/function_generator.rbs +9 -0
- data/sig/generators/torque/skill_generator.rbs +7 -0
- data/sig/generators/torque/trigger_generator.rbs +9 -0
- data/sig/generators/torque/type_generator.rbs +9 -0
- data/sig/generators/torque/view_generator.rbs +9 -0
- data/sig/manifest.yaml +7 -0
- data/sig/torque/postgresql/adapter/database_statements.rbs +42 -0
- data/sig/torque/postgresql/adapter/inheritance_statements.rbs +36 -0
- data/sig/torque/postgresql/adapter/oid/array.rbs +20 -0
- data/sig/torque/postgresql/adapter/oid/box.rbs +28 -0
- data/sig/torque/postgresql/adapter/oid/circle.rbs +33 -0
- data/sig/torque/postgresql/adapter/oid/composite.rbs +35 -0
- data/sig/torque/postgresql/adapter/oid/enum.rbs +28 -0
- data/sig/torque/postgresql/adapter/oid/enum_set.rbs +19 -0
- data/sig/torque/postgresql/adapter/oid/interval.rbs +20 -0
- data/sig/torque/postgresql/adapter/oid/line.rbs +33 -0
- data/sig/torque/postgresql/adapter/oid/lquery.rbs +21 -0
- data/sig/torque/postgresql/adapter/oid/ltree.rbs +32 -0
- data/sig/torque/postgresql/adapter/oid/range.rbs +28 -0
- data/sig/torque/postgresql/adapter/oid/segment.rbs +38 -0
- data/sig/torque/postgresql/adapter/oid/struct.rbs +41 -0
- data/sig/torque/postgresql/adapter/oid/struct_list.rbs +20 -0
- data/sig/torque/postgresql/adapter/oid/struct_set.rbs +18 -0
- data/sig/torque/postgresql/adapter/oid.rbs +9 -0
- data/sig/torque/postgresql/adapter/quoting.rbs +20 -0
- data/sig/torque/postgresql/adapter/schema_creation.rbs +13 -0
- data/sig/torque/postgresql/adapter/schema_definitions.rbs +55 -0
- data/sig/torque/postgresql/adapter/schema_dumper.rbs +37 -0
- data/sig/torque/postgresql/adapter/schema_overrides.rbs +46 -0
- data/sig/torque/postgresql/adapter/schema_statements.rbs +50 -0
- data/sig/torque/postgresql/adapter.rbs +24 -0
- data/sig/torque/postgresql/arel/infix_operation.rbs +79 -0
- data/sig/torque/postgresql/arel/join_source.rbs +19 -0
- data/sig/torque/postgresql/arel/nodes.rbs +83 -0
- data/sig/torque/postgresql/arel/operations.rbs +28 -0
- data/sig/torque/postgresql/arel/select_manager.rbs +15 -0
- data/sig/torque/postgresql/arel/visitors.rbs +30 -0
- data/sig/torque/postgresql/arel.rbs +6 -0
- data/sig/torque/postgresql/associations/association_scope.rbs +28 -0
- data/sig/torque/postgresql/associations/belongs_to_many_association.rbs +51 -0
- data/sig/torque/postgresql/associations/builder/belongs_to_many.rbs +24 -0
- data/sig/torque/postgresql/associations/builder/has_many.rbs +15 -0
- data/sig/torque/postgresql/associations/builder.rbs +8 -0
- data/sig/torque/postgresql/associations/foreign_association.rbs +16 -0
- data/sig/torque/postgresql/associations/join_dependency.rbs +16 -0
- data/sig/torque/postgresql/associations/preloader/association.rbs +32 -0
- data/sig/torque/postgresql/associations/preloader/loader_query.rbs +20 -0
- data/sig/torque/postgresql/associations/preloader.rbs +8 -0
- data/sig/torque/postgresql/associations.rbs +17 -0
- data/sig/torque/postgresql/attributes/base.rbs +37 -0
- data/sig/torque/postgresql/attributes/builder/enum.rbs +38 -0
- data/sig/torque/postgresql/attributes/builder/full_text_search.rbs +27 -0
- data/sig/torque/postgresql/attributes/builder/period.rbs +106 -0
- data/sig/torque/postgresql/attributes/builder.rbs +19 -0
- data/sig/torque/postgresql/attributes/composite.rbs +28 -0
- data/sig/torque/postgresql/attributes/enum.rbs +69 -0
- data/sig/torque/postgresql/attributes/enum_set.rbs +75 -0
- data/sig/torque/postgresql/attributes/full_text_search.rbs +13 -0
- data/sig/torque/postgresql/attributes/lazy.rbs +17 -0
- data/sig/torque/postgresql/attributes/lquery.rbs +48 -0
- data/sig/torque/postgresql/attributes/ltree.rbs +46 -0
- data/sig/torque/postgresql/attributes/period.rbs +13 -0
- data/sig/torque/postgresql/attributes/simple_enum.rbs +39 -0
- data/sig/torque/postgresql/attributes/struct.rbs +38 -0
- data/sig/torque/postgresql/attributes.rbs +6 -0
- data/sig/torque/postgresql/autosave_association.rbs +16 -0
- data/sig/torque/postgresql/auxiliary_statement/recursive.rbs +22 -0
- data/sig/torque/postgresql/auxiliary_statement/settings.rbs +63 -0
- data/sig/torque/postgresql/auxiliary_statement.rbs +57 -0
- data/sig/torque/postgresql/base.rbs +41 -0
- data/sig/torque/postgresql/collector.rbs +7 -0
- data/sig/torque/postgresql/config.rbs +9 -0
- data/sig/torque/postgresql/function.rbs +21 -0
- data/sig/torque/postgresql/geometry_builder.rbs +20 -0
- data/sig/torque/postgresql/i18n.rbs +15 -0
- data/sig/torque/postgresql/inheritance/expander.rbs +23 -0
- data/sig/torque/postgresql/inheritance/record.rbs +20 -0
- data/sig/torque/postgresql/inheritance.rbs +47 -0
- data/sig/torque/postgresql/insert_all.rbs +22 -0
- data/sig/torque/postgresql/migration/command_recorder.rbs +30 -0
- data/sig/torque/postgresql/migration.rbs +6 -0
- data/sig/torque/postgresql/predicate_builder/arel_attribute_handler.rbs +15 -0
- data/sig/torque/postgresql/predicate_builder/array_handler.rbs +26 -0
- data/sig/torque/postgresql/predicate_builder/composite_handler.rbs +26 -0
- data/sig/torque/postgresql/predicate_builder/enumerator_lazy_handler.rbs +18 -0
- data/sig/torque/postgresql/predicate_builder/ltree_handler.rbs +36 -0
- data/sig/torque/postgresql/predicate_builder/regexp_handler.rbs +16 -0
- data/sig/torque/postgresql/predicate_builder/struct_handler.rbs +24 -0
- data/sig/torque/postgresql/predicate_builder.rbs +23 -0
- data/sig/torque/postgresql/predicate_table.rbs +24 -0
- data/sig/torque/postgresql/railtie.rbs +10 -0
- data/sig/torque/postgresql/reflection/abstract_reflection.rbs +22 -0
- data/sig/torque/postgresql/reflection/association_reflection.rbs +18 -0
- data/sig/torque/postgresql/reflection/belongs_to_many_reflection.rbs +30 -0
- data/sig/torque/postgresql/reflection/has_many_reflection.rbs +14 -0
- data/sig/torque/postgresql/reflection/runtime_reflection.rbs +18 -0
- data/sig/torque/postgresql/reflection/through_reflection.rbs +14 -0
- data/sig/torque/postgresql/reflection.rbs +11 -0
- data/sig/torque/postgresql/relation/auxiliary_statement.rbs +29 -0
- data/sig/torque/postgresql/relation/buckets.rbs +35 -0
- data/sig/torque/postgresql/relation/cross_join.rbs +63 -0
- data/sig/torque/postgresql/relation/distinct_on.rbs +16 -0
- data/sig/torque/postgresql/relation/inheritance.rbs +48 -0
- data/sig/torque/postgresql/relation/join_series.rbs +28 -0
- data/sig/torque/postgresql/relation/merger.rbs +23 -0
- data/sig/torque/postgresql/relation/segments.rbs +27 -0
- data/sig/torque/postgresql/relation.rbs +48 -0
- data/sig/torque/postgresql/schema_cache/bound_schema_reflection.rbs +14 -0
- data/sig/torque/postgresql/schema_cache/inheritance.rbs +12 -0
- data/sig/torque/postgresql/schema_cache/schema_reflection.rbs +14 -0
- data/sig/torque/postgresql/schema_cache.rbs +43 -0
- data/sig/torque/postgresql/table_name.rbs +20 -0
- data/sig/torque/postgresql/validations.rbs +13 -0
- data/sig/torque/postgresql/version.rbs +5 -0
- data/sig/torque/postgresql/versioned_commands/command_migration.rbs +54 -0
- data/sig/torque/postgresql/versioned_commands/generator.rbs +17 -0
- data/sig/torque/postgresql/versioned_commands/migration_context.rbs +30 -0
- data/sig/torque/postgresql/versioned_commands/migrator.rbs +19 -0
- data/sig/torque/postgresql/versioned_commands/schema_table.rbs +23 -0
- data/sig/torque/postgresql/versioned_commands.rbs +25 -0
- data/sig/torque/postgresql.rbs +26 -0
- data/skills/torque-postgresql/SKILL.md +70 -0
- data/spec/fixtures/migrations/20250101000008_create_trigger_audit_users_v1.sql +3 -0
- data/spec/initialize.rb +29 -2
- data/spec/mocks/cache_query.rb +13 -1
- 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/rbs_helper.rb +8 -0
- data/spec/schema.rb +35 -4
- data/spec/spec_helper.rb +1 -0
- data/spec/tests/arel_spec.rb +3 -3
- data/spec/tests/auxiliary_statement_spec.rb +12 -0
- data/spec/tests/buckets_spec.rb +122 -0
- data/spec/tests/composite_spec.rb +867 -0
- data/spec/tests/cross_join_spec.rb +185 -0
- data/spec/tests/distinct_on_spec.rb +5 -0
- data/spec/tests/enum_set_spec.rb +4 -0
- data/spec/tests/enum_spec.rb +20 -4
- data/spec/tests/geometric_builder_spec.rb +16 -5
- data/spec/tests/has_many_spec.rb +2 -1
- data/spec/tests/interval_spec.rb +6 -0
- data/spec/tests/ltree_spec.rb +886 -0
- data/spec/tests/predicate_builder_spec.rb +120 -0
- data/spec/tests/relation_spec.rb +25 -69
- data/spec/tests/segments_spec.rb +264 -0
- data/spec/tests/struct_spec.rb +1170 -0
- data/spec/tests/table_inheritance_spec.rb +1037 -56
- data/spec/tests/versioned_commands_spec.rb +145 -0
- metadata +198 -2
|
@@ -0,0 +1,117 @@
|
|
|
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 or a delegator
|
|
17
|
+
def self.from(type)
|
|
18
|
+
type = OID.unwrap(type)
|
|
19
|
+
type = type.subtype if type.is_a?(ARRAY)
|
|
20
|
+
type if type.is_a?(self)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.create(row, type_map)
|
|
24
|
+
oid_klass = new(row['typname'])
|
|
25
|
+
type_map.register_type(row['oid'].to_i, oid_klass)
|
|
26
|
+
type_map.register_type(row['typarray'].to_i, ARRAY.new(oid_klass, row['typdelim']))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def initialize(name)
|
|
30
|
+
@name = name.to_s.freeze
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# The class that represents the composite type, loaded in a lazy way.
|
|
34
|
+
# It cannot be memoized here because Rails freezes the types it
|
|
35
|
+
# resolves through the type map, so the lookup owns the cache
|
|
36
|
+
def klass
|
|
37
|
+
Attributes::Composite.lookup(name)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# The ordered list of columns of the composite type, as a hash of
|
|
41
|
+
# name and type
|
|
42
|
+
def columns
|
|
43
|
+
klass.columns
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def type
|
|
47
|
+
:composite
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def cast(value)
|
|
51
|
+
case value
|
|
52
|
+
when klass then value
|
|
53
|
+
when ::Hash then build(value)
|
|
54
|
+
when DATA then deserialize(value)
|
|
55
|
+
when ::String then deserialize(value)
|
|
56
|
+
when ::Array then build(columns.keys.zip(value).to_h)
|
|
57
|
+
else value
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# A record that was already serialized comes back as the encoder and
|
|
62
|
+
# its values, which is how it is kept around for dirty tracking
|
|
63
|
+
def deserialize(value)
|
|
64
|
+
return if value.nil?
|
|
65
|
+
|
|
66
|
+
fields = value.is_a?(DATA) ? value.values : DECODER.decode(value)
|
|
67
|
+
instance = klass.new
|
|
68
|
+
set = attributes_of(instance)
|
|
69
|
+
|
|
70
|
+
columns.each_key.with_index do |attr_name, idx|
|
|
71
|
+
set.write_from_database(attr_name, fields[idx])
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
instance
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# The record is handed over to the adapter as an encoder and its
|
|
78
|
+
# values, the same way arrays are, so that columns are type casted by
|
|
79
|
+
# the connection right before the record literal is built. Values come
|
|
80
|
+
# from the record itself, so that types declared by the class, like
|
|
81
|
+
# encrypted ones, are the ones being applied
|
|
82
|
+
def serialize(value)
|
|
83
|
+
return if value.nil?
|
|
84
|
+
|
|
85
|
+
set = attributes_of(cast(value))
|
|
86
|
+
values = columns.each_key.map { |attr_name| set[attr_name].value_for_database }
|
|
87
|
+
|
|
88
|
+
DATA.new(ENCODER, values)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def changed_in_place?(raw_old_value, new_value)
|
|
92
|
+
deserialize(raw_old_value) != new_value
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def ==(other)
|
|
96
|
+
other.class == self.class && other.name == name
|
|
97
|
+
end
|
|
98
|
+
alias eql? ==
|
|
99
|
+
|
|
100
|
+
def hash
|
|
101
|
+
[self.class, name].hash
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
private
|
|
105
|
+
|
|
106
|
+
def build(attrs)
|
|
107
|
+
klass.new(**attrs.symbolize_keys)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def attributes_of(record)
|
|
111
|
+
record.instance_variable_get(:@attributes)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'ltree'
|
|
4
|
+
|
|
5
|
+
module Torque
|
|
6
|
+
module PostgreSQL
|
|
7
|
+
module Adapter
|
|
8
|
+
module OID
|
|
9
|
+
# Same as a path, but the items are the pure parts of a pattern: +:any+
|
|
10
|
+
# for a star, a Range for a quantified star and an Array for a group of
|
|
11
|
+
# alternatives. Everything else is a label, written as it appears in SQL
|
|
12
|
+
class Lquery < Ltree
|
|
13
|
+
STAR = /\A\*\{(?<min>\d+)?(?<comma>,)?(?<max>\d+)?\}\z/
|
|
14
|
+
|
|
15
|
+
def type
|
|
16
|
+
:lquery
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def value_class
|
|
22
|
+
LQuery
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def items_of(entry, sanitize)
|
|
26
|
+
case entry
|
|
27
|
+
when ::Symbol then entry == :any ? [:any] : super
|
|
28
|
+
when ::Range then [entry]
|
|
29
|
+
when ::Array then [entry.flat_map { |item| items_of(item, sanitize) }]
|
|
30
|
+
else super
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def compile(item)
|
|
35
|
+
case item
|
|
36
|
+
when ::Symbol then item == :any ? '*' : item.to_s
|
|
37
|
+
when ::Range then quantifier(item)
|
|
38
|
+
when ::Array then item.map { |entry| compile(entry) }.join('|')
|
|
39
|
+
else item.to_s
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def quantifier(range)
|
|
44
|
+
min = range.begin
|
|
45
|
+
max = range.end
|
|
46
|
+
max -= 1 if max && range.exclude_end?
|
|
47
|
+
|
|
48
|
+
return '*' if min.nil? && max.nil?
|
|
49
|
+
return "*{#{min}}" if min == max
|
|
50
|
+
|
|
51
|
+
"*{#{min},#{max}}"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def parse(text)
|
|
55
|
+
return :any if text == '*'
|
|
56
|
+
return text.split('|') if text.include?('|') && !text.match?(/[!{]/)
|
|
57
|
+
|
|
58
|
+
match = text.match(STAR)
|
|
59
|
+
return text if match.nil?
|
|
60
|
+
|
|
61
|
+
min = match[:min]&.to_i
|
|
62
|
+
max = match[:comma] ? match[:max]&.to_i : min
|
|
63
|
+
min..max
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Torque
|
|
4
|
+
module PostgreSQL
|
|
5
|
+
module Adapter
|
|
6
|
+
module OID
|
|
7
|
+
# Moves a label path between the shapes the gem accepts, its pure list
|
|
8
|
+
# of items and the text form PostgreSQL stores. Nothing is validated
|
|
9
|
+
# here, that is left to PostgreSQL
|
|
10
|
+
class Ltree < ActiveModel::Type::Value
|
|
11
|
+
|
|
12
|
+
def type
|
|
13
|
+
:ltree
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def cast(value)
|
|
17
|
+
return value if value.is_a?(value_class)
|
|
18
|
+
return if value.blank?
|
|
19
|
+
|
|
20
|
+
value_class.new(value)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def serialize(value)
|
|
24
|
+
path = cast(value)
|
|
25
|
+
path.map { |item| compile(item) }.join('.') unless path.nil?
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def deserialize(value)
|
|
29
|
+
return if value.nil?
|
|
30
|
+
|
|
31
|
+
value_class.new(value, sanitize: false)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def items_for(value, sanitize: true)
|
|
35
|
+
::Array.wrap(value).flat_map { |entry| items_of(entry, sanitize) }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def type_cast_for_schema(value)
|
|
39
|
+
serialize(value).inspect
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def changed_in_place?(raw_old_value, new_value)
|
|
43
|
+
raw_old_value != serialize(new_value)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def ==(other)
|
|
47
|
+
other.class == self.class
|
|
48
|
+
end
|
|
49
|
+
alias eql? ==
|
|
50
|
+
|
|
51
|
+
def hash
|
|
52
|
+
self.class.hash
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def value_class
|
|
58
|
+
LTree
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def items_of(entry, sanitize)
|
|
62
|
+
return items_for(LTree.compatible(entry), sanitize: sanitize) if LTree.compatible?(entry)
|
|
63
|
+
|
|
64
|
+
case entry
|
|
65
|
+
when LTree, LQuery then entry.items
|
|
66
|
+
when ::Array then items_for(entry, sanitize: sanitize)
|
|
67
|
+
when ::ActiveRecord::Base then labels_of(record_label(entry), sanitize)
|
|
68
|
+
else labels_of(entry.to_s, sanitize)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def labels_of(text, sanitize)
|
|
73
|
+
text = sanitize(text) if sanitize
|
|
74
|
+
text.split('.').map { |item| parse(item) }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def record_label(record)
|
|
78
|
+
id = record.id
|
|
79
|
+
raise ArgumentError, <<~MSG.squish if id.nil?
|
|
80
|
+
Unable to use #{record.class.name} as a label because its
|
|
81
|
+
#{record.class.primary_key} is still empty.
|
|
82
|
+
MSG
|
|
83
|
+
|
|
84
|
+
raise ArgumentError, <<~MSG.squish if id.is_a?(::Array)
|
|
85
|
+
Unable to use #{record.class.name} as a label because it has a
|
|
86
|
+
composite primary key, which cannot be a single label.
|
|
87
|
+
MSG
|
|
88
|
+
|
|
89
|
+
id.to_s
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def sanitize(text)
|
|
93
|
+
replacements = PostgreSQL.config.ltree.sanitize
|
|
94
|
+
return text if replacements.blank?
|
|
95
|
+
|
|
96
|
+
text.gsub(Regexp.union(replacements.keys), replacements)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def compile(item)
|
|
100
|
+
item.to_s
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def parse(text)
|
|
104
|
+
text
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Torque
|
|
4
|
+
module PostgreSQL
|
|
5
|
+
module Adapter
|
|
6
|
+
module OID
|
|
7
|
+
class Struct < ActiveRecord::Type::Json
|
|
8
|
+
attr_reader :klass, :type, :backfill
|
|
9
|
+
|
|
10
|
+
# A comparable representation of a struct-backed value, based on the
|
|
11
|
+
# values it exposes instead of the document it generates, so that
|
|
12
|
+
# non-deterministic types like encrypted attributes stay stable
|
|
13
|
+
def self.state(value)
|
|
14
|
+
case value
|
|
15
|
+
when nil then nil
|
|
16
|
+
when ::Array then value.map { |item| state(item) }
|
|
17
|
+
else value.respond_to?(:attributes) ? [value.class, value.attributes] : value
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def initialize(klass, type: :jsonb, backfill: nil)
|
|
22
|
+
super()
|
|
23
|
+
@klass = klass
|
|
24
|
+
@type = type
|
|
25
|
+
@backfill = backfill
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def cast(value)
|
|
29
|
+
case value
|
|
30
|
+
when klass then value
|
|
31
|
+
when ::Hash then build(value)
|
|
32
|
+
when ::String then cast(parse(value))
|
|
33
|
+
else value
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def deserialize(value)
|
|
38
|
+
build(parse(value), from_database: true, blank: true)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def serialize(value)
|
|
42
|
+
case value
|
|
43
|
+
when klass then empty?(value) ? nil : super(document(value))
|
|
44
|
+
when nil then nil
|
|
45
|
+
else super
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def changed?(old_value, new_value, _new_value_before_cast)
|
|
50
|
+
Struct.state(old_value) != Struct.state(new_value)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def changed_in_place?(raw_old_value, new_value)
|
|
54
|
+
Struct.state(without_backfill.deserialize(raw_old_value)) != Struct.state(new_value)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# The same type, but reading documents exactly as they are stored, so
|
|
58
|
+
# that backfilled properties are seen as changes
|
|
59
|
+
def without_backfill
|
|
60
|
+
return self if backfill.nil?
|
|
61
|
+
@without_backfill ||= self.class.new(klass, type: type)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def empty?(value)
|
|
65
|
+
document(value).empty?
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# The value as it goes inside another document, which is a hash, and
|
|
69
|
+
# not the encoded JSON that a column receives
|
|
70
|
+
def document_for(value)
|
|
71
|
+
value.nil? ? nil : document(value)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# The hash that represents the value on the database, holding only the
|
|
75
|
+
# properties that were actually written to it. Properties backed by
|
|
76
|
+
# another struct are kept as documents, so that they are not stored as
|
|
77
|
+
# an encoded string inside this one
|
|
78
|
+
def document(value)
|
|
79
|
+
set = value.instance_variable_get(:@attributes)
|
|
80
|
+
set.keys.to_h do |name|
|
|
81
|
+
attribute = set[name]
|
|
82
|
+
type = OID.unwrap(attribute.type)
|
|
83
|
+
|
|
84
|
+
next [name, type.document_for(attribute.value)] if type.is_a?(Struct)
|
|
85
|
+
[name, attribute.value_for_database]
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def blank_document
|
|
90
|
+
{}
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def ==(other)
|
|
94
|
+
other.class == self.class && other.klass == klass && other.type == type
|
|
95
|
+
end
|
|
96
|
+
alias eql? ==
|
|
97
|
+
|
|
98
|
+
def hash
|
|
99
|
+
[self.class, klass, type].hash
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
private
|
|
103
|
+
|
|
104
|
+
def parse(value)
|
|
105
|
+
return value unless value.is_a?(::String)
|
|
106
|
+
ActiveSupport::JSON.decode(value) rescue nil
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def build(data, from_database: false, blank: false)
|
|
110
|
+
instance = klass.new
|
|
111
|
+
blank!(instance) if blank
|
|
112
|
+
return instance unless data.is_a?(::Hash)
|
|
113
|
+
|
|
114
|
+
from_database ? load(instance, data) : instance.assign_attributes(data)
|
|
115
|
+
instance
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Reset the instance to the state of an empty document, keeping the
|
|
119
|
+
# class-level defaults that were marked to be backfilled
|
|
120
|
+
def blank!(instance)
|
|
121
|
+
return if backfill == true
|
|
122
|
+
|
|
123
|
+
keep = ::Array.wrap(backfill).map(&:to_s)
|
|
124
|
+
set = instance.instance_variable_get(:@attributes).map do |attribute|
|
|
125
|
+
next attribute if keep.include?(attribute.name)
|
|
126
|
+
ActiveModel::Attribute.uninitialized(attribute.name, attribute.type)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
instance.instance_variable_set(:@attributes, set)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Properties that the class doesn't declare are kept as they are
|
|
133
|
+
# stored, regardless of the class being strict about them or not
|
|
134
|
+
def load(instance, data)
|
|
135
|
+
default = ActiveModel::Type.default_value
|
|
136
|
+
set = instance.instance_variable_get(:@attributes)
|
|
137
|
+
|
|
138
|
+
data.each do |key, value|
|
|
139
|
+
if klass.attribute_names.include?(key = key.to_s)
|
|
140
|
+
set.write_from_database(key, value)
|
|
141
|
+
else
|
|
142
|
+
set[key] = ActiveModel::Attribute.from_database(key, value, default)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Torque
|
|
4
|
+
module PostgreSQL
|
|
5
|
+
module Adapter
|
|
6
|
+
module OID
|
|
7
|
+
class StructList < Struct
|
|
8
|
+
def cast(value)
|
|
9
|
+
case value
|
|
10
|
+
when ::String then cast(parse(value))
|
|
11
|
+
when ::Array then value.map { |item| super(item) }
|
|
12
|
+
else value
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def deserialize(value)
|
|
17
|
+
::Array.wrap(parse(value)).map do |item|
|
|
18
|
+
build(item, from_database: true, blank: true)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def serialize(value)
|
|
23
|
+
case value
|
|
24
|
+
when ::Array then value.empty? ? nil : encode(value)
|
|
25
|
+
when nil then nil
|
|
26
|
+
else super
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def document_for(value)
|
|
31
|
+
::Array.wrap(value).map { |item| item.is_a?(klass) ? document(item) : item }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def empty?(value)
|
|
35
|
+
value.is_a?(::Array) ? value.empty? : super
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def blank_document
|
|
39
|
+
[]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def encode(items)
|
|
45
|
+
hashes = items.map { |item| item.is_a?(klass) ? document(item) : item }
|
|
46
|
+
ActiveSupport::JSON.encode(hashes)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Torque
|
|
4
|
+
module PostgreSQL
|
|
5
|
+
module Adapter
|
|
6
|
+
module OID
|
|
7
|
+
class StructSet < ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array
|
|
8
|
+
def deserialize(value)
|
|
9
|
+
value.nil? ? [] : super
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def serialize(value)
|
|
13
|
+
return if value.is_a?(::Array) && value.empty?
|
|
14
|
+
super
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def changed_in_place?(raw_old_value, new_value)
|
|
18
|
+
Struct.state(without_backfill.deserialize(raw_old_value)) !=
|
|
19
|
+
Struct.state(new_value)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def without_backfill
|
|
23
|
+
return self if subtype.backfill.nil?
|
|
24
|
+
@without_backfill ||= self.class.new(subtype.without_backfill, delimiter)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def empty?(value)
|
|
28
|
+
value.is_a?(::Array) ? value.empty? : subtype.empty?(value)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def blank_document
|
|
32
|
+
[]
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -13,7 +13,7 @@ module Torque
|
|
|
13
13
|
|
|
14
14
|
# Quotes type names for use in SQL queries.
|
|
15
15
|
def quote_type_name(name, *args)
|
|
16
|
-
QUOTED_TYPE_NAMES[args] ||= begin
|
|
16
|
+
QUOTED_TYPE_NAMES[[name.to_s, *args]] ||= begin
|
|
17
17
|
name = name.to_s
|
|
18
18
|
args << 'public' if args.empty? && !name.include?('.')
|
|
19
19
|
quote_identifier_name(name, *args)
|
|
@@ -36,11 +36,20 @@ module Torque
|
|
|
36
36
|
lookup_cast_type(column.sql_type)
|
|
37
37
|
elsif column.is_a?(Column) && column.array?
|
|
38
38
|
# When using +change_column_default+
|
|
39
|
-
|
|
39
|
+
lookup_cast_type_for_column(column)
|
|
40
40
|
end
|
|
41
41
|
|
|
42
42
|
type.nil? ? super : quote(type.serialize(value.to_a))
|
|
43
43
|
end
|
|
44
|
+
|
|
45
|
+
# Rails 8.1 dropped +lookup_cast_type_from_column+, and the sql_type of
|
|
46
|
+
# an array column does not carry the array part, so only the oid can
|
|
47
|
+
# resolve the real type
|
|
48
|
+
def lookup_cast_type_for_column(column)
|
|
49
|
+
return lookup_cast_type_from_column(column) unless AR810
|
|
50
|
+
|
|
51
|
+
type_map.lookup(column.oid, column.fmod, column.sql_type)
|
|
52
|
+
end
|
|
44
53
|
end
|
|
45
54
|
end
|
|
46
55
|
end
|