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
|
@@ -6,13 +6,33 @@ module Torque
|
|
|
6
6
|
module Inheritance
|
|
7
7
|
|
|
8
8
|
# :nodoc:
|
|
9
|
-
def
|
|
10
|
-
@values.fetch(:
|
|
9
|
+
def expand_records_values
|
|
10
|
+
@values.fetch(:expand_records, FROZEN_EMPTY_ARRAY)
|
|
11
11
|
end
|
|
12
12
|
# :nodoc:
|
|
13
|
-
def
|
|
13
|
+
def expand_records_values=(value)
|
|
14
14
|
assert_modifiable!
|
|
15
|
-
@values[:
|
|
15
|
+
@values[:expand_records] = value
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# :nodoc:
|
|
19
|
+
def expand_records_eager_load_value
|
|
20
|
+
@values.fetch(:expand_records_eager_load, nil)
|
|
21
|
+
end
|
|
22
|
+
# :nodoc:
|
|
23
|
+
def expand_records_eager_load_value=(value)
|
|
24
|
+
assert_modifiable!
|
|
25
|
+
@values[:expand_records_eager_load] = value
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# :nodoc:
|
|
29
|
+
def expand_records_scoped_value
|
|
30
|
+
@values.fetch(:expand_records_scoped, nil) || {}
|
|
31
|
+
end
|
|
32
|
+
# :nodoc:
|
|
33
|
+
def expand_records_scoped_value=(value)
|
|
34
|
+
assert_modifiable!
|
|
35
|
+
@values[:expand_records_scoped] = value
|
|
16
36
|
end
|
|
17
37
|
|
|
18
38
|
# :nodoc:
|
|
@@ -25,6 +45,8 @@ module Torque
|
|
|
25
45
|
@values[:itself_only] = value
|
|
26
46
|
end
|
|
27
47
|
|
|
48
|
+
RECORD_CLASS_TOKEN = :_regclass
|
|
49
|
+
|
|
28
50
|
delegate :quote_table_name, :quote_column_name, to: :connection
|
|
29
51
|
|
|
30
52
|
# Specify that the results should come only from the table that the
|
|
@@ -38,46 +60,90 @@ module Torque
|
|
|
38
60
|
|
|
39
61
|
# Like #itself_only, but modifies relation in place.
|
|
40
62
|
def itself_only!(*)
|
|
63
|
+
raise_itself_only_conflict! if expand_records_values.present?
|
|
64
|
+
|
|
41
65
|
self.itself_only_value = true
|
|
42
66
|
self
|
|
43
67
|
end
|
|
44
68
|
|
|
45
|
-
#
|
|
46
|
-
#
|
|
69
|
+
# Load the columns that only exist on the inherited tables, so that
|
|
70
|
+
# records come out complete and writable. Defaults to every dependent
|
|
71
|
+
# that adds columns
|
|
72
|
+
#
|
|
73
|
+
# Activity.expand_records
|
|
74
|
+
# # Runs one additional query per inherited table
|
|
47
75
|
#
|
|
48
|
-
# Activity.
|
|
49
|
-
# #
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
spawn.cast_records!(*types, **options)
|
|
76
|
+
# Activity.expand_records(ActivityBook, eager_load: true)
|
|
77
|
+
# # Runs a single query using outer joins
|
|
78
|
+
def expand_records(*types, **options)
|
|
79
|
+
spawn.expand_records!(*types, **options)
|
|
53
80
|
end
|
|
54
81
|
|
|
55
|
-
# Like #
|
|
56
|
-
def
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
82
|
+
# Like #expand_records, but modifies relation in place
|
|
83
|
+
def expand_records!(*types, eager_load: false, filter: false)
|
|
84
|
+
raise_itself_only_conflict! if itself_only_value === true
|
|
85
|
+
|
|
86
|
+
types = types.presence || model.inheritance_expandable_dependents.values
|
|
87
|
+
|
|
88
|
+
where!(regclass.pg_cast(:varchar).in(types.map(&:table_name))) if filter
|
|
89
|
+
self.expand_records_values = types
|
|
90
|
+
self.expand_records_eager_load_value = eager_load
|
|
60
91
|
self
|
|
61
92
|
end
|
|
62
93
|
|
|
94
|
+
# Accept the record class marker as a column, which is the only way an
|
|
95
|
+
# explicit selection can still produce casted records
|
|
96
|
+
#
|
|
97
|
+
# Activity.select(:_regclass, :id, :title)
|
|
98
|
+
def select(*fields)
|
|
99
|
+
return super if fields.empty?
|
|
100
|
+
return super(*fields, build_record_class_marker) if fields.delete(RECORD_CLASS_TOKEN)
|
|
101
|
+
|
|
102
|
+
super
|
|
103
|
+
end
|
|
104
|
+
|
|
63
105
|
private
|
|
64
106
|
|
|
107
|
+
def raise_itself_only_conflict!
|
|
108
|
+
raise InheritanceError.new(<<~MSG.squish)
|
|
109
|
+
Reading from ONLY a table never returns records from its
|
|
110
|
+
inherited tables, so itself_only and expand_records cannot be
|
|
111
|
+
combined.
|
|
112
|
+
MSG
|
|
113
|
+
end
|
|
114
|
+
|
|
65
115
|
# Hook arel build to add any necessary table
|
|
66
116
|
def build_arel(*)
|
|
67
117
|
arel = super
|
|
68
118
|
arel.only if self.itself_only_value === true
|
|
69
|
-
|
|
119
|
+
|
|
120
|
+
arel.project(build_record_class_marker) if inheritance_discriminated?
|
|
121
|
+
build_inheritances(arel) if self.expand_records_eager_load_value
|
|
70
122
|
arel
|
|
71
123
|
end
|
|
72
124
|
|
|
125
|
+
def inheritance_discriminated?
|
|
126
|
+
return false if self.itself_only_value === true
|
|
127
|
+
return false if select_values.present?
|
|
128
|
+
return false unless from_clause.empty?
|
|
129
|
+
|
|
130
|
+
model.physically_inheritances?
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def build_record_class_marker
|
|
134
|
+
regclass.as(_record_class_column_name)
|
|
135
|
+
end
|
|
136
|
+
|
|
73
137
|
# Build all necessary data for inheritances
|
|
74
138
|
def build_inheritances(arel)
|
|
75
|
-
return if self.
|
|
139
|
+
return if self.expand_records_values.empty?
|
|
76
140
|
|
|
77
|
-
|
|
141
|
+
columns = build_inheritances_joins(arel, self.expand_records_values)
|
|
142
|
+
# The joins are still needed, but an explicit select owns the projection
|
|
143
|
+
return if columns.empty? || select_values.present?
|
|
78
144
|
|
|
79
|
-
|
|
80
|
-
|
|
145
|
+
mergeable = inheritance_mergeable_attributes
|
|
146
|
+
projections = columns.map do |column, arel_tables|
|
|
81
147
|
next arel_tables.first[column] if arel_tables.size == 1
|
|
82
148
|
|
|
83
149
|
if mergeable.include?(column)
|
|
@@ -87,8 +153,7 @@ module Torque
|
|
|
87
153
|
end
|
|
88
154
|
end
|
|
89
155
|
|
|
90
|
-
|
|
91
|
-
self.select_extra_values += columns.flatten if columns.any?
|
|
156
|
+
arel.project(*projections.flatten)
|
|
92
157
|
end
|
|
93
158
|
|
|
94
159
|
# Build as many left outer join as necessary for each dependent table
|
|
@@ -111,15 +176,67 @@ module Torque
|
|
|
111
176
|
columns
|
|
112
177
|
end
|
|
113
178
|
|
|
114
|
-
def build_auto_caster_marker(arel, types)
|
|
115
|
-
attribute = regclass.pg_cast(:varchar).in(types.map(&:table_name))
|
|
116
|
-
attribute.as(self.class._auto_cast_attribute.to_s)
|
|
117
|
-
end
|
|
118
|
-
|
|
119
179
|
def regclass
|
|
120
180
|
arel_table['tableoid'].pg_cast(:regclass)
|
|
121
181
|
end
|
|
122
182
|
|
|
183
|
+
module Expansion
|
|
184
|
+
def preload_associations(records)
|
|
185
|
+
super
|
|
186
|
+
return if expand_records_scoped_value.blank?
|
|
187
|
+
|
|
188
|
+
expand_records_scoped_value.each do |base_model, targets|
|
|
189
|
+
preloaded_association_names.each do |name|
|
|
190
|
+
reflection = model.reflect_on_association(name)
|
|
191
|
+
next if reflection.nil? || reflection.polymorphic? || !(reflection.klass <= base_model)
|
|
192
|
+
|
|
193
|
+
loaded = records.flat_map { |record| record.association(name).target }
|
|
194
|
+
PostgreSQL::Inheritance::Expander.new(reflection.klass, loaded.compact, targets).call
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
private
|
|
200
|
+
|
|
201
|
+
# Records only carry the columns of the queried table, so
|
|
202
|
+
# expanding has to happen after they have been instantiated
|
|
203
|
+
def exec_queries
|
|
204
|
+
records = super
|
|
205
|
+
warn_about_missing_record_class(records)
|
|
206
|
+
return records if expand_records_values.empty? || expand_records_eager_load_value
|
|
207
|
+
|
|
208
|
+
PostgreSQL::Inheritance::Expander.new(model, records, expand_records_values).call
|
|
209
|
+
records.each(&:readonly!) if readonly_value
|
|
210
|
+
records
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def preloaded_association_names
|
|
214
|
+
list = preload_values + (eager_loading? ? [] : includes_values)
|
|
215
|
+
list.flat_map { |entry| entry.is_a?(Hash) ? entry.keys : entry }
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Warn once per query that actually lost the real class, instead of
|
|
219
|
+
# once per explicit select that merely omits the marker
|
|
220
|
+
def warn_about_missing_record_class(records)
|
|
221
|
+
return unless model.physically_inheritances?
|
|
222
|
+
return if itself_only_value === true
|
|
223
|
+
return if records.empty? || group_values.present? || distinct_value || select_values.empty?
|
|
224
|
+
return unless from_clause.empty?
|
|
225
|
+
return if select_values.any? { |value| record_class_marker?(value) }
|
|
226
|
+
|
|
227
|
+
warn(<<~MSG.squish)
|
|
228
|
+
#{model.name} was queried with an explicit select that omits
|
|
229
|
+
:_regclass, so its records will not be instantiated as their
|
|
230
|
+
real class.
|
|
231
|
+
MSG
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def record_class_marker?(value)
|
|
235
|
+
return value.right.to_s == _record_class_column_name if value.is_a?(::Arel::Nodes::As)
|
|
236
|
+
value.is_a?(String) && value.include?(_record_class_column_name)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
end
|
|
123
240
|
end
|
|
124
241
|
end
|
|
125
242
|
end
|
|
@@ -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
|
|
@@ -46,18 +47,34 @@ module Torque
|
|
|
46
47
|
end
|
|
47
48
|
end
|
|
48
49
|
|
|
49
|
-
# Merge settings related to inheritance tables
|
|
50
|
+
# Merge settings related to inheritance tables, going through the
|
|
51
|
+
# public operations so that their conflicts are still detected
|
|
50
52
|
def merge_inheritance
|
|
51
53
|
return unless relation.is_a?(Relation::Inheritance)
|
|
52
54
|
|
|
53
|
-
relation.
|
|
55
|
+
relation.itself_only! if other.itself_only_value.present?
|
|
54
56
|
|
|
55
|
-
if other.
|
|
56
|
-
|
|
57
|
-
|
|
57
|
+
return if other.expand_records_values.blank?
|
|
58
|
+
|
|
59
|
+
if relation.model == other.model
|
|
60
|
+
types = (relation.expand_records_values + other.expand_records_values).uniq
|
|
61
|
+
relation.expand_records!(*types, eager_load: other.expand_records_eager_load_value)
|
|
62
|
+
else
|
|
63
|
+
raise_eager_load_conflict! if other.expand_records_eager_load_value
|
|
64
|
+
|
|
65
|
+
relation.expand_records_scoped_value = relation.expand_records_scoped_value
|
|
66
|
+
.merge(other.model => other.expand_records_values) { |_, a, b| (a + b).uniq }
|
|
58
67
|
end
|
|
59
68
|
end
|
|
60
69
|
|
|
70
|
+
def raise_eager_load_conflict!
|
|
71
|
+
raise InheritanceError.new(<<~MSG.squish)
|
|
72
|
+
Expanding the records of a different model happens through a
|
|
73
|
+
preload, which has no query to eager load the inherited tables
|
|
74
|
+
into, so the two cannot be merged.
|
|
75
|
+
MSG
|
|
76
|
+
end
|
|
77
|
+
|
|
61
78
|
# Merge settings related to buckets
|
|
62
79
|
def merge_buckets
|
|
63
80
|
return unless defined?(Relation::Buckets) && relation.is_a?(Relation::Buckets)
|
|
@@ -66,6 +83,14 @@ module Torque
|
|
|
66
83
|
relation.buckets_value = other.buckets_value
|
|
67
84
|
end
|
|
68
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
|
+
|
|
69
94
|
end
|
|
70
95
|
|
|
71
96
|
ActiveRecord::Relation::Merger.prepend Merger
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Torque
|
|
4
|
+
module PostgreSQL
|
|
5
|
+
module Relation
|
|
6
|
+
module Segments
|
|
7
|
+
|
|
8
|
+
# :nodoc:
|
|
9
|
+
def segments_value
|
|
10
|
+
@values.fetch(:segments, ::ActiveRecord::QueryMethods::FROZEN_EMPTY_HASH)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# :nodoc:
|
|
14
|
+
def segments_value=(value)
|
|
15
|
+
assert_modifiable!
|
|
16
|
+
@values[:segments] = value
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Maps names to conditions so that the next calculation returns one
|
|
20
|
+
# value per name, each aggregate filtered by its condition. Anything
|
|
21
|
+
# that can become a relation is accepted as a condition, an array
|
|
22
|
+
# applies each of its items in order, and +nil+ means the aggregate is
|
|
23
|
+
# not filtered at all. For example:
|
|
24
|
+
#
|
|
25
|
+
# User.segments(all: nil, adults: { age: 18.. }).count
|
|
26
|
+
# # Returns { all: 15, adults: 12 }
|
|
27
|
+
#
|
|
28
|
+
# User.group(:role).segments(adults: { age: 18.. }).sum(:age)
|
|
29
|
+
# # Returns { 'visitor' => { adults: 480 }, ... }
|
|
30
|
+
def segments(**map)
|
|
31
|
+
spawn.segments!(**map)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Like #segments, but modifies relation in place.
|
|
35
|
+
def segments!(**map)
|
|
36
|
+
predicates = map.to_h { |name, condition| [name, segment_predicate_for(name, condition)] }
|
|
37
|
+
self.segments_value = segments_value.merge(predicates)
|
|
38
|
+
self
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def execute_simple_calculation(operation, column_name, distinct)
|
|
44
|
+
return super if segments_value.blank?
|
|
45
|
+
|
|
46
|
+
execute_segments_calculation(operation, column_name, distinct)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def execute_grouped_calculation(operation, column_name, distinct)
|
|
50
|
+
return super if segments_value.blank?
|
|
51
|
+
|
|
52
|
+
execute_segments_calculation(operation, column_name, distinct)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def execute_segments_calculation(operation, column_name, distinct)
|
|
56
|
+
column_name = primary_key if operation == 'count' && column_name == :all && distinct
|
|
57
|
+
column = aggregate_column(column_name)
|
|
58
|
+
aggregate = operation_over_aggregate_column(column, operation, distinct)
|
|
59
|
+
|
|
60
|
+
relation = except(:group, :select).distinct!(false)
|
|
61
|
+
relation.order_values = [] if group_values.empty?
|
|
62
|
+
|
|
63
|
+
groups = relation.arel_columns(group_values)
|
|
64
|
+
relation.group_values = groups
|
|
65
|
+
|
|
66
|
+
result = skip_query_cache_if_necessary do
|
|
67
|
+
klass.with_connection do |connection|
|
|
68
|
+
relation.select_values = segments_projections(groups, aggregate, connection)
|
|
69
|
+
connection.select_all(relation.arel, "#{klass.name} Segments", async: @async)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
result.then { |data| segments_result(data, groups, column, operation) }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def segments_projections(groups, aggregate, connection)
|
|
77
|
+
tracker = ::ActiveRecord::Calculations::ColumnAliasTracker.new(connection)
|
|
78
|
+
projections = groups.map do |group|
|
|
79
|
+
name = tracker.alias_for(connection.visitor.compile(group).downcase)
|
|
80
|
+
group.as(connection.quote_column_name(name))
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
segments_value.each_with_object(projections) do |(name, predicate), list|
|
|
84
|
+
node = predicate.nil? ? aggregate.dup : aggregate.filter(predicate)
|
|
85
|
+
list << node.as(connection.quote_column_name(name.to_s))
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def segments_result(data, groups, column, operation)
|
|
90
|
+
type = column.try(:type_caster) || ::ActiveRecord::Type.default_value
|
|
91
|
+
type = type.subtype if ::ActiveRecord::Enum::EnumType === type
|
|
92
|
+
|
|
93
|
+
key_types = groups.each_with_index.to_h do |group, index|
|
|
94
|
+
name = data.columns[index]
|
|
95
|
+
key_type = group.try(:type_caster)
|
|
96
|
+
key_type ||= type_for(group) { data.column_types.fetch(name, ::ActiveRecord::Type.default_value) }
|
|
97
|
+
[name, key_type]
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
rows = data.cast_values(key_types)
|
|
101
|
+
rows = rows.map { |value| [value] } if data.columns.size == 1
|
|
102
|
+
|
|
103
|
+
rows = rows.to_h do |row|
|
|
104
|
+
key = row.take(groups.size)
|
|
105
|
+
key = key.first if key.size == 1
|
|
106
|
+
|
|
107
|
+
values = row.drop(groups.size).map do |value|
|
|
108
|
+
type_cast_calculated_value(value, operation, type)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
[key, segments_value.keys.zip(values).to_h]
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
groups.empty? ? rows.values.first : rows
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def segment_predicate_for(name, condition)
|
|
118
|
+
return if !condition.is_a?(::ActiveRecord::Relation) && condition.blank?
|
|
119
|
+
|
|
120
|
+
items = condition.is_a?(::Array) ? condition : [condition]
|
|
121
|
+
relation = items.reduce(klass.unscoped) do |result, item|
|
|
122
|
+
case item
|
|
123
|
+
when ::ActiveRecord::Relation then result.merge(item)
|
|
124
|
+
when Symbol then result.public_send(item)
|
|
125
|
+
when Proc then item.arity.zero? ? result.instance_exec(&item) : item.call(result)
|
|
126
|
+
else result.where(item)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
raise ArgumentError, <<~MSG.squish if relation.where_clause.empty?
|
|
131
|
+
Segment #{name} does not filter anything, use nil for an unfiltered segment.
|
|
132
|
+
MSG
|
|
133
|
+
|
|
134
|
+
relation.where_clause.ast
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
@@ -13,9 +13,11 @@ module Torque
|
|
|
13
13
|
include DistinctOn
|
|
14
14
|
include Inheritance
|
|
15
15
|
|
|
16
|
-
SINGLE_VALUE_METHODS = %i[
|
|
16
|
+
SINGLE_VALUE_METHODS = %i[
|
|
17
|
+
itself_only buckets segments expand_records_eager_load expand_records_scoped
|
|
18
|
+
]
|
|
17
19
|
MULTI_VALUE_METHODS = %i[
|
|
18
|
-
select_extra distinct_on auxiliary_statements
|
|
20
|
+
select_extra distinct_on auxiliary_statements expand_records
|
|
19
21
|
]
|
|
20
22
|
|
|
21
23
|
VALUE_METHODS = SINGLE_VALUE_METHODS + MULTI_VALUE_METHODS
|
|
@@ -76,7 +78,11 @@ module Torque
|
|
|
76
78
|
return unless relation
|
|
77
79
|
|
|
78
80
|
table = predicate_builder.send(:table)
|
|
79
|
-
|
|
81
|
+
part = part_table_for(table, relation.to_s)
|
|
82
|
+
return part if part
|
|
83
|
+
|
|
84
|
+
associated = AR810 ? table.associated_with(relation.to_s) : table.associated_with?(relation.to_s)
|
|
85
|
+
if associated
|
|
80
86
|
table.associated_table(relation.to_s).send(:klass)
|
|
81
87
|
else
|
|
82
88
|
raise ArgumentError, "Relation for #{relation} not found on #{klass}"
|
|
@@ -87,8 +93,11 @@ module Torque
|
|
|
87
93
|
# the given column
|
|
88
94
|
def cast_for_condition(column, value)
|
|
89
95
|
column = columns_hash[column.to_s] unless column.is_a?(ARColumn)
|
|
90
|
-
|
|
91
|
-
connection
|
|
96
|
+
|
|
97
|
+
klass.with_connection do |connection|
|
|
98
|
+
caster = connection.lookup_cast_type_for_column(column)
|
|
99
|
+
connection.type_cast(caster.serialize(value))
|
|
100
|
+
end
|
|
92
101
|
end
|
|
93
102
|
|
|
94
103
|
private
|
|
@@ -99,6 +108,16 @@ module Torque
|
|
|
99
108
|
arel
|
|
100
109
|
end
|
|
101
110
|
|
|
111
|
+
def part_table_for(table, name)
|
|
112
|
+
return unless table.has_column?(name)
|
|
113
|
+
|
|
114
|
+
type = Adapter::OID.unwrap(table.type(name))
|
|
115
|
+
handler = PredicateBuilder.handler_for(type)
|
|
116
|
+
return if handler.nil?
|
|
117
|
+
|
|
118
|
+
handler.new(predicate_builder).table_for(table.arel_table[name], type)
|
|
119
|
+
end
|
|
120
|
+
|
|
102
121
|
class_methods do
|
|
103
122
|
# Easy and storable way to access the name used to get the record table
|
|
104
123
|
# name when using inheritance tables
|
|
@@ -107,11 +126,10 @@ module Torque
|
|
|
107
126
|
.inheritance.record_class_column_name.to_sym
|
|
108
127
|
end
|
|
109
128
|
|
|
110
|
-
#
|
|
111
|
-
#
|
|
112
|
-
def
|
|
113
|
-
@@
|
|
114
|
-
.inheritance.auto_cast_column_name.to_sym
|
|
129
|
+
# The same name as a string, which is how it arrives on every single
|
|
130
|
+
# row that has to be discriminated
|
|
131
|
+
def _record_class_column_name
|
|
132
|
+
@@record_class_column_name ||= _record_class_attribute.to_s.freeze
|
|
115
133
|
end
|
|
116
134
|
end
|
|
117
135
|
|
|
@@ -138,12 +156,14 @@ module Torque
|
|
|
138
156
|
# the operation of ActiveRecord Relation
|
|
139
157
|
ActiveRecord::Relation.include Relation
|
|
140
158
|
ActiveRecord::Relation.prepend Relation::Initializer
|
|
159
|
+
ActiveRecord::Relation.prepend Relation::Inheritance::Expansion
|
|
141
160
|
|
|
142
161
|
ActiveRecord::Relation::SINGLE_VALUE_METHODS.concat(Relation::SINGLE_VALUE_METHODS)
|
|
143
162
|
ActiveRecord::Relation::MULTI_VALUE_METHODS.concat(Relation::MULTI_VALUE_METHODS)
|
|
144
163
|
ActiveRecord::Relation::VALUE_METHODS.concat(Relation::VALUE_METHODS)
|
|
145
|
-
ActiveRecord::QueryMethods::VALID_UNSCOPING_VALUES.merge(%i[
|
|
146
|
-
distinct_on auxiliary_statements buckets
|
|
164
|
+
ActiveRecord::QueryMethods::VALID_UNSCOPING_VALUES.merge(%i[expand_records itself_only
|
|
165
|
+
expand_records_eager_load expand_records_scoped distinct_on auxiliary_statements buckets
|
|
166
|
+
segments])
|
|
147
167
|
|
|
148
168
|
end
|
|
149
169
|
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Torque
|
|
4
|
+
module PostgreSQL
|
|
5
|
+
module Validations
|
|
6
|
+
|
|
7
|
+
# Checks if the objects nested into an attribute are valid, adding a
|
|
8
|
+
# single error to the attribute when any of them is not. It takes the
|
|
9
|
+
# regular +allow_nil+ and +allow_blank+ options, which is how values that
|
|
10
|
+
# are merely absent are left out of it
|
|
11
|
+
#
|
|
12
|
+
# Example:
|
|
13
|
+
# validates :home, nested: true
|
|
14
|
+
# validates :settings, nested: true, allow_blank: true
|
|
15
|
+
class NestedValidator < ActiveModel::EachValidator
|
|
16
|
+
def validate_each(record, attribute, value)
|
|
17
|
+
invalid = Array.wrap(value).any? do |item|
|
|
18
|
+
item.respond_to?(:invalid?) && item.invalid?
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
record.errors.add(attribute, :invalid) if invalid
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
::ActiveRecord::Base.include(Validations)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -62,7 +62,7 @@ module Torque
|
|
|
62
62
|
end
|
|
63
63
|
end
|
|
64
64
|
|
|
65
|
-
CommandMigration = Struct.new(*%i[filename version op type object_name op_version scope]) do
|
|
65
|
+
CommandMigration = ::Struct.new(*%i[filename version op type object_name op_version scope]) do
|
|
66
66
|
delegate :execute, to: '@migration'
|
|
67
67
|
|
|
68
68
|
def initialize(filename, *args)
|
|
@@ -131,6 +131,29 @@ module Torque
|
|
|
131
131
|
execute "DROP TYPE #{name.first.first};"
|
|
132
132
|
end
|
|
133
133
|
|
|
134
|
+
# Drop the trigger and any function bundled with it
|
|
135
|
+
def drop_trigger
|
|
136
|
+
content = File.read(filename)
|
|
137
|
+
trigger, table = content.scan(Regexp.new([
|
|
138
|
+
'^\s*CREATE\s+(?:OR\s+REPLACE\s+)?',
|
|
139
|
+
"TRIGGER\\s+#{NAME_MATCH}",
|
|
140
|
+
'.*?\bON\s+',
|
|
141
|
+
NAME_MATCH,
|
|
142
|
+
].join, 'mi')).first
|
|
143
|
+
|
|
144
|
+
execute "DROP TRIGGER #{trigger} ON #{table};"
|
|
145
|
+
|
|
146
|
+
definitions = content.scan(Regexp.new([
|
|
147
|
+
'^\s*CREATE\s+(?:OR\s+REPLACE\s+)?',
|
|
148
|
+
"FUNCTION\\s+#{NAME_MATCH}",
|
|
149
|
+
'\s*(\([_a-z0-9 ,]*\))?',
|
|
150
|
+
].join, 'mi'))
|
|
151
|
+
return if definitions.empty?
|
|
152
|
+
|
|
153
|
+
functions = definitions.map(&:join).join(', ')
|
|
154
|
+
execute "DROP FUNCTION #{functions};"
|
|
155
|
+
end
|
|
156
|
+
|
|
134
157
|
# Drop view or materialized view
|
|
135
158
|
def drop_view
|
|
136
159
|
mat, name = File.read(filename).scan(Regexp.new([
|
|
@@ -117,6 +117,45 @@ module Torque
|
|
|
117
117
|
MSG
|
|
118
118
|
end
|
|
119
119
|
|
|
120
|
+
# Validate that the content of the command is correct
|
|
121
|
+
def validate_trigger!(content, name)
|
|
122
|
+
result = content.scan(Regexp.new([
|
|
123
|
+
'^\s*CREATE\s+(OR\s+REPLACE)?\s*',
|
|
124
|
+
"TRIGGER\\s+#{NAME_MATCH}",
|
|
125
|
+
].join, 'mi'))
|
|
126
|
+
|
|
127
|
+
raise ArgumentError, <<~MSG.squish if result.empty?
|
|
128
|
+
Missing or invalid trigger definition.
|
|
129
|
+
MSG
|
|
130
|
+
|
|
131
|
+
raise ArgumentError, <<~MSG.squish if result.size > 1
|
|
132
|
+
More than one trigger definition found.
|
|
133
|
+
MSG
|
|
134
|
+
|
|
135
|
+
raise ArgumentError, <<~MSG.squish unless result.all?(&:first)
|
|
136
|
+
'OR REPLACE' is required for proper migration support.
|
|
137
|
+
MSG
|
|
138
|
+
|
|
139
|
+
trigger_name = result.first.last.downcase.sub('.', '_')
|
|
140
|
+
raise ArgumentError, <<~MSG.squish if trigger_name != name.downcase
|
|
141
|
+
Trigger name must match file name.
|
|
142
|
+
MSG
|
|
143
|
+
|
|
144
|
+
functions = content.scan(Regexp.new([
|
|
145
|
+
'^\s*CREATE\s+(OR\s+REPLACE)?\s*',
|
|
146
|
+
"FUNCTION\\s+#{NAME_MATCH}",
|
|
147
|
+
].join, 'mi'))
|
|
148
|
+
return if functions.empty?
|
|
149
|
+
|
|
150
|
+
raise ArgumentError, <<~MSG.squish if functions.map(&:last).uniq(&:downcase).many?
|
|
151
|
+
Only one function can be bundled with a trigger.
|
|
152
|
+
MSG
|
|
153
|
+
|
|
154
|
+
raise ArgumentError, <<~MSG.squish unless functions.all?(&:first)
|
|
155
|
+
'OR REPLACE' is required for proper migration support.
|
|
156
|
+
MSG
|
|
157
|
+
end
|
|
158
|
+
|
|
120
159
|
# Validate that the content of the command is correct
|
|
121
160
|
def validate_view!(content, name)
|
|
122
161
|
result = content.scan(Regexp.new([
|