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
|
@@ -3,21 +3,17 @@
|
|
|
3
3
|
module Torque
|
|
4
4
|
module PostgreSQL
|
|
5
5
|
module PredicateBuilder
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
6
|
+
# Serves every column of the ltree extension. A value of the same kind as
|
|
7
|
+
# the column is an equality, a value of the opposite kind is a match, and
|
|
8
|
+
# an Array is a list of values as soon as its first entry is a whole one
|
|
9
9
|
class LtreeHandler
|
|
10
10
|
class << self
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
# own path are claimed as well, otherwise a record would be reduced to
|
|
15
|
-
# its primary key before it had the chance to describe itself
|
|
16
|
-
def candidate?(value, type)
|
|
17
|
-
return false unless type.is_a?(Adapter::OID::Ltree)
|
|
11
|
+
def candidate?(type)
|
|
12
|
+
column_of(type).is_a?(Adapter::OID::Ltree)
|
|
13
|
+
end
|
|
18
14
|
|
|
19
|
-
|
|
20
|
-
|
|
15
|
+
def column_of(type)
|
|
16
|
+
type.is_a?(ARRAY_OID) ? type.subtype : type
|
|
21
17
|
end
|
|
22
18
|
end
|
|
23
19
|
|
|
@@ -26,17 +22,89 @@ module Torque
|
|
|
26
22
|
end
|
|
27
23
|
|
|
28
24
|
def call(attribute, type, value)
|
|
29
|
-
|
|
30
|
-
|
|
25
|
+
@attribute = attribute
|
|
26
|
+
@column = self.class.column_of(type)
|
|
27
|
+
@array = type.is_a?(ARRAY_OID)
|
|
28
|
+
@list = list?(value)
|
|
31
29
|
|
|
32
|
-
|
|
30
|
+
values = list ? value : [value]
|
|
31
|
+
pattern = values.any? { |entry| lquery.cast(entry)&.pattern? }
|
|
32
|
+
pattern == lquery_column? ? equality(values) : match(values)
|
|
33
33
|
end
|
|
34
34
|
|
|
35
35
|
private
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
attr_reader :attribute, :column, :array, :list
|
|
38
|
+
|
|
39
|
+
def list?(value)
|
|
40
|
+
return false unless value.is_a?(::Array)
|
|
41
|
+
|
|
42
|
+
first = value.first
|
|
43
|
+
value.empty? || first.is_a?(::Array) || first.is_a?(LTree) || first.is_a?(LQuery)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def lquery_column?
|
|
47
|
+
column.is_a?(Adapter::OID::Lquery)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def lquery
|
|
51
|
+
Adapter::OID::Lquery.new
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def ltree
|
|
55
|
+
Adapter::OID::Ltree.new
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Bound as the value the type produces, since Rails deep dups a raw
|
|
59
|
+
# value before compiling the query, and a dup of a record has no id
|
|
60
|
+
def bind(value, type)
|
|
61
|
+
FN.bind(attribute.name, type.cast(value), type)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# PostgreSQL defines no equality operator for lquery, so a pattern
|
|
65
|
+
# column is compared by its text form
|
|
66
|
+
def equality(values)
|
|
67
|
+
target = lquery_column? ? attribute.pg_cast('text', array) : attribute
|
|
68
|
+
return single_equality(target, bind(values.first, column)) unless list
|
|
69
|
+
return empty_equality(target) if values.empty?
|
|
70
|
+
return target.overlaps(bind(values, ARRAY_OID.new(column))) if array
|
|
71
|
+
|
|
72
|
+
binds = values.map { |entry| bind(entry, column) }
|
|
73
|
+
target.in(binds)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def single_equality(target, bind)
|
|
77
|
+
return target.eq(bind) unless array
|
|
78
|
+
|
|
79
|
+
FN.infix(:"=", bind, FN.any(target))
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def empty_equality(target)
|
|
83
|
+
return target.in([]) unless array
|
|
84
|
+
|
|
85
|
+
FN.cardinality(attribute).eq(0)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def match(values)
|
|
89
|
+
other = lquery_column? ? ltree : lquery
|
|
90
|
+
cast = other.type.to_s
|
|
91
|
+
return single_match(bind(values.first, other).pg_cast(cast)) unless list
|
|
92
|
+
|
|
93
|
+
list_match(bind(values, ARRAY_OID.new(other)).pg_cast(cast, true))
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# PostgreSQL defines ~ for every pair except lquery[] against ltree
|
|
97
|
+
def single_match(bind)
|
|
98
|
+
return attribute.matches_lquery(bind) unless array && lquery_column?
|
|
99
|
+
|
|
100
|
+
FN.infix(:"~", bind, FN.any(attribute))
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# PostgreSQL defines ? for every pair except lquery against ltree[]
|
|
104
|
+
def list_match(binds)
|
|
105
|
+
return attribute.matches_any_lquery(binds) if array || !lquery_column?
|
|
106
|
+
|
|
107
|
+
FN.infix(:"~", attribute, FN.any(binds))
|
|
40
108
|
end
|
|
41
109
|
end
|
|
42
110
|
end
|
|
@@ -13,10 +13,12 @@ module Torque
|
|
|
13
13
|
|
|
14
14
|
class << self
|
|
15
15
|
# Only a hash describes properties, everything else is left alone so
|
|
16
|
-
# that whole documents are still compared as documents
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
# that whole documents are still compared as documents. Without a
|
|
17
|
+
# value, a single document has properties that can be reached
|
|
18
|
+
def candidate?(type, value = nil)
|
|
19
|
+
return false if !type.is_a?(Adapter::OID::Struct) || type.is_a?(Adapter::OID::StructList)
|
|
20
|
+
|
|
21
|
+
value.nil? || value.is_a?(::Hash)
|
|
20
22
|
end
|
|
21
23
|
end
|
|
22
24
|
|
|
@@ -25,21 +27,39 @@ module Torque
|
|
|
25
27
|
end
|
|
26
28
|
|
|
27
29
|
def call(attribute, struct, value)
|
|
28
|
-
|
|
30
|
+
table = table_for(attribute, struct)
|
|
31
|
+
nodes = predicate_builder.with(table).build_from_hash(value.stringify_keys)
|
|
32
|
+
|
|
33
|
+
::Arel::Nodes::Grouping.new(nodes.reduce(:and))
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# The properties of the document as the columns of a table, so that any
|
|
37
|
+
# of them can be resolved by name. A document that no class describes
|
|
38
|
+
# is open, which means any path into it is accepted, without a type
|
|
39
|
+
def table_for(attribute, struct)
|
|
40
|
+
raise ArgumentError, <<~MSG.squish if struct.is_a?(Adapter::OID::Struct) && struct.type != :jsonb
|
|
29
41
|
Unable to build a condition over "#{attribute.name}" because json
|
|
30
42
|
columns cannot be queried by their properties. Use jsonb instead.
|
|
31
43
|
MSG
|
|
32
44
|
|
|
33
45
|
@struct = struct
|
|
34
|
-
|
|
35
|
-
|
|
46
|
+
PredicateTable.new(self, attribute, Arel::Nodes::Property)
|
|
47
|
+
end
|
|
36
48
|
|
|
37
|
-
|
|
49
|
+
# The node for a path into the document, resolved one property at a
|
|
50
|
+
# time so that each level is typed by the class that declares it
|
|
51
|
+
def property_for(attribute, struct, path)
|
|
52
|
+
path.reduce(attribute) do |node, key|
|
|
53
|
+
struct = node.type_caster if node.is_a?(Arel::Nodes::Property)
|
|
54
|
+
table_for(node, struct)[key]
|
|
55
|
+
end
|
|
38
56
|
end
|
|
39
57
|
|
|
40
58
|
# The type of a single property, plus what it has to be casted to once
|
|
41
59
|
# it is extracted from the document as text
|
|
42
60
|
def type_of(name)
|
|
61
|
+
return [ActiveModel::Type.default_value, nil] unless @struct.is_a?(Adapter::OID::Struct)
|
|
62
|
+
|
|
43
63
|
klass = @struct.klass
|
|
44
64
|
name = name.to_s
|
|
45
65
|
|
|
@@ -60,7 +80,7 @@ module Torque
|
|
|
60
80
|
name = type&.type
|
|
61
81
|
return if name.nil? || PLAIN_TYPES.include?(name)
|
|
62
82
|
|
|
63
|
-
ActiveRecord::Base.
|
|
83
|
+
ActiveRecord::Base.with_connection { |c| c.type_to_sql(name) }
|
|
64
84
|
end
|
|
65
85
|
end
|
|
66
86
|
end
|
|
@@ -14,6 +14,20 @@ module Torque
|
|
|
14
14
|
module PredicateBuilder
|
|
15
15
|
ARRAY_OID = ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array
|
|
16
16
|
|
|
17
|
+
# The handler that serves a column, either to build a condition with the
|
|
18
|
+
# given value or, without one, to resolve the parts of the column. The
|
|
19
|
+
# ltree handler is picked by the column alone, composite and struct also
|
|
20
|
+
# look at the value: a Hash means the parts, anything else the whole
|
|
21
|
+
def self.handler_for(type, value = nil)
|
|
22
|
+
if PostgreSQL.config.composite.enabled && CompositeHandler.candidate?(type, value)
|
|
23
|
+
CompositeHandler
|
|
24
|
+
elsif PostgreSQL.config.struct.enabled && StructHandler.candidate?(type, value)
|
|
25
|
+
StructHandler
|
|
26
|
+
elsif PostgreSQL.config.ltree.enabled && !value.nil? && LtreeHandler.candidate?(type)
|
|
27
|
+
LtreeHandler
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
17
31
|
def initialize(*)
|
|
18
32
|
super
|
|
19
33
|
|
|
@@ -34,25 +48,38 @@ module Torque
|
|
|
34
48
|
|
|
35
49
|
# Values described as a hash of columns or properties are turned into
|
|
36
50
|
# conditions over the column that holds them. Whole values are left to
|
|
37
|
-
# the regular handlers, so that they are compared as a single value
|
|
51
|
+
# the regular handlers, so that they are compared as a single value, and
|
|
52
|
+
# so is nil, which is a null check on every kind of column
|
|
38
53
|
def build(attribute, value, operator = nil)
|
|
39
|
-
|
|
40
|
-
|
|
54
|
+
return super if value.nil?
|
|
55
|
+
|
|
56
|
+
type = Adapter::OID.unwrap(table.type(attribute.name))
|
|
57
|
+
handler = PredicateBuilder.handler_for(type, value) || handler_for_path(attribute, value)
|
|
41
58
|
return super if handler.nil?
|
|
42
59
|
|
|
43
60
|
handler.new(self).call(attribute, type, value)
|
|
44
61
|
end
|
|
45
62
|
|
|
63
|
+
# A struct or composite column stands for the table of its own parts, so
|
|
64
|
+
# a Hash given to order, group or pluck reaches a part the way where does
|
|
65
|
+
def resolve_arel_attribute(table_name, column_name, &block)
|
|
66
|
+
return super unless table.has_column?(table_name)
|
|
67
|
+
|
|
68
|
+
type = Adapter::OID.unwrap(table.type(table_name))
|
|
69
|
+
handler = PredicateBuilder.handler_for(type)
|
|
70
|
+
return super if handler.nil?
|
|
71
|
+
|
|
72
|
+
handler.new(self).table_for(table.arel_table[table_name], type)[column_name]
|
|
73
|
+
end
|
|
74
|
+
|
|
46
75
|
private
|
|
47
76
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
LtreeHandler
|
|
55
|
-
end
|
|
77
|
+
# A Hash beneath a property that is not a document of its own is just
|
|
78
|
+
# a deeper path, the way jsonb reads it, which is how an entry of an
|
|
79
|
+
# array is reached
|
|
80
|
+
def handler_for_path(attribute, value)
|
|
81
|
+
return unless PostgreSQL.config.struct.enabled
|
|
82
|
+
StructHandler if attribute.is_a?(Arel::Nodes::Property) && value.is_a?(::Hash)
|
|
56
83
|
end
|
|
57
84
|
end
|
|
58
85
|
|
|
@@ -30,6 +30,20 @@ module Torque
|
|
|
30
30
|
Relation.include(Relation::Buckets)
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
+
if torque_config.segments
|
|
34
|
+
require_relative 'relation/segments'
|
|
35
|
+
Relation.include(Relation::Segments)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
## Cross Join Setup
|
|
39
|
+
if torque_config.cross_join.enabled
|
|
40
|
+
require_relative 'relation/cross_join'
|
|
41
|
+
Relation.include(Relation::CrossJoin)
|
|
42
|
+
|
|
43
|
+
# TODO: delegate join_struct and join_composite once they exist
|
|
44
|
+
ActiveRecord::Base.singleton_class.delegate(:cross_join, to: :all)
|
|
45
|
+
end
|
|
46
|
+
|
|
33
47
|
## Schemas Enabled Setup
|
|
34
48
|
if (config = torque_config.schemas).enabled
|
|
35
49
|
require_relative 'adapter/schema_overrides'
|
|
@@ -141,6 +155,9 @@ module Torque
|
|
|
141
155
|
require_relative 'adapter/oid/ltree'
|
|
142
156
|
require_relative 'adapter/oid/lquery'
|
|
143
157
|
|
|
158
|
+
require_relative 'attributes/ltree'
|
|
159
|
+
require_relative 'attributes/lquery'
|
|
160
|
+
|
|
144
161
|
ar_type.register(:ltree, Adapter::OID::Ltree, adapter: :postgresql)
|
|
145
162
|
ar_type.register(:lquery, Adapter::OID::Lquery, adapter: :postgresql)
|
|
146
163
|
end
|
|
@@ -32,11 +32,15 @@ module Torque
|
|
|
32
32
|
# Get all auxiliary statements bound attributes and the base bound
|
|
33
33
|
# attributes as well
|
|
34
34
|
def bound_attributes
|
|
35
|
-
|
|
36
|
-
visitor.accept(self.arel.ast, ::Arel::Collectors::Composite.new(
|
|
35
|
+
collector = ::Arel::Collectors::Composite.new(
|
|
37
36
|
::Arel::Collectors::SQLString.new,
|
|
38
37
|
::Arel::Collectors::Bind.new,
|
|
39
|
-
)
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
klass.with_connection do |connection|
|
|
41
|
+
visitor = ::Arel::Visitors::PostgreSQL.new(connection)
|
|
42
|
+
visitor.accept(arel.ast, collector).value.last
|
|
43
|
+
end
|
|
40
44
|
end
|
|
41
45
|
|
|
42
46
|
private
|
|
@@ -34,6 +34,7 @@ module Torque
|
|
|
34
34
|
MSG
|
|
35
35
|
|
|
36
36
|
count ||= 1 if values.is_a?(Range)
|
|
37
|
+
attribute = resolve_column(attribute).first if attribute.is_a?(Hash)
|
|
37
38
|
attribute = arel_table[attribute] unless ::Arel.arel_node?(attribute)
|
|
38
39
|
self.buckets_value = [attribute, values, count, cast, as]
|
|
39
40
|
self
|
|
@@ -51,7 +52,7 @@ module Torque
|
|
|
51
52
|
|
|
52
53
|
keys = buckets_keys
|
|
53
54
|
self.group_values = [FN.group_by(build_buckets_node, :bucket)]
|
|
54
|
-
super.transform_keys { |key| keys[key - 1] }
|
|
55
|
+
super.transform_keys { |key| keys[key - 1] if key&.positive? }
|
|
55
56
|
end
|
|
56
57
|
|
|
57
58
|
module Initializer
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Torque
|
|
4
|
+
module PostgreSQL
|
|
5
|
+
module Relation
|
|
6
|
+
module CrossJoin
|
|
7
|
+
|
|
8
|
+
Entry = ::Struct.new(:node, :table)
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
# Assemble the join node and the table that stands for what it
|
|
12
|
+
# exposes, which is what makes the result reachable by name
|
|
13
|
+
def build(source, as, columns, mode, with_ordinality, definitions = nil)
|
|
14
|
+
columns = columns_for(source, columns)
|
|
15
|
+
ordinality = ordinality_for(with_ordinality)
|
|
16
|
+
validate_ordinality!(ordinality, columns, definitions)
|
|
17
|
+
|
|
18
|
+
source, lateral = source_for(source, columns)
|
|
19
|
+
item = Arel::Nodes::FromItem.new(source, as, columns: columns,
|
|
20
|
+
definitions: definitions, ordinality: ordinality, lateral: lateral)
|
|
21
|
+
|
|
22
|
+
Entry.new(join_for(mode, item), ::Arel::Table.new(as.to_s))
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
# A list of documents describes its own columns, everything else
|
|
28
|
+
# has to be told what they are
|
|
29
|
+
def columns_for(source, columns)
|
|
30
|
+
return columns if columns.present? || !source.is_a?(::Array)
|
|
31
|
+
|
|
32
|
+
sample = source.first
|
|
33
|
+
sample.keys if sample.is_a?(::Hash)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# The name of the counter column, which +true+ takes from the
|
|
37
|
+
# configuration
|
|
38
|
+
def ordinality_for(value)
|
|
39
|
+
case value
|
|
40
|
+
when true then PostgreSQL.config.cross_join.ordinality_column
|
|
41
|
+
when ::Symbol, ::String then value
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# PostgreSQL matches the alias list positionally, so a list that
|
|
46
|
+
# does not name every column silently renames the ones it reaches
|
|
47
|
+
# instead of naming the counter. Without knowing the columns, the
|
|
48
|
+
# only name that can be promised is the one PostgreSQL picks
|
|
49
|
+
def validate_ordinality!(ordinality, columns, definitions)
|
|
50
|
+
return if ordinality.blank? || columns.present? || definitions.present?
|
|
51
|
+
return if ordinality.to_s == 'ordinality'
|
|
52
|
+
|
|
53
|
+
raise ArgumentError, <<~MSG.squish
|
|
54
|
+
Unable to name the ordinality column "#{ordinality}" because the
|
|
55
|
+
columns of the source are unknown. Provide "columns:" so that
|
|
56
|
+
all of them can be named, or let it be called "ordinality".
|
|
57
|
+
MSG
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Only a subquery needs LATERAL, since it is a noise word in front
|
|
61
|
+
# of a function and PostgreSQL lets those see the row already
|
|
62
|
+
def source_for(source, columns)
|
|
63
|
+
case source
|
|
64
|
+
when ::ActiveRecord::Relation then [::Arel::Nodes::Grouping.new(source.arel.ast), true]
|
|
65
|
+
when ::Arel::SelectManager then [::Arel::Nodes::Grouping.new(source.ast), true]
|
|
66
|
+
when ::Array then [unnest_rows(source, columns), false]
|
|
67
|
+
when ::Arel::Nodes::Node then [source, false]
|
|
68
|
+
else
|
|
69
|
+
raise ArgumentError, <<~MSG.squish
|
|
70
|
+
Unable to join "#{source.class}". Provide an Arel node, a
|
|
71
|
+
relation, or an Array of rows.
|
|
72
|
+
MSG
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Rows become one array per column, which costs a single bind for
|
|
77
|
+
# each of them instead of one for every single value
|
|
78
|
+
def unnest_rows(rows, columns)
|
|
79
|
+
raise ArgumentError, 'Unable to join an empty list of rows.' if rows.empty?
|
|
80
|
+
|
|
81
|
+
raise ArgumentError, <<~MSG.squish if columns.blank?
|
|
82
|
+
Unable to name the columns of the given rows. Provide
|
|
83
|
+
"columns:" or use a Hash for each row.
|
|
84
|
+
MSG
|
|
85
|
+
|
|
86
|
+
keys = columns.map(&:to_sym)
|
|
87
|
+
values = rows.map { |row| row_values(row, keys) }
|
|
88
|
+
args = values.transpose.map { |list| ::Arel.array(list) }
|
|
89
|
+
FN.unnest(*args)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Hash rows are read by name, so that they can be given in any
|
|
93
|
+
# order, and their keys are compared as symbols because that is
|
|
94
|
+
# not something the caller should have to match
|
|
95
|
+
def row_values(row, keys)
|
|
96
|
+
return ::Array.wrap(row) unless row.is_a?(::Hash)
|
|
97
|
+
|
|
98
|
+
row.symbolize_keys.values_at(*keys)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# A lateral reference can only be combined with an inner or a left
|
|
102
|
+
# join, and a cross join is how an inner one without a condition
|
|
103
|
+
# has to be written
|
|
104
|
+
def join_for(mode, item)
|
|
105
|
+
case mode.to_sym
|
|
106
|
+
when :cross then Arel::Nodes::CrossJoin.new(item)
|
|
107
|
+
when :left
|
|
108
|
+
on = ::Arel::Nodes::On.new(::Arel::Nodes::True.new)
|
|
109
|
+
::Arel::Nodes::OuterJoin.new(item, on)
|
|
110
|
+
else
|
|
111
|
+
raise ArgumentError, <<~MSG.squish
|
|
112
|
+
The "#{mode}" is not implemented as a join type. PostgreSQL
|
|
113
|
+
only combines a lateral reference with :cross or :left.
|
|
114
|
+
MSG
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# :nodoc:
|
|
120
|
+
def cross_joins_value
|
|
121
|
+
@values.fetch(:cross_joins, ::ActiveRecord::QueryMethods::FROZEN_EMPTY_HASH)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# :nodoc:
|
|
125
|
+
def cross_joins_value=(value)
|
|
126
|
+
assert_modifiable!
|
|
127
|
+
@values[:cross_joins] = value
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Joins the query against a set produced by the given source, which
|
|
131
|
+
# can be an Arel node, another relation, or a list of rows. The result
|
|
132
|
+
# is reachable by the name given to +as+. For example:
|
|
133
|
+
#
|
|
134
|
+
# User.cross_join(FN.generate_series(1, 5), as: :sample, columns: %i[value])
|
|
135
|
+
# # Returns 5 rows for each user
|
|
136
|
+
#
|
|
137
|
+
# User.cross_join([{ id: 1, label: 'a' }], as: :labels)
|
|
138
|
+
# # Returns the rows of the list combined with each user
|
|
139
|
+
def cross_join(source, **xargs)
|
|
140
|
+
spawn.cross_join!(source, **xargs)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Like #cross_join, but modifies relation in place.
|
|
144
|
+
def cross_join!(source, as:, columns: nil, mode: :cross, with_ordinality: false)
|
|
145
|
+
entry = CrossJoin.build(source, as, columns, mode, with_ordinality)
|
|
146
|
+
self.cross_joins_value = cross_joins_value.merge(as.to_sym => entry)
|
|
147
|
+
self
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
private
|
|
151
|
+
|
|
152
|
+
# Hook arel build to add the joins, which is done here instead of
|
|
153
|
+
# through the join values because those are inspected by Rails for
|
|
154
|
+
# table aliases, and these sources have no table to inspect
|
|
155
|
+
def build_arel(*)
|
|
156
|
+
arel = super
|
|
157
|
+
return arel if cross_joins_value.blank?
|
|
158
|
+
|
|
159
|
+
nodes = cross_joins_value.each_value.map(&:node)
|
|
160
|
+
arel.join_sources.concat(nodes)
|
|
161
|
+
arel
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# The merger is loaded with the relation, so the part that knows about
|
|
165
|
+
# this feature is coordinated from here
|
|
166
|
+
module Merger
|
|
167
|
+
def merge # :nodoc:
|
|
168
|
+
result = super
|
|
169
|
+
merge_cross_joins
|
|
170
|
+
result
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
private
|
|
174
|
+
|
|
175
|
+
def merge_cross_joins
|
|
176
|
+
return unless relation.is_a?(Relation::CrossJoin)
|
|
177
|
+
return if other.cross_joins_value.blank?
|
|
178
|
+
|
|
179
|
+
relation.cross_joins_value =
|
|
180
|
+
relation.cross_joins_value.merge(other.cross_joins_value)
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
::ActiveRecord::Relation::Merger.prepend(CrossJoin::Merger)
|
|
187
|
+
|
|
188
|
+
::ActiveRecord::Relation::SINGLE_VALUE_METHODS.push(:cross_joins)
|
|
189
|
+
::ActiveRecord::Relation::VALUE_METHODS.push(:cross_joins)
|
|
190
|
+
::ActiveRecord::QueryMethods::VALID_UNSCOPING_VALUES.add(:cross_joins)
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
@@ -219,11 +219,10 @@ module Torque
|
|
|
219
219
|
# once per explicit select that merely omits the marker
|
|
220
220
|
def warn_about_missing_record_class(records)
|
|
221
221
|
return unless model.physically_inheritances?
|
|
222
|
+
return if itself_only_value === true
|
|
222
223
|
return if records.empty? || group_values.present? || distinct_value || select_values.empty?
|
|
223
224
|
return unless from_clause.empty?
|
|
224
|
-
return if itself_only_value === true
|
|
225
225
|
return if select_values.any? { |value| record_class_marker?(value) }
|
|
226
|
-
return if records.any? { |record| record.class != model }
|
|
227
226
|
|
|
228
227
|
warn(<<~MSG.squish)
|
|
229
228
|
#{model.name} was queried with an explicit select that omits
|
|
@@ -87,6 +87,7 @@ module Torque
|
|
|
87
87
|
|
|
88
88
|
return yield(result, relation.arel_table) if block_given?
|
|
89
89
|
|
|
90
|
+
with = relation.resolve_column(with).first if with.is_a?(Hash)
|
|
90
91
|
with = relation.arel_table[with.to_s] if with.is_a?(Symbol)
|
|
91
92
|
with = with.pg_cast(cast) if cast && with.respond_to?(:pg_cast)
|
|
92
93
|
|
|
@@ -13,6 +13,7 @@ module Torque
|
|
|
13
13
|
merge_auxiliary_statements
|
|
14
14
|
merge_inheritance
|
|
15
15
|
merge_buckets
|
|
16
|
+
merge_segments
|
|
16
17
|
|
|
17
18
|
relation
|
|
18
19
|
end
|
|
@@ -82,6 +83,14 @@ module Torque
|
|
|
82
83
|
relation.buckets_value = other.buckets_value
|
|
83
84
|
end
|
|
84
85
|
|
|
86
|
+
# Merge the named conditions used to segment calculations
|
|
87
|
+
def merge_segments
|
|
88
|
+
return unless defined?(Relation::Segments) && relation.is_a?(Relation::Segments)
|
|
89
|
+
return if other.segments_value.blank?
|
|
90
|
+
|
|
91
|
+
relation.segments_value = relation.segments_value.merge(other.segments_value)
|
|
92
|
+
end
|
|
93
|
+
|
|
85
94
|
end
|
|
86
95
|
|
|
87
96
|
ActiveRecord::Relation::Merger.prepend Merger
|