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,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Torque
|
|
4
|
+
module PostgreSQL
|
|
5
|
+
module PredicateBuilder
|
|
6
|
+
# Turns a hash of properties into a proper set of conditions over the
|
|
7
|
+
# document that holds them, handing each of them back to the predicate
|
|
8
|
+
# builder so that the whole +where+ vocabulary works inside a struct
|
|
9
|
+
class StructHandler
|
|
10
|
+
# Properties are extracted as text, so only the ones whose type has a
|
|
11
|
+
# counterpart on the database are casted before being compared
|
|
12
|
+
PLAIN_TYPES = %i[string text].freeze
|
|
13
|
+
|
|
14
|
+
class << self
|
|
15
|
+
# Only a hash describes properties, everything else is left alone so
|
|
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)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def initialize(predicate_builder)
|
|
26
|
+
@predicate_builder = predicate_builder
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def call(attribute, struct, value)
|
|
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
|
|
41
|
+
Unable to build a condition over "#{attribute.name}" because json
|
|
42
|
+
columns cannot be queried by their properties. Use jsonb instead.
|
|
43
|
+
MSG
|
|
44
|
+
|
|
45
|
+
@struct = struct
|
|
46
|
+
PredicateTable.new(self, attribute, Arel::Nodes::Property)
|
|
47
|
+
end
|
|
48
|
+
|
|
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
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# The type of a single property, plus what it has to be casted to once
|
|
59
|
+
# it is extracted from the document as text
|
|
60
|
+
def type_of(name)
|
|
61
|
+
return [ActiveModel::Type.default_value, nil] unless @struct.is_a?(Adapter::OID::Struct)
|
|
62
|
+
|
|
63
|
+
klass = @struct.klass
|
|
64
|
+
name = name.to_s
|
|
65
|
+
|
|
66
|
+
raise ArgumentError, <<~MSG.squish if klass.strict? && !klass.attribute_names.include?(name)
|
|
67
|
+
Unable to build a condition for "#{name}" because it is not a
|
|
68
|
+
declared property of #{klass.name}.
|
|
69
|
+
MSG
|
|
70
|
+
|
|
71
|
+
type = klass.attribute_types[name]
|
|
72
|
+
[type, cast_for(type)]
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
attr_reader :predicate_builder
|
|
78
|
+
|
|
79
|
+
def cast_for(type)
|
|
80
|
+
name = type&.type
|
|
81
|
+
return if name.nil? || PLAIN_TYPES.include?(name)
|
|
82
|
+
|
|
83
|
+
ActiveRecord::Base.with_connection { |c| c.type_to_sql(name) }
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative 'predicate_builder/array_handler'
|
|
4
4
|
|
|
5
|
+
require_relative 'predicate_builder/composite_handler'
|
|
6
|
+
require_relative 'predicate_builder/ltree_handler'
|
|
7
|
+
require_relative 'predicate_builder/struct_handler'
|
|
5
8
|
require_relative 'predicate_builder/regexp_handler'
|
|
6
9
|
require_relative 'predicate_builder/arel_attribute_handler'
|
|
7
10
|
require_relative 'predicate_builder/enumerator_lazy_handler'
|
|
@@ -11,6 +14,20 @@ module Torque
|
|
|
11
14
|
module PredicateBuilder
|
|
12
15
|
ARRAY_OID = ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array
|
|
13
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
|
+
|
|
14
31
|
def initialize(*)
|
|
15
32
|
super
|
|
16
33
|
|
|
@@ -28,6 +45,42 @@ module Torque
|
|
|
28
45
|
register_handler(::Arel::Attributes::Attribute, ArelAttributeHandler.new(self))
|
|
29
46
|
end
|
|
30
47
|
end
|
|
48
|
+
|
|
49
|
+
# Values described as a hash of columns or properties are turned into
|
|
50
|
+
# conditions over the column that holds them. Whole values are left to
|
|
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
|
|
53
|
+
def build(attribute, value, operator = nil)
|
|
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)
|
|
58
|
+
return super if handler.nil?
|
|
59
|
+
|
|
60
|
+
handler.new(self).call(attribute, type, value)
|
|
61
|
+
end
|
|
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
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
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)
|
|
83
|
+
end
|
|
31
84
|
end
|
|
32
85
|
|
|
33
86
|
::ActiveRecord::PredicateBuilder.prepend(PredicateBuilder)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Torque
|
|
4
|
+
module PostgreSQL
|
|
5
|
+
# Mimics ActiveRecord::TableMetadata so that the parts of a value that holds
|
|
6
|
+
# other values, like the columns of a composite type or the properties of a
|
|
7
|
+
# document, can be treated as the columns of the value they belong to. That
|
|
8
|
+
# is what allows each of them to be handed back to the predicate builder
|
|
9
|
+
#
|
|
10
|
+
# The handler is the one that knows how to resolve a name, and the node is
|
|
11
|
+
# the one that knows how to reach it
|
|
12
|
+
class PredicateTable
|
|
13
|
+
def initialize(handler, source, node)
|
|
14
|
+
@handler = handler
|
|
15
|
+
@source = source
|
|
16
|
+
@node = node
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def arel_table
|
|
20
|
+
self
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def [](name)
|
|
24
|
+
@node.new(@source, name, *@handler.type_of(name))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def type(name)
|
|
28
|
+
@handler.type_of(name).first
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Anything that gets here is meant to be resolved as a part of the value,
|
|
32
|
+
# which is what +type_of+ is there to accept or reject
|
|
33
|
+
def has_column?(*)
|
|
34
|
+
true
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def primary_key
|
|
38
|
+
nil
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Rails 8.1 renamed this, so both names answer for the supported versions
|
|
42
|
+
def associated_with(*)
|
|
43
|
+
false
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
alias associated_with? associated_with
|
|
47
|
+
|
|
48
|
+
def aggregated_with?(*)
|
|
49
|
+
false
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def polymorphic_association?
|
|
53
|
+
false
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def through_association?
|
|
57
|
+
false
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -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'
|
|
@@ -85,6 +99,32 @@ module Torque
|
|
|
85
99
|
end
|
|
86
100
|
end
|
|
87
101
|
|
|
102
|
+
## Struct Enabled Setup
|
|
103
|
+
if (config = torque_config.struct).enabled
|
|
104
|
+
require_relative 'adapter/oid/struct'
|
|
105
|
+
require_relative 'adapter/oid/struct_list'
|
|
106
|
+
require_relative 'adapter/oid/struct_set'
|
|
107
|
+
|
|
108
|
+
require_relative 'attributes/struct'
|
|
109
|
+
|
|
110
|
+
Attributes::Struct.include_on(ActiveRecord::Base)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
## Composite Enabled Setup
|
|
114
|
+
if (config = torque_config.composite).enabled
|
|
115
|
+
require_relative 'adapter/oid/composite'
|
|
116
|
+
require_relative 'attributes/composite'
|
|
117
|
+
|
|
118
|
+
ar_type.register(:composite, Adapter::OID::Composite, adapter: :postgresql)
|
|
119
|
+
|
|
120
|
+
config.namespace ||= ::Object.const_set('Composite', Module.new)
|
|
121
|
+
|
|
122
|
+
# Define a method to find composite classes based on the namespace
|
|
123
|
+
config.namespace.define_singleton_method(:const_missing) do |name|
|
|
124
|
+
Attributes::Composite.lookup(name)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
88
128
|
## Geometry Enabled Setup
|
|
89
129
|
if (config = torque_config.geometry).enabled
|
|
90
130
|
require_relative 'adapter/oid/box'
|
|
@@ -110,6 +150,18 @@ module Torque
|
|
|
110
150
|
ar_type.register(:interval, Adapter::OID::Interval, adapter: :postgresql)
|
|
111
151
|
end
|
|
112
152
|
|
|
153
|
+
## LTree Enabled Setup
|
|
154
|
+
if (config = torque_config.ltree).enabled
|
|
155
|
+
require_relative 'adapter/oid/ltree'
|
|
156
|
+
require_relative 'adapter/oid/lquery'
|
|
157
|
+
|
|
158
|
+
require_relative 'attributes/ltree'
|
|
159
|
+
require_relative 'attributes/lquery'
|
|
160
|
+
|
|
161
|
+
ar_type.register(:ltree, Adapter::OID::Ltree, adapter: :postgresql)
|
|
162
|
+
ar_type.register(:lquery, Adapter::OID::Lquery, adapter: :postgresql)
|
|
163
|
+
end
|
|
164
|
+
|
|
113
165
|
## Full Text Search Enabled Setup
|
|
114
166
|
if (config = torque_config.full_text_search).enabled
|
|
115
167
|
require_relative 'attributes/full_text_search'
|
|
@@ -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
|