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,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'simple_enum'
|
|
4
|
+
|
|
5
|
+
module Torque
|
|
6
|
+
module PostgreSQL
|
|
7
|
+
module Attributes
|
|
8
|
+
# The common ground between the classes that back a column with a set of
|
|
9
|
+
# typed properties, like structs and composite types. It carries enough of
|
|
10
|
+
# the ActiveRecord attribute surface for its own features, and for the
|
|
11
|
+
# ActiveRecord concerns that build on top of it, to work
|
|
12
|
+
class Base
|
|
13
|
+
include ActiveModel::Model
|
|
14
|
+
include ActiveModel::Attributes
|
|
15
|
+
include ActiveModel::Serialization
|
|
16
|
+
include ActiveModel::Serializers::JSON
|
|
17
|
+
include ActiveModel::Dirty
|
|
18
|
+
include ActiveModel::Validations::Callbacks
|
|
19
|
+
|
|
20
|
+
include SimpleEnum
|
|
21
|
+
include ActiveRecord::Store
|
|
22
|
+
include(AR810 ? ActiveModel::Attributes::Normalization : ActiveRecord::Normalization)
|
|
23
|
+
include ActiveRecord::Encryption::EncryptableRecord
|
|
24
|
+
|
|
25
|
+
class_attribute :filter_attributes, instance_accessor: false, default: []
|
|
26
|
+
|
|
27
|
+
class << self
|
|
28
|
+
# These classes are not backed by a table, so nothing here has the
|
|
29
|
+
# extra information that a column would provide
|
|
30
|
+
def columns_hash
|
|
31
|
+
{}
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Encrypt individual attributes, so their values are stored encrypted
|
|
35
|
+
# inside the column that holds them
|
|
36
|
+
def encrypts(*names, **options)
|
|
37
|
+
names.each do |name|
|
|
38
|
+
raise ArgumentError, <<~MSG.squish unless attribute_names.include?(name.to_s)
|
|
39
|
+
Unable to encrypt "#{name}" because it is not a declared
|
|
40
|
+
attribute of #{self.name}.
|
|
41
|
+
MSG
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
super
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def inspection_filter
|
|
48
|
+
ActiveSupport::ParameterFilter.new(filter_attributes)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def inspect
|
|
53
|
+
self.class.inspection_filter.filter(to_h).inspect
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Plain access to any property, declared or not
|
|
57
|
+
def [](key)
|
|
58
|
+
key = key.to_s
|
|
59
|
+
@attributes.key?(key) ? @attributes.fetch_value(key) : nil
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def []=(key, value)
|
|
63
|
+
assign_attributes(key => value)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def ==(other)
|
|
67
|
+
other.class == self.class && other.attributes == attributes
|
|
68
|
+
end
|
|
69
|
+
alias eql? ==
|
|
70
|
+
|
|
71
|
+
def empty?
|
|
72
|
+
@attributes.keys.empty?
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def hash
|
|
76
|
+
[self.class, attributes].hash
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def to_h
|
|
80
|
+
attributes.symbolize_keys
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def type_for_attribute(name, &block)
|
|
84
|
+
self.class.type_for_attribute(name, &block)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def read_attribute_before_type_cast(name)
|
|
88
|
+
@attributes[name.to_s].value_before_type_cast
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def read_attribute_for_database(name)
|
|
92
|
+
@attributes[name.to_s].value_for_database
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
|
|
5
|
+
module Torque
|
|
6
|
+
module PostgreSQL
|
|
7
|
+
module Attributes
|
|
8
|
+
class Composite < Base
|
|
9
|
+
class << self
|
|
10
|
+
attr_writer :type_name
|
|
11
|
+
|
|
12
|
+
# Find or create the class that will handle the composite type. It
|
|
13
|
+
# first checks the irregular types mapping, then the namespace
|
|
14
|
+
def lookup(name)
|
|
15
|
+
name = name.to_s.underscore
|
|
16
|
+
mapping = PostgreSQL.config.composite.irregular_types
|
|
17
|
+
return resolve(mapping[name].constantize, name) if mapping.key?(name)
|
|
18
|
+
|
|
19
|
+
const = name.camelize
|
|
20
|
+
namespace = PostgreSQL.config.composite.namespace
|
|
21
|
+
|
|
22
|
+
return resolve(namespace.const_get(const), name) if namespace.const_defined?(const)
|
|
23
|
+
resolve(namespace.const_set(const, Class.new(Composite)), name)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def inherited(subclass)
|
|
27
|
+
super
|
|
28
|
+
subclass.instance_variable_set(:@load_columns_monitor, ::Monitor.new)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# The name of the composite type on the database
|
|
32
|
+
def type_name
|
|
33
|
+
@type_name ||= name.demodulize.underscore
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# The ordered list of columns of the composite type, as a hash of
|
|
37
|
+
# name and type, loaded in a lazy way
|
|
38
|
+
def columns
|
|
39
|
+
load_columns
|
|
40
|
+
@columns
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Load the columns from the database and define any attribute that
|
|
44
|
+
# was not manually declared
|
|
45
|
+
def load_columns
|
|
46
|
+
return if self == Composite || defined?(@columns)
|
|
47
|
+
|
|
48
|
+
@load_columns_monitor.synchronize do
|
|
49
|
+
next if defined?(@columns)
|
|
50
|
+
|
|
51
|
+
columns = ActiveRecord::Base.with_connection { |c| c.composite_column_types(type_name) }
|
|
52
|
+
columns.each do |attr_name, attr_type|
|
|
53
|
+
attribute(attr_name, attr_type) unless attribute_names.include?(attr_name)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
@columns = columns
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Drop the loaded columns, so that they are collected again on the
|
|
61
|
+
# next use, which happens after the type is changed
|
|
62
|
+
def reset_columns!
|
|
63
|
+
@load_columns_monitor.synchronize do
|
|
64
|
+
remove_instance_variable(:@columns) if defined?(@columns)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def new(*args, **xargs, &block)
|
|
69
|
+
load_columns
|
|
70
|
+
super
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Columns have to be in place before any of them can be decorated
|
|
74
|
+
def encrypts(*names, **options)
|
|
75
|
+
load_columns
|
|
76
|
+
super
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def enum(*args, **xargs)
|
|
80
|
+
load_columns
|
|
81
|
+
super
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
# Make sure that the class knows the name of the type it handles,
|
|
87
|
+
# since it can be resolved from an irregular mapping
|
|
88
|
+
def resolve(klass, name)
|
|
89
|
+
klass.type_name = name
|
|
90
|
+
klass
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
Composite = Attributes::Composite
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -39,11 +39,22 @@ module Torque
|
|
|
39
39
|
super
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
+
def inherited(subclass)
|
|
43
|
+
super
|
|
44
|
+
subclass.instance_variable_set(:@load_values_monitor, ::Monitor.new)
|
|
45
|
+
end
|
|
46
|
+
|
|
42
47
|
# Load the list of values in a lazy way
|
|
43
48
|
def values
|
|
44
|
-
|
|
45
|
-
|
|
49
|
+
return if self == Enum
|
|
50
|
+
return @values if defined?(@values)
|
|
51
|
+
|
|
52
|
+
@load_values_monitor.synchronize do
|
|
53
|
+
next if defined?(@values)
|
|
54
|
+
@values = ::ActiveRecord::Base.with_connection { |c| c.enum_values(type_name) }.freeze
|
|
46
55
|
end
|
|
56
|
+
|
|
57
|
+
@values
|
|
47
58
|
end
|
|
48
59
|
|
|
49
60
|
# List of values as symbols
|
|
@@ -104,11 +115,6 @@ module Torque
|
|
|
104
115
|
valid?(method_name) ? new(method_name.to_s) : super
|
|
105
116
|
end
|
|
106
117
|
|
|
107
|
-
# Get a connection based on its name
|
|
108
|
-
def connection
|
|
109
|
-
::ActiveRecord::Base.connection
|
|
110
|
-
end
|
|
111
|
-
|
|
112
118
|
end
|
|
113
119
|
|
|
114
120
|
# Override string initializer to check for a valid value
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Torque
|
|
4
|
+
module PostgreSQL
|
|
5
|
+
module Attributes
|
|
6
|
+
# A pattern that matches label paths, as described by the +lquery+ data
|
|
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
|
|
10
|
+
class LQuery
|
|
11
|
+
include Enumerable
|
|
12
|
+
|
|
13
|
+
MARKERS = /[*|!{}@%]/
|
|
14
|
+
LEVEL = /\A(?<not>!)?(?<variants>.*?)(?:\{(?<min>\d*)(?<comma>,)?(?<max>\d*)\})?\z/
|
|
15
|
+
VARIANT = /\A(?<name>.*?)(?<modifiers>[*@%]*)\z/
|
|
16
|
+
|
|
17
|
+
attr_reader :items
|
|
18
|
+
|
|
19
|
+
delegate :inspect, to: :to_s
|
|
20
|
+
delegate :each, :size, :empty?, :[], to: :items
|
|
21
|
+
|
|
22
|
+
def self.[](*items)
|
|
23
|
+
new(items)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def initialize(value = nil, sanitize: true)
|
|
27
|
+
@items = type.items_for(value, sanitize: sanitize).freeze
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def to_s
|
|
31
|
+
type.serialize(self)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def pattern?
|
|
35
|
+
items.any? { |item| !item.is_a?(::String) || item.match?(MARKERS) }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def pattern
|
|
39
|
+
@pattern ||= compile
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def match?(path)
|
|
43
|
+
path = LTree.new(path) unless path.is_a?(LTree)
|
|
44
|
+
pattern.match?(path.to_s)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def =~(path)
|
|
48
|
+
path = LTree.new(path) unless path.is_a?(LTree)
|
|
49
|
+
pattern =~ path.to_s
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def ==(other)
|
|
53
|
+
other = other.items if other.is_a?(self.class)
|
|
54
|
+
other.is_a?(::Array) && items == other
|
|
55
|
+
end
|
|
56
|
+
alias eql? ==
|
|
57
|
+
|
|
58
|
+
def hash
|
|
59
|
+
items.hash
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def type
|
|
65
|
+
Adapter::OID::Lquery.new
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def compile
|
|
69
|
+
levels = to_s.split('.', -1).map { |level| compile_level(level) }
|
|
70
|
+
Regexp.new("\\A#{levels.join}\\z")
|
|
71
|
+
end
|
|
72
|
+
|
|
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)}"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def compile_variants(variants)
|
|
82
|
+
sources = variants.map { |variant| compile_variant(variant) }
|
|
83
|
+
"(?:#{sources.join('|')})"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def compile_variant(text)
|
|
87
|
+
match = text.match(VARIANT)
|
|
88
|
+
modifiers = match[:modifiers]
|
|
89
|
+
prefix = modifiers.include?('*')
|
|
90
|
+
name = match[:name]
|
|
91
|
+
|
|
92
|
+
source = modifiers.include?('%') ? words(name, prefix) : label(name, prefix)
|
|
93
|
+
modifiers.include?('@') ? "(?i:#{source})" : source
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def label(name, prefix)
|
|
97
|
+
"#{Regexp.escape(name)}#{'[^.]*' if prefix}"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def words(name, prefix)
|
|
101
|
+
lookaheads = name.split('_').reject(&:empty?).map do |word|
|
|
102
|
+
"(?=(?:[^.]*_)?#{Regexp.escape(word)}#{'[^._]*' if prefix}(?:[_.]|\\z))"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
"#{lookaheads.join}[^.]+"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def quantifier(match)
|
|
109
|
+
return match[:variants] == '*' ? '*' : '' if match[:min].nil?
|
|
110
|
+
|
|
111
|
+
min = match[:min].presence || 0
|
|
112
|
+
max = match[:comma] ? match[:max] : min
|
|
113
|
+
"{#{min},#{max}}"
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
LQuery = Attributes::LQuery
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Torque
|
|
4
|
+
module PostgreSQL
|
|
5
|
+
module Attributes
|
|
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
|
+
|
|
12
|
+
attr_reader :items
|
|
13
|
+
|
|
14
|
+
delegate :inspect, to: :to_s
|
|
15
|
+
delegate :each, :size, :empty?, :[], to: :items
|
|
16
|
+
alias depth size
|
|
17
|
+
|
|
18
|
+
class << self
|
|
19
|
+
def [](*items)
|
|
20
|
+
new(items)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Whether the object knows how to describe itself as a path, which is
|
|
24
|
+
# what allows any class to be used where a path is expected
|
|
25
|
+
def compatible?(value)
|
|
26
|
+
method = PostgreSQL.config.ltree.compatible_method
|
|
27
|
+
method.present? && value.respond_to?(method)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# The path that the object describes for itself
|
|
31
|
+
def compatible(value)
|
|
32
|
+
method = PostgreSQL.config.ltree.compatible_method
|
|
33
|
+
value.public_send(method) if compatible?(value)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def initialize(value = nil, sanitize: true)
|
|
38
|
+
@items = type.items_for(value, sanitize: sanitize).freeze
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def to_s
|
|
42
|
+
type.serialize(self)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def ==(other)
|
|
46
|
+
other = other.items if other.is_a?(self.class)
|
|
47
|
+
other.is_a?(::Array) && items == other
|
|
48
|
+
end
|
|
49
|
+
alias eql? ==
|
|
50
|
+
|
|
51
|
+
def hash
|
|
52
|
+
items.hash
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def root?
|
|
56
|
+
size <= 1
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def root
|
|
60
|
+
self.class.new(items.first(1), sanitize: false)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def parent
|
|
64
|
+
self.class.new(items[0..-2], sanitize: false) unless root?
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def /(other)
|
|
68
|
+
self.class.new(items + self.class.new(other).items, sanitize: false)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
alias_method :+, :/
|
|
72
|
+
|
|
73
|
+
# Same as the +@>+ operator, which includes the path itself
|
|
74
|
+
def ancestor_of?(other)
|
|
75
|
+
other = self.class.new(other).items
|
|
76
|
+
size <= other.size && other.first(size) == items
|
|
77
|
+
end
|
|
78
|
+
alias covers? ancestor_of?
|
|
79
|
+
|
|
80
|
+
# Same as the +<@+ operator, which includes the path itself
|
|
81
|
+
def descendant_of?(other)
|
|
82
|
+
self.class.new(other).ancestor_of?(self)
|
|
83
|
+
end
|
|
84
|
+
alias covered_by? descendant_of?
|
|
85
|
+
|
|
86
|
+
# The longest common ancestor, which never includes the last label of
|
|
87
|
+
# any of the paths, exactly like PostgreSQL's own +lca+
|
|
88
|
+
def lca(*others)
|
|
89
|
+
paths = [self, *others].map { |path| self.class.new(path).items[0..-2] }
|
|
90
|
+
result = paths.shift
|
|
91
|
+
paths.each { |path| result = common_prefix(result, path) }
|
|
92
|
+
self.class.new(result, sanitize: false)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def index_of(subpath, offset = 0)
|
|
96
|
+
subpath = self.class.new(subpath).items
|
|
97
|
+
offset += size if offset.negative?
|
|
98
|
+
return -1 if subpath.empty? || offset.negative?
|
|
99
|
+
|
|
100
|
+
range = offset..(size - subpath.size)
|
|
101
|
+
position = range.find { |i| items[i, subpath.size] == subpath }
|
|
102
|
+
position || -1
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
private
|
|
106
|
+
|
|
107
|
+
def type
|
|
108
|
+
Adapter::OID::Ltree.new
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def common_prefix(one, other)
|
|
112
|
+
limit = [one.size, other.size].min
|
|
113
|
+
size = (0...limit).find { |i| one[i] != other[i] }
|
|
114
|
+
one.first(size || limit)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
LTree = Attributes::LTree
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Torque
|
|
4
|
+
module PostgreSQL
|
|
5
|
+
module Attributes
|
|
6
|
+
# Brings Rails' own +enum+ to classes that are not backed by a table, which
|
|
7
|
+
# means everything that depends on a record being persisted, or on a
|
|
8
|
+
# relation, is left out. Only the +value?+ methods are generated
|
|
9
|
+
module SimpleEnum
|
|
10
|
+
extend ActiveSupport::Concern
|
|
11
|
+
|
|
12
|
+
# Replaces the module that Rails uses to generate the methods of an
|
|
13
|
+
# enum, so that only the ones that make sense here are defined
|
|
14
|
+
class Methods < Module
|
|
15
|
+
def initialize(klass)
|
|
16
|
+
@klass = klass
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
attr_reader :klass
|
|
22
|
+
|
|
23
|
+
def define_enum_methods(name, value_method_name, value, _scopes, instance_methods)
|
|
24
|
+
return unless instance_methods
|
|
25
|
+
|
|
26
|
+
klass.send(:detect_enum_conflict!, name, "#{value_method_name}?")
|
|
27
|
+
define_method("#{value_method_name}?") do
|
|
28
|
+
@attributes[name].value_for_database == value
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
included do
|
|
34
|
+
class_attribute :defined_enums, instance_writer: false, default: {}
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
class_methods do
|
|
38
|
+
include ActiveRecord::Enum
|
|
39
|
+
|
|
40
|
+
# Scopes require a relation, so they are never an option here
|
|
41
|
+
def enum(name, values = nil, **options)
|
|
42
|
+
super(name, values, **options, scopes: false)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def _enum_methods_module
|
|
48
|
+
@_enum_methods_module ||= begin
|
|
49
|
+
mod = Methods.new(self)
|
|
50
|
+
include mod
|
|
51
|
+
mod
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# There are no dangerous class methods to check against, since enums
|
|
56
|
+
# never define one, and anything that the base class already
|
|
57
|
+
# provides is off limits
|
|
58
|
+
def detect_enum_conflict!(enum_name, method_name, klass_method = false)
|
|
59
|
+
return if klass_method
|
|
60
|
+
return unless Base.method_defined?(method_name) ||
|
|
61
|
+
Base.private_method_defined?(method_name)
|
|
62
|
+
|
|
63
|
+
raise ArgumentError, <<~MSG.squish
|
|
64
|
+
You tried to define an enum named "#{enum_name}" on #{name}, but
|
|
65
|
+
it generates a method "#{method_name}" that is already defined.
|
|
66
|
+
MSG
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
|
|
5
|
+
module Torque
|
|
6
|
+
module PostgreSQL
|
|
7
|
+
module Attributes
|
|
8
|
+
class Struct < Base
|
|
9
|
+
# ActiveModel keeps a sentinel object as the original value of a
|
|
10
|
+
# property that was never written to, which leaks into +changes+ and
|
|
11
|
+
# into anything built on top of it, so it reads as nil instead
|
|
12
|
+
class Uninitialized < ActiveModel::Attribute.const_get(:Uninitialized)
|
|
13
|
+
def original_value
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
class << self
|
|
18
|
+
attr_writer :strict
|
|
19
|
+
|
|
20
|
+
# Whether properties that are not declared by the class are rejected
|
|
21
|
+
def strict?
|
|
22
|
+
return !!@strict if defined?(@strict)
|
|
23
|
+
return !!superclass.strict? if superclass.respond_to?(:strict?)
|
|
24
|
+
|
|
25
|
+
PostgreSQL.config.struct.default_strict
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Declared properties without a default are uninitialized, so that
|
|
29
|
+
# they are absent from the document until they are written to
|
|
30
|
+
def _default_attributes
|
|
31
|
+
@_struct_default_attributes ||= super.map do |attribute|
|
|
32
|
+
next attribute if attribute.is_a?(ActiveModel::Attribute::UserProvidedDefault)
|
|
33
|
+
Uninitialized.new(attribute.name, attribute.type)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Provide a method on the given class to setup which columns are
|
|
38
|
+
# backed by a struct class
|
|
39
|
+
def include_on(klass, method_name = nil)
|
|
40
|
+
method_name ||= PostgreSQL.config.struct.base_method
|
|
41
|
+
klass.define_singleton_method(method_name) do |column, struct_klass, **options|
|
|
42
|
+
Struct.build_on(self, column, struct_klass, **options)
|
|
43
|
+
rescue Interrupt
|
|
44
|
+
# Not able to build the attribute, maybe pending migrations
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Setup the struct column on the given model, adding the proper
|
|
49
|
+
# attribute type, default, validation, and delegations
|
|
50
|
+
def build_on(model, column, klass, default: nil, array: nil, delegate: nil, backfill: nil, strict: nil)
|
|
51
|
+
return unless model.table_exists?
|
|
52
|
+
|
|
53
|
+
column = column.to_s
|
|
54
|
+
has_encryption = model.respond_to?(:encrypted_attributes) &&
|
|
55
|
+
model.encrypted_attributes&.include?(column.to_sym)
|
|
56
|
+
|
|
57
|
+
raise ArgumentError, <<~MSG.squish if has_encryption
|
|
58
|
+
Unable to setup the struct column "#{column}" on #{model.name}
|
|
59
|
+
because the column is encrypted. Encryption is supported on
|
|
60
|
+
individual struct attributes instead.
|
|
61
|
+
MSG
|
|
62
|
+
|
|
63
|
+
info = model.columns_hash[column]
|
|
64
|
+
return if info.nil?
|
|
65
|
+
|
|
66
|
+
raise ArgumentError, <<~MSG.squish unless %i[json jsonb].include?(info.type)
|
|
67
|
+
Unable to setup the struct column "#{column}" on #{model.name}
|
|
68
|
+
because #{info.sql_type} columns cannot hold a document.
|
|
69
|
+
MSG
|
|
70
|
+
|
|
71
|
+
klass = klass.constantize if klass.is_a?(String) || klass.is_a?(Symbol)
|
|
72
|
+
klass.strict = !!strict if !strict.nil? && klass.respond_to?(:strict=)
|
|
73
|
+
|
|
74
|
+
type =
|
|
75
|
+
if array
|
|
76
|
+
Adapter::OID::StructList.new(klass, type: info.type, backfill: backfill)
|
|
77
|
+
else
|
|
78
|
+
Adapter::OID::Struct.new(klass, type: info.type, backfill: backfill)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
type = Adapter::OID::StructSet.new(type) if info.array?
|
|
82
|
+
|
|
83
|
+
# Defaults that can be composed from the record have to be resolved
|
|
84
|
+
# on it, all the others are plain attribute defaults
|
|
85
|
+
if default.is_a?(Proc) || default.is_a?(Symbol)
|
|
86
|
+
model.attribute(column, type)
|
|
87
|
+
resolve_default_on(model, column, default)
|
|
88
|
+
else
|
|
89
|
+
model.attribute(column, type, default: -> { default&.deep_dup || type.blank_document })
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
Array.wrap(delegate).flat_map do |property|
|
|
93
|
+
[property, "#{property}="]
|
|
94
|
+
end.then do |delegations|
|
|
95
|
+
model.delegate(*delegations, to: column) if delegations.any?
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
model.validates(column, nested: true, allow_blank: true)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
private
|
|
102
|
+
|
|
103
|
+
def reset_default_attributes!
|
|
104
|
+
@_struct_default_attributes = nil
|
|
105
|
+
super
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Write the default on the record, as soon as it is initialized, so
|
|
109
|
+
# that it can be composed from the record and properly stored
|
|
110
|
+
def resolve_default_on(model, column, default)
|
|
111
|
+
model.after_initialize do
|
|
112
|
+
next unless new_record?
|
|
113
|
+
next unless read_attribute_before_type_cast(column).nil?
|
|
114
|
+
next if attribute_changed?(column)
|
|
115
|
+
|
|
116
|
+
value = default.is_a?(Proc) ? instance_exec(&default) : public_send(default)
|
|
117
|
+
write_attribute(column, value)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Properties that are not declared by the class are kept as they are,
|
|
123
|
+
# without any type, which is only allowed when the class is not strict
|
|
124
|
+
def attribute_writer_missing(name, value)
|
|
125
|
+
return super if self.class.strict?
|
|
126
|
+
|
|
127
|
+
name = name.to_s
|
|
128
|
+
attribute = @attributes[name] if @attributes.key?(name)
|
|
129
|
+
attribute ||= ActiveModel::Attribute.from_database(name, nil, ActiveModel::Type.default_value)
|
|
130
|
+
@attributes[name] = attribute.with_value_from_user(value)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
Struct = Attributes::Struct
|
|
136
|
+
end
|
|
137
|
+
end
|