torque-postgresql 4.1.0 → 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 +28 -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 +9 -0
- data/lib/torque/postgresql/adapter/oid/array.rb +0 -4
- data/lib/torque/postgresql/adapter/oid/composite.rb +2 -1
- 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/lquery.rb +45 -28
- data/lib/torque/postgresql/adapter/oid/ltree.rb +67 -9
- data/lib/torque/postgresql/adapter/oid/struct.rb +1 -1
- data/lib/torque/postgresql/adapter/oid.rb +13 -0
- data/lib/torque/postgresql/adapter/schema_definitions.rb +5 -0
- data/lib/torque/postgresql/adapter/schema_dumper.rb +1 -0
- data/lib/torque/postgresql/arel/nodes.rb +51 -0
- data/lib/torque/postgresql/arel/visitors.rb +46 -0
- data/lib/torque/postgresql/attributes/base.rb +10 -7
- data/lib/torque/postgresql/attributes/composite.rb +1 -5
- data/lib/torque/postgresql/attributes/enum.rb +1 -6
- data/lib/torque/postgresql/attributes/lquery.rb +67 -127
- data/lib/torque/postgresql/attributes/ltree.rb +35 -83
- data/lib/torque/postgresql/base.rb +9 -1
- data/lib/torque/postgresql/config.rb +21 -3
- data/lib/torque/postgresql/geometry_builder.rb +4 -10
- data/lib/torque/postgresql/inheritance.rb +5 -5
- 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 +18 -5
- data/lib/torque/postgresql/predicate_builder/ltree_handler.rb +86 -18
- data/lib/torque/postgresql/predicate_builder/struct_handler.rb +29 -9
- data/lib/torque/postgresql/predicate_builder.rb +38 -11
- data/lib/torque/postgresql/railtie.rb +17 -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 +1 -2
- data/lib/torque/postgresql/relation/join_series.rb +1 -0
- data/lib/torque/postgresql/relation/merger.rb +9 -0
- data/lib/torque/postgresql/relation/segments.rb +140 -0
- data/lib/torque/postgresql/relation.rb +23 -4
- data/lib/torque/postgresql/version.rb +1 -1
- data/lib/torque/postgresql/versioned_commands/command_migration.rb +23 -0
- data/lib/torque/postgresql/versioned_commands.rb +39 -0
- data/lib/torque/postgresql.rb +0 -2
- 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 +9 -0
- data/spec/mocks/cache_query.rb +1 -1
- data/spec/rbs_helper.rb +8 -0
- data/spec/schema.rb +2 -0
- data/spec/spec_helper.rb +1 -0
- data/spec/tests/arel_spec.rb +1 -3
- data/spec/tests/auxiliary_statement_spec.rb +12 -0
- data/spec/tests/buckets_spec.rb +122 -0
- data/spec/tests/composite_spec.rb +67 -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 +398 -64
- 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 +202 -0
- data/spec/tests/table_inheritance_spec.rb +10 -0
- data/spec/tests/versioned_commands_spec.rb +145 -0
- metadata +153 -2
|
@@ -4,173 +4,113 @@ module Torque
|
|
|
4
4
|
module PostgreSQL
|
|
5
5
|
module Attributes
|
|
6
6
|
# A pattern that matches label paths, as described by the +lquery+ data
|
|
7
|
-
# type. It
|
|
8
|
-
#
|
|
7
|
+
# type. It holds the pure items of the pattern: labels as Strings, +:any+
|
|
8
|
+
# for a star, a Range for a quantified star and an Array for a group of
|
|
9
|
+
# alternatives
|
|
9
10
|
class LQuery
|
|
11
|
+
include Enumerable
|
|
12
|
+
|
|
10
13
|
MARKERS = /[*|!{}@%]/
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
ALTERNATIVE = /#{LABEL}[@*%]{0,3}/
|
|
14
|
-
ITEM = /\A!?#{ALTERNATIVE}(?:\|#{ALTERNATIVE})*#{QUANTIFIER}?\z/
|
|
15
|
-
STAR_ITEM = /\A\*#{QUANTIFIER}?\z/
|
|
16
|
-
|
|
17
|
-
class << self
|
|
18
|
-
def [](*items)
|
|
19
|
-
new(items)
|
|
20
|
-
end
|
|
14
|
+
LEVEL = /\A(?<not>!)?(?<variants>.*?)(?:\{(?<min>\d*)(?<comma>,)?(?<max>\d*)\})?\z/
|
|
15
|
+
VARIANT = /\A(?<name>.*?)(?<modifiers>[*@%]*)\z/
|
|
21
16
|
|
|
22
|
-
|
|
23
|
-
def load(value)
|
|
24
|
-
new(value, normalize: false)
|
|
25
|
-
end
|
|
17
|
+
attr_reader :items
|
|
26
18
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
19
|
+
delegate :inspect, to: :to_s
|
|
20
|
+
delegate :each, :size, :empty?, :[], to: :items
|
|
21
|
+
|
|
22
|
+
def self.[](*items)
|
|
23
|
+
new(items)
|
|
24
|
+
end
|
|
32
25
|
|
|
33
|
-
|
|
26
|
+
def initialize(value = nil, sanitize: true)
|
|
27
|
+
@items = type.items_for(value, sanitize: sanitize).freeze
|
|
28
|
+
end
|
|
34
29
|
|
|
35
|
-
|
|
36
|
-
|
|
30
|
+
def to_s
|
|
31
|
+
type.serialize(self)
|
|
32
|
+
end
|
|
37
33
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
when ::Range, ::Array, LQuery then true
|
|
41
|
-
when ::String then item.match?(MARKERS)
|
|
42
|
-
else false
|
|
43
|
-
end
|
|
44
|
-
end
|
|
34
|
+
def pattern?
|
|
35
|
+
items.any? { |item| !item.is_a?(::String) || item.match?(MARKERS) }
|
|
45
36
|
end
|
|
46
37
|
|
|
47
|
-
|
|
38
|
+
def pattern
|
|
39
|
+
@pattern ||= compile
|
|
40
|
+
end
|
|
48
41
|
|
|
49
|
-
def
|
|
50
|
-
|
|
51
|
-
|
|
42
|
+
def match?(path)
|
|
43
|
+
path = LTree.new(path) unless path.is_a?(LTree)
|
|
44
|
+
pattern.match?(path.to_s)
|
|
52
45
|
end
|
|
53
46
|
|
|
54
|
-
def
|
|
55
|
-
|
|
47
|
+
def =~(path)
|
|
48
|
+
path = LTree.new(path) unless path.is_a?(LTree)
|
|
49
|
+
pattern =~ path.to_s
|
|
56
50
|
end
|
|
57
51
|
|
|
58
52
|
def ==(other)
|
|
59
|
-
other
|
|
53
|
+
other = other.items if other.is_a?(self.class)
|
|
54
|
+
other.is_a?(::Array) && items == other
|
|
60
55
|
end
|
|
61
56
|
alias eql? ==
|
|
62
57
|
|
|
63
58
|
def hash
|
|
64
|
-
|
|
59
|
+
items.hash
|
|
65
60
|
end
|
|
66
61
|
|
|
67
62
|
private
|
|
68
63
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
def expand(value)
|
|
72
|
-
return expand(LTree.compatible(value)) if LTree.compatible?(value)
|
|
73
|
-
|
|
74
|
-
value = value.split('.') if value.is_a?(::String)
|
|
75
|
-
Array.wrap(value).flat_map { |item| expand_item(item) }
|
|
64
|
+
def type
|
|
65
|
+
Adapter::OID::Lquery.new
|
|
76
66
|
end
|
|
77
67
|
|
|
78
|
-
def
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
[item]
|
|
68
|
+
def compile
|
|
69
|
+
levels = to_s.split('.', -1).map { |level| compile_level(level) }
|
|
70
|
+
Regexp.new("\\A#{levels.join}\\z")
|
|
82
71
|
end
|
|
83
72
|
|
|
84
|
-
def
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
else
|
|
91
|
-
raise ArgumentError, <<~MSG.squish
|
|
92
|
-
Unable to use #{item.inspect} as an lquery item. Items are labels,
|
|
93
|
-
a Range for a quantified star, or an Array of alternatives.
|
|
94
|
-
MSG
|
|
95
|
-
end
|
|
73
|
+
def compile_level(text)
|
|
74
|
+
match = text.match(LEVEL)
|
|
75
|
+
variants = match[:variants]
|
|
76
|
+
label = variants == '*' ? '[^.]+' : compile_variants(variants.split('|'))
|
|
77
|
+
label = "(?!#{label}(?:\\.|\\z))[^.]+" if match[:not]
|
|
78
|
+
"(?:(?:\\A|\\.)#{label})#{quantifier(match)}"
|
|
96
79
|
end
|
|
97
80
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
min = range.begin
|
|
102
|
-
max = range.end
|
|
103
|
-
max -= 1 if max && range.exclude_end?
|
|
104
|
-
|
|
105
|
-
invalid = min&.negative? || max&.negative? || (min && max && max < min)
|
|
106
|
-
raise ArgumentError, <<~MSG.squish if invalid
|
|
107
|
-
#{range.inspect} is not a valid quantifier for an lquery star.
|
|
108
|
-
MSG
|
|
109
|
-
|
|
110
|
-
return '*' if min.nil? && max.nil?
|
|
111
|
-
return "*{#{min}}" if min == max
|
|
112
|
-
return "*{#{min},}" if max.nil?
|
|
113
|
-
return "*{,#{max}}" if min.nil?
|
|
114
|
-
"*{#{min},#{max}}"
|
|
81
|
+
def compile_variants(variants)
|
|
82
|
+
sources = variants.map { |variant| compile_variant(variant) }
|
|
83
|
+
"(?:#{sources.join('|')})"
|
|
115
84
|
end
|
|
116
85
|
|
|
117
|
-
def
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
86
|
+
def compile_variant(text)
|
|
87
|
+
match = text.match(VARIANT)
|
|
88
|
+
modifiers = match[:modifiers]
|
|
89
|
+
prefix = modifiers.include?('*')
|
|
90
|
+
name = match[:name]
|
|
121
91
|
|
|
122
|
-
|
|
123
|
-
|
|
92
|
+
source = modifiers.include?('%') ? words(name, prefix) : label(name, prefix)
|
|
93
|
+
modifiers.include?('@') ? "(?i:#{source})" : source
|
|
124
94
|
end
|
|
125
95
|
|
|
126
|
-
def
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
raise ArgumentError, <<~MSG.squish unless alternative.match?(/\A#{ALTERNATIVE}\z/)
|
|
130
|
-
#{entry.inspect} is not a valid lquery alternative. Alternatives are
|
|
131
|
-
single labels, optionally followed by the #{'@*%'.inspect} modifiers.
|
|
132
|
-
MSG
|
|
133
|
-
|
|
134
|
-
alternative
|
|
96
|
+
def label(name, prefix)
|
|
97
|
+
"#{Regexp.escape(name)}#{'[^.]*' if prefix}"
|
|
135
98
|
end
|
|
136
99
|
|
|
137
|
-
def
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
head, quantifier = split_quantifier(text)
|
|
142
|
-
item = sanitize_labels(head) + quantifier
|
|
143
|
-
|
|
144
|
-
raise ArgumentError, <<~MSG.squish unless item.match?(ITEM)
|
|
145
|
-
#{entry.inspect} is not a valid lquery item. An item is a label with
|
|
146
|
-
the optional #{'@*%'.inspect} modifiers, optionally negated with "!",
|
|
147
|
-
alternated with "|" and quantified with "{n,m}".
|
|
148
|
-
MSG
|
|
149
|
-
|
|
150
|
-
item
|
|
151
|
-
end
|
|
152
|
-
|
|
153
|
-
def assert_plain!(entry)
|
|
154
|
-
entry = LTree.resolve_record(entry)
|
|
155
|
-
plain = entry.is_a?(::String) || entry.is_a?(::Symbol) || entry.is_a?(::Numeric)
|
|
156
|
-
raise ArgumentError, <<~MSG.squish unless plain
|
|
157
|
-
Unable to use #{entry.inspect} as part of an lquery item.
|
|
158
|
-
MSG
|
|
159
|
-
|
|
160
|
-
entry.to_s
|
|
161
|
-
end
|
|
100
|
+
def words(name, prefix)
|
|
101
|
+
lookaheads = name.split('_').reject(&:empty?).map do |word|
|
|
102
|
+
"(?=(?:[^.]*_)?#{Regexp.escape(word)}#{'[^._]*' if prefix}(?:[_.]|\\z))"
|
|
103
|
+
end
|
|
162
104
|
|
|
163
|
-
|
|
164
|
-
# the structure of the pattern nor the digits of a quantifier
|
|
165
|
-
def sanitize_labels(text)
|
|
166
|
-
text.gsub(LABEL) { |label| LTree.sanitize(label) }
|
|
105
|
+
"#{lookaheads.join}[^.]+"
|
|
167
106
|
end
|
|
168
107
|
|
|
169
|
-
def
|
|
170
|
-
match
|
|
171
|
-
return [text, ''] if match.nil?
|
|
108
|
+
def quantifier(match)
|
|
109
|
+
return match[:variants] == '*' ? '*' : '' if match[:min].nil?
|
|
172
110
|
|
|
173
|
-
[
|
|
111
|
+
min = match[:min].presence || 0
|
|
112
|
+
max = match[:comma] ? match[:max] : min
|
|
113
|
+
"{#{min},#{max}}"
|
|
174
114
|
end
|
|
175
115
|
end
|
|
176
116
|
end
|
|
@@ -3,23 +3,21 @@
|
|
|
3
3
|
module Torque
|
|
4
4
|
module PostgreSQL
|
|
5
5
|
module Attributes
|
|
6
|
-
# A label path, as stored by the +ltree+ data type. It
|
|
7
|
-
# labels, so it
|
|
8
|
-
#
|
|
9
|
-
class LTree
|
|
10
|
-
|
|
6
|
+
# A label path, as stored by the +ltree+ data type. It holds the plain
|
|
7
|
+
# list of labels, so it enumerates like any other list, and an +ltree[]+
|
|
8
|
+
# column simply becomes an Array of these
|
|
9
|
+
class LTree
|
|
10
|
+
include Enumerable
|
|
11
11
|
|
|
12
|
+
attr_reader :items
|
|
13
|
+
|
|
14
|
+
delegate :inspect, to: :to_s
|
|
15
|
+
delegate :each, :size, :empty?, :[], to: :items
|
|
12
16
|
alias depth size
|
|
13
17
|
|
|
14
18
|
class << self
|
|
15
|
-
def [](*
|
|
16
|
-
new(
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
# Values coming from the database are valid by construction, so they
|
|
20
|
-
# skip both the normalization and the validation
|
|
21
|
-
def load(value)
|
|
22
|
-
new(value.to_s.split('.'), normalize: false)
|
|
19
|
+
def [](*items)
|
|
20
|
+
new(items)
|
|
23
21
|
end
|
|
24
22
|
|
|
25
23
|
# Whether the object knows how to describe itself as a path, which is
|
|
@@ -34,44 +32,24 @@ module Torque
|
|
|
34
32
|
method = PostgreSQL.config.ltree.compatible_method
|
|
35
33
|
value.public_send(method) if compatible?(value)
|
|
36
34
|
end
|
|
35
|
+
end
|
|
37
36
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def resolve_record(value)
|
|
42
|
-
return value unless value.is_a?(::ActiveRecord::Base)
|
|
43
|
-
|
|
44
|
-
id = value.id
|
|
45
|
-
raise ArgumentError, <<~MSG.squish if id.nil?
|
|
46
|
-
Unable to use #{value.class.name} as a label because its
|
|
47
|
-
#{value.class.primary_key} is still empty.
|
|
48
|
-
MSG
|
|
49
|
-
|
|
50
|
-
raise ArgumentError, <<~MSG.squish if id.is_a?(::Array)
|
|
51
|
-
Unable to use #{value.class.name} as a label because it has a
|
|
52
|
-
composite primary key, which cannot be a single label.
|
|
53
|
-
MSG
|
|
54
|
-
|
|
55
|
-
id
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
# Apply the configured replacements, so callers can feed a source that
|
|
59
|
-
# does not satisfy PostgreSQL's rules for a label on its own
|
|
60
|
-
def sanitize(value)
|
|
61
|
-
replacements = PostgreSQL.config.ltree.sanitize
|
|
62
|
-
return value if replacements.blank?
|
|
37
|
+
def initialize(value = nil, sanitize: true)
|
|
38
|
+
@items = type.items_for(value, sanitize: sanitize).freeze
|
|
39
|
+
end
|
|
63
40
|
|
|
64
|
-
|
|
65
|
-
|
|
41
|
+
def to_s
|
|
42
|
+
type.serialize(self)
|
|
66
43
|
end
|
|
67
44
|
|
|
68
|
-
def
|
|
69
|
-
|
|
70
|
-
|
|
45
|
+
def ==(other)
|
|
46
|
+
other = other.items if other.is_a?(self.class)
|
|
47
|
+
other.is_a?(::Array) && items == other
|
|
71
48
|
end
|
|
49
|
+
alias eql? ==
|
|
72
50
|
|
|
73
|
-
def
|
|
74
|
-
|
|
51
|
+
def hash
|
|
52
|
+
items.hash
|
|
75
53
|
end
|
|
76
54
|
|
|
77
55
|
def root?
|
|
@@ -79,24 +57,23 @@ module Torque
|
|
|
79
57
|
end
|
|
80
58
|
|
|
81
59
|
def root
|
|
82
|
-
self.class.new(first,
|
|
60
|
+
self.class.new(items.first(1), sanitize: false)
|
|
83
61
|
end
|
|
84
62
|
|
|
85
|
-
# A path with a single label has no parent, and neither does an empty one
|
|
86
63
|
def parent
|
|
87
|
-
self.class.new(
|
|
64
|
+
self.class.new(items[0..-2], sanitize: false) unless root?
|
|
88
65
|
end
|
|
89
66
|
|
|
90
67
|
def /(other)
|
|
91
|
-
self.class.new(
|
|
68
|
+
self.class.new(items + self.class.new(other).items, sanitize: false)
|
|
92
69
|
end
|
|
93
70
|
|
|
94
71
|
alias_method :+, :/
|
|
95
72
|
|
|
96
73
|
# Same as the +@>+ operator, which includes the path itself
|
|
97
74
|
def ancestor_of?(other)
|
|
98
|
-
other = self.class.new(other)
|
|
99
|
-
size <= other.size && other.first(size) ==
|
|
75
|
+
other = self.class.new(other).items
|
|
76
|
+
size <= other.size && other.first(size) == items
|
|
100
77
|
end
|
|
101
78
|
alias covers? ancestor_of?
|
|
102
79
|
|
|
@@ -109,51 +86,26 @@ module Torque
|
|
|
109
86
|
# The longest common ancestor, which never includes the last label of
|
|
110
87
|
# any of the paths, exactly like PostgreSQL's own +lca+
|
|
111
88
|
def lca(*others)
|
|
112
|
-
paths = [self, *others].map { |path| self.class.new(path)[0..-2]
|
|
113
|
-
result = paths.shift
|
|
89
|
+
paths = [self, *others].map { |path| self.class.new(path).items[0..-2] }
|
|
90
|
+
result = paths.shift
|
|
114
91
|
paths.each { |path| result = common_prefix(result, path) }
|
|
115
|
-
self.class.new(result,
|
|
92
|
+
self.class.new(result, sanitize: false)
|
|
116
93
|
end
|
|
117
94
|
|
|
118
|
-
# The position where the given subpath starts, or -1 when it is not
|
|
119
|
-
# present. Named apart from +index+ so that Array's own contract, of
|
|
120
|
-
# returning +nil+ when the element is missing, stays intact
|
|
121
95
|
def index_of(subpath, offset = 0)
|
|
122
|
-
subpath = self.class.new(subpath)
|
|
96
|
+
subpath = self.class.new(subpath).items
|
|
123
97
|
offset += size if offset.negative?
|
|
124
98
|
return -1 if subpath.empty? || offset.negative?
|
|
125
99
|
|
|
126
100
|
range = offset..(size - subpath.size)
|
|
127
|
-
position = range.find { |i|
|
|
101
|
+
position = range.find { |i| items[i, subpath.size] == subpath }
|
|
128
102
|
position || -1
|
|
129
103
|
end
|
|
130
104
|
|
|
131
105
|
private
|
|
132
106
|
|
|
133
|
-
def
|
|
134
|
-
|
|
135
|
-
entries.each { |label| assert_valid_label!(label) }
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
def split_labels(value)
|
|
139
|
-
return normalized(self.class.compatible(value)) if self.class.compatible?(value)
|
|
140
|
-
|
|
141
|
-
value = self.class.resolve_record(value)
|
|
142
|
-
plain = value.is_a?(::String) || value.is_a?(::Symbol) || value.is_a?(::Numeric)
|
|
143
|
-
raise ArgumentError, <<~MSG.squish unless plain
|
|
144
|
-
Unable to use #{value.inspect} as part of an ltree path. A path is a
|
|
145
|
-
plain sequence of labels, so it accepts neither the alternatives nor
|
|
146
|
-
the quantifiers that only make sense on an lquery.
|
|
147
|
-
MSG
|
|
148
|
-
|
|
149
|
-
self.class.sanitize(value.to_s).split('.')
|
|
150
|
-
end
|
|
151
|
-
|
|
152
|
-
def assert_valid_label!(label)
|
|
153
|
-
raise ArgumentError, <<~MSG.squish unless label.match?(LABEL)
|
|
154
|
-
#{label.inspect} is not a valid ltree label. Labels are limited to
|
|
155
|
-
letters, numbers, underscores and dashes.
|
|
156
|
-
MSG
|
|
107
|
+
def type
|
|
108
|
+
Adapter::OID::Ltree.new
|
|
157
109
|
end
|
|
158
110
|
|
|
159
111
|
def common_prefix(one, other)
|
|
@@ -18,7 +18,15 @@ module Torque
|
|
|
18
18
|
|
|
19
19
|
class_methods do
|
|
20
20
|
delegate :distinct_on, :with, :itself_only, :expand_records, :join_series,
|
|
21
|
-
:buckets, to: :all
|
|
21
|
+
:buckets, :segments, to: :all
|
|
22
|
+
|
|
23
|
+
# The Arel node for a path into a document column, typed by the struct
|
|
24
|
+
# class that backs the column when there is one
|
|
25
|
+
def arel_property_of(column, *path)
|
|
26
|
+
type = Adapter::OID.unwrap(attribute_types[column.to_s])
|
|
27
|
+
handler = PredicateBuilder::StructHandler.new(predicate_builder)
|
|
28
|
+
handler.property_for(arel_table[column], type, path)
|
|
29
|
+
end
|
|
22
30
|
|
|
23
31
|
# Composite values are objects that can be invalid on their own, so the
|
|
24
32
|
# attributes backed by one are validated alongside the record
|
|
@@ -38,6 +38,9 @@ module Torque
|
|
|
38
38
|
# Add support for querying and calculating histogram buckets
|
|
39
39
|
config.buckets = true
|
|
40
40
|
|
|
41
|
+
# Add support for segmenting calculations by named conditions
|
|
42
|
+
config.segments = true
|
|
43
|
+
|
|
41
44
|
# Set a list of irregular model name when associated with table names
|
|
42
45
|
config.irregular_models = {}
|
|
43
46
|
def config.irregular_models=(hash)
|
|
@@ -309,8 +312,8 @@ module Torque
|
|
|
309
312
|
ltree.enabled = true
|
|
310
313
|
|
|
311
314
|
# A hash of replacements applied to every label provided by the
|
|
312
|
-
# application before it
|
|
313
|
-
# satisfy PostgreSQL's rules on its own can still be used. Dashes, for
|
|
315
|
+
# application before it is sent to the database, so that a source that
|
|
316
|
+
# does not satisfy PostgreSQL's rules on its own can still be used. Dashes, for
|
|
314
317
|
# example, are only accepted as of PostgreSQL 16
|
|
315
318
|
# { '-' => '_' } turns dashes into underscores
|
|
316
319
|
# { '-' => '' } drops dashes
|
|
@@ -411,7 +414,7 @@ module Torque
|
|
|
411
414
|
|
|
412
415
|
# Define the list of commands that are going to be versioned by this
|
|
413
416
|
# method
|
|
414
|
-
vs.types = %i[function type view]
|
|
417
|
+
vs.types = %i[function type view trigger]
|
|
415
418
|
|
|
416
419
|
# The name of the table that will inherit from +schema_migrations+ and
|
|
417
420
|
# store the list of versioned commands that have been executed
|
|
@@ -419,5 +422,20 @@ module Torque
|
|
|
419
422
|
|
|
420
423
|
end
|
|
421
424
|
|
|
425
|
+
# Configure cross join features
|
|
426
|
+
config.nested(:cross_join) do |cross_join|
|
|
427
|
+
|
|
428
|
+
# This is an experimental feature that developers must explicitly opt-in.
|
|
429
|
+
# TODO: hook into how conditions and columns are resolved, so that the
|
|
430
|
+
# name of a joined set can shadow a real column
|
|
431
|
+
cross_join.enabled = false
|
|
432
|
+
|
|
433
|
+
# The name of the column added by +with_ordinality+ when it is given
|
|
434
|
+
# +true+ instead of a name. It matches what PostgreSQL calls the column
|
|
435
|
+
# when it is not named at all
|
|
436
|
+
cross_join.ordinality_column = :ordinality
|
|
437
|
+
|
|
438
|
+
end
|
|
439
|
+
|
|
422
440
|
end
|
|
423
441
|
end
|
|
@@ -24,8 +24,7 @@ module Torque
|
|
|
24
24
|
case value
|
|
25
25
|
when ::String
|
|
26
26
|
return if value.blank?
|
|
27
|
-
value.gsub
|
|
28
|
-
build_klass(*value.split(','))
|
|
27
|
+
build_klass(*value.gsub(DESTRUCTOR, '').split(','))
|
|
29
28
|
when ::Hash
|
|
30
29
|
build_klass(*value.symbolize_keys.slice(*pieces).values)
|
|
31
30
|
when ::Array
|
|
@@ -49,8 +48,8 @@ module Torque
|
|
|
49
48
|
parts = parts&.compact&.flatten
|
|
50
49
|
return if parts.blank?
|
|
51
50
|
|
|
52
|
-
|
|
53
|
-
format(formation, *parts.
|
|
51
|
+
parts = ::Array.new(pieces.size) { |index| parts[index] }
|
|
52
|
+
format(formation, *parts.map(&number_serializer))
|
|
54
53
|
end
|
|
55
54
|
|
|
56
55
|
def deserialize(value)
|
|
@@ -81,13 +80,8 @@ module Torque
|
|
|
81
80
|
|
|
82
81
|
def build_klass(*args)
|
|
83
82
|
return nil if args.empty?
|
|
84
|
-
check_invalid_format!(args)
|
|
85
83
|
|
|
86
|
-
config_class.new(*args.
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
def check_invalid_format!(args)
|
|
90
|
-
raise 'Invalid format' if args.size < pieces.size
|
|
84
|
+
config_class.new(*args.first(pieces.size).map(&:to_f))
|
|
91
85
|
end
|
|
92
86
|
end
|
|
93
87
|
end
|
|
@@ -45,7 +45,7 @@ module Torque
|
|
|
45
45
|
# Check if the model's table depends on any inheritance
|
|
46
46
|
def physically_inherited?
|
|
47
47
|
inheritance_cache(:@physically_inherited) do
|
|
48
|
-
|
|
48
|
+
connection_pool.schema_cache.dependencies(
|
|
49
49
|
defined?(@table_name) ? @table_name : decorated_table_name,
|
|
50
50
|
).present?
|
|
51
51
|
end
|
|
@@ -56,7 +56,7 @@ module Torque
|
|
|
56
56
|
# Get the list of all tables directly or indirectly dependent of the
|
|
57
57
|
# current one
|
|
58
58
|
def inheritance_dependents
|
|
59
|
-
|
|
59
|
+
connection_pool.schema_cache.associations(table_name) || []
|
|
60
60
|
end
|
|
61
61
|
|
|
62
62
|
# Check whether the model's table has directly or indirectly dependents
|
|
@@ -100,7 +100,7 @@ module Torque
|
|
|
100
100
|
def casted_dependents
|
|
101
101
|
inheritance_cache(:@casted_dependents) do
|
|
102
102
|
inheritance_dependents.map do |table_name|
|
|
103
|
-
[table_name,
|
|
103
|
+
[table_name, connection_pool.schema_cache.lookup_model(table_name)]
|
|
104
104
|
end.to_h.freeze
|
|
105
105
|
end
|
|
106
106
|
end
|
|
@@ -121,8 +121,8 @@ module Torque
|
|
|
121
121
|
table = super
|
|
122
122
|
|
|
123
123
|
adapter = ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
|
|
124
|
-
if Torque::PostgreSQL.config.eager_load &&
|
|
125
|
-
|
|
124
|
+
if Torque::PostgreSQL.config.eager_load && connection_pool.db_config.adapter_class <= adapter
|
|
125
|
+
connection_pool.schema_cache.add_model_name(table, self)
|
|
126
126
|
end
|
|
127
127
|
|
|
128
128
|
table
|
|
@@ -12,13 +12,17 @@ module Torque
|
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
def call_for_array(attribute, value)
|
|
15
|
-
if
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
15
|
+
return attribute.eq(nil) if value.nil?
|
|
16
|
+
|
|
17
|
+
value = value.to_a if value.is_a?(::Set)
|
|
18
|
+
return call_with_value(attribute, value) unless value.is_a?(::Array)
|
|
19
|
+
return call_with_empty(attribute) if value.empty?
|
|
20
|
+
|
|
21
|
+
nils, values = value.map { |entry| id_of(entry) }.partition(&:nil?)
|
|
22
|
+
return attribute.eq(nil) if values.empty?
|
|
23
|
+
|
|
24
|
+
condition = call_with_array(attribute, values)
|
|
25
|
+
nils.empty? ? condition : condition.or(attribute.eq(nil))
|
|
22
26
|
end
|
|
23
27
|
|
|
24
28
|
private
|
|
@@ -35,8 +39,12 @@ module Torque
|
|
|
35
39
|
FN.cardinality(attribute).eq(0)
|
|
36
40
|
end
|
|
37
41
|
|
|
42
|
+
def id_of(entry)
|
|
43
|
+
entry.is_a?(::ActiveRecord::Base) ? entry.id : entry
|
|
44
|
+
end
|
|
45
|
+
|
|
38
46
|
def array_attribute?(attribute)
|
|
39
|
-
attribute.type_caster.is_a?(ARRAY_OID)
|
|
47
|
+
Adapter::OID.unwrap(attribute.type_caster).is_a?(ARRAY_OID)
|
|
40
48
|
end
|
|
41
49
|
end
|
|
42
50
|
|
|
@@ -8,14 +8,15 @@ module Torque
|
|
|
8
8
|
# whole +where+ vocabulary works inside a composite
|
|
9
9
|
class CompositeHandler
|
|
10
10
|
class << self
|
|
11
|
-
# Values that this handler knows how to deal with
|
|
12
|
-
#
|
|
13
|
-
def candidate?(
|
|
11
|
+
# Values that this handler knows how to deal with. Without a value,
|
|
12
|
+
# only a single composite has parts that can be reached on their own
|
|
13
|
+
def candidate?(type, value = nil)
|
|
14
14
|
return false unless Adapter::OID::Composite.from(type)
|
|
15
|
+
return !type.is_a?(ARRAY_OID) if value.nil?
|
|
15
16
|
|
|
16
17
|
case value
|
|
17
18
|
when ::Hash, Attributes::Composite then true
|
|
18
|
-
when ::Array then value.any? { |entry| candidate?(
|
|
19
|
+
when ::Array then value.compact.any? { |entry| candidate?(type, entry) }
|
|
19
20
|
else false
|
|
20
21
|
end
|
|
21
22
|
end
|
|
@@ -41,6 +42,13 @@ module Torque
|
|
|
41
42
|
conditions_for(attribute, value)
|
|
42
43
|
end
|
|
43
44
|
|
|
45
|
+
# The columns of the composite value as the columns of a table, so that
|
|
46
|
+
# any of them can be resolved by name
|
|
47
|
+
def table_for(source, type)
|
|
48
|
+
@composite = Adapter::OID::Composite.from(type)
|
|
49
|
+
PredicateTable.new(self, source, Arel::Nodes::Column)
|
|
50
|
+
end
|
|
51
|
+
|
|
44
52
|
# The type of a single column of the composite type
|
|
45
53
|
def type_of(name)
|
|
46
54
|
columns = @composite.columns
|
|
@@ -65,6 +73,11 @@ module Torque
|
|
|
65
73
|
name = attribute.name
|
|
66
74
|
cast = composite.name
|
|
67
75
|
|
|
76
|
+
if !array && value.is_a?(::Array)
|
|
77
|
+
records = value.map { |entry| FN.bind(name, entry, composite).pg_cast(cast) }
|
|
78
|
+
return attribute.in(records)
|
|
79
|
+
end
|
|
80
|
+
|
|
68
81
|
return attribute.eq(FN.bind(name, value, composite).pg_cast(cast)) unless array
|
|
69
82
|
|
|
70
83
|
if value.is_a?(::Array)
|
|
@@ -99,7 +112,7 @@ module Torque
|
|
|
99
112
|
|
|
100
113
|
def group_for(source, entry)
|
|
101
114
|
entry = entry.to_h unless entry.is_a?(::Hash)
|
|
102
|
-
table =
|
|
115
|
+
table = table_for(source, composite)
|
|
103
116
|
nodes = predicate_builder.with(table).build_from_hash(entry.stringify_keys)
|
|
104
117
|
::Arel::Nodes::Grouping.new(nodes.reduce(:and))
|
|
105
118
|
end
|