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.
Files changed (218) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +52 -0
  3. data/lib/generators/torque/skill_generator.rb +17 -0
  4. data/lib/generators/torque/templates/trigger.sql.erb +3 -0
  5. data/lib/generators/torque/trigger_generator.rb +13 -0
  6. data/lib/torque/postgresql/adapter/database_statements.rb +98 -7
  7. data/lib/torque/postgresql/adapter/inheritance_statements.rb +297 -0
  8. data/lib/torque/postgresql/adapter/oid/array.rb +13 -0
  9. data/lib/torque/postgresql/adapter/oid/box.rb +1 -1
  10. data/lib/torque/postgresql/adapter/oid/circle.rb +1 -1
  11. data/lib/torque/postgresql/adapter/oid/composite.rb +117 -0
  12. data/lib/torque/postgresql/adapter/oid/enum.rb +1 -1
  13. data/lib/torque/postgresql/adapter/oid/enum_set.rb +2 -2
  14. data/lib/torque/postgresql/adapter/oid/interval.rb +1 -1
  15. data/lib/torque/postgresql/adapter/oid/line.rb +1 -1
  16. data/lib/torque/postgresql/adapter/oid/lquery.rb +70 -0
  17. data/lib/torque/postgresql/adapter/oid/ltree.rb +111 -0
  18. data/lib/torque/postgresql/adapter/oid/segment.rb +1 -1
  19. data/lib/torque/postgresql/adapter/oid/struct.rb +150 -0
  20. data/lib/torque/postgresql/adapter/oid/struct_list.rb +52 -0
  21. data/lib/torque/postgresql/adapter/oid/struct_set.rb +38 -0
  22. data/lib/torque/postgresql/adapter/oid.rb +13 -0
  23. data/lib/torque/postgresql/adapter/quoting.rb +11 -2
  24. data/lib/torque/postgresql/adapter/schema_definitions.rb +51 -0
  25. data/lib/torque/postgresql/adapter/schema_dumper.rb +27 -0
  26. data/lib/torque/postgresql/adapter/schema_statements.rb +207 -0
  27. data/lib/torque/postgresql/adapter.rb +2 -5
  28. data/lib/torque/postgresql/arel/nodes.rb +114 -1
  29. data/lib/torque/postgresql/arel/visitors.rb +65 -8
  30. data/lib/torque/postgresql/associations/join_dependency.rb +55 -0
  31. data/lib/torque/postgresql/associations.rb +3 -0
  32. data/lib/torque/postgresql/attributes/base.rb +97 -0
  33. data/lib/torque/postgresql/attributes/composite.rb +98 -0
  34. data/lib/torque/postgresql/attributes/enum.rb +13 -7
  35. data/lib/torque/postgresql/attributes/lquery.rb +120 -0
  36. data/lib/torque/postgresql/attributes/ltree.rb +121 -0
  37. data/lib/torque/postgresql/attributes/simple_enum.rb +72 -0
  38. data/lib/torque/postgresql/attributes/struct.rb +137 -0
  39. data/lib/torque/postgresql/base.rb +26 -15
  40. data/lib/torque/postgresql/config.rb +100 -15
  41. data/lib/torque/postgresql/geometry_builder.rb +4 -10
  42. data/lib/torque/postgresql/inheritance/expander.rb +66 -0
  43. data/lib/torque/postgresql/inheritance/record.rb +52 -0
  44. data/lib/torque/postgresql/inheritance.rb +141 -68
  45. data/lib/torque/postgresql/migration/command_recorder.rb +76 -0
  46. data/lib/torque/postgresql/predicate_builder/arel_attribute_handler.rb +1 -1
  47. data/lib/torque/postgresql/predicate_builder/array_handler.rb +16 -8
  48. data/lib/torque/postgresql/predicate_builder/composite_handler.rb +122 -0
  49. data/lib/torque/postgresql/predicate_builder/ltree_handler.rb +112 -0
  50. data/lib/torque/postgresql/predicate_builder/struct_handler.rb +88 -0
  51. data/lib/torque/postgresql/predicate_builder.rb +53 -0
  52. data/lib/torque/postgresql/predicate_table.rb +61 -0
  53. data/lib/torque/postgresql/railtie.rb +52 -0
  54. data/lib/torque/postgresql/relation/auxiliary_statement.rb +7 -3
  55. data/lib/torque/postgresql/relation/buckets.rb +2 -1
  56. data/lib/torque/postgresql/relation/cross_join.rb +193 -0
  57. data/lib/torque/postgresql/relation/inheritance.rb +145 -28
  58. data/lib/torque/postgresql/relation/join_series.rb +1 -0
  59. data/lib/torque/postgresql/relation/merger.rb +30 -5
  60. data/lib/torque/postgresql/relation/segments.rb +140 -0
  61. data/lib/torque/postgresql/relation.rb +32 -12
  62. data/lib/torque/postgresql/schema_cache.rb +1 -0
  63. data/lib/torque/postgresql/validations.rb +29 -0
  64. data/lib/torque/postgresql/version.rb +1 -1
  65. data/lib/torque/postgresql/versioned_commands/command_migration.rb +24 -1
  66. data/lib/torque/postgresql/versioned_commands.rb +39 -0
  67. data/lib/torque/postgresql.rb +4 -0
  68. data/sig/generators/torque/function_generator.rbs +9 -0
  69. data/sig/generators/torque/skill_generator.rbs +7 -0
  70. data/sig/generators/torque/trigger_generator.rbs +9 -0
  71. data/sig/generators/torque/type_generator.rbs +9 -0
  72. data/sig/generators/torque/view_generator.rbs +9 -0
  73. data/sig/manifest.yaml +7 -0
  74. data/sig/torque/postgresql/adapter/database_statements.rbs +42 -0
  75. data/sig/torque/postgresql/adapter/inheritance_statements.rbs +36 -0
  76. data/sig/torque/postgresql/adapter/oid/array.rbs +20 -0
  77. data/sig/torque/postgresql/adapter/oid/box.rbs +28 -0
  78. data/sig/torque/postgresql/adapter/oid/circle.rbs +33 -0
  79. data/sig/torque/postgresql/adapter/oid/composite.rbs +35 -0
  80. data/sig/torque/postgresql/adapter/oid/enum.rbs +28 -0
  81. data/sig/torque/postgresql/adapter/oid/enum_set.rbs +19 -0
  82. data/sig/torque/postgresql/adapter/oid/interval.rbs +20 -0
  83. data/sig/torque/postgresql/adapter/oid/line.rbs +33 -0
  84. data/sig/torque/postgresql/adapter/oid/lquery.rbs +21 -0
  85. data/sig/torque/postgresql/adapter/oid/ltree.rbs +32 -0
  86. data/sig/torque/postgresql/adapter/oid/range.rbs +28 -0
  87. data/sig/torque/postgresql/adapter/oid/segment.rbs +38 -0
  88. data/sig/torque/postgresql/adapter/oid/struct.rbs +41 -0
  89. data/sig/torque/postgresql/adapter/oid/struct_list.rbs +20 -0
  90. data/sig/torque/postgresql/adapter/oid/struct_set.rbs +18 -0
  91. data/sig/torque/postgresql/adapter/oid.rbs +9 -0
  92. data/sig/torque/postgresql/adapter/quoting.rbs +20 -0
  93. data/sig/torque/postgresql/adapter/schema_creation.rbs +13 -0
  94. data/sig/torque/postgresql/adapter/schema_definitions.rbs +55 -0
  95. data/sig/torque/postgresql/adapter/schema_dumper.rbs +37 -0
  96. data/sig/torque/postgresql/adapter/schema_overrides.rbs +46 -0
  97. data/sig/torque/postgresql/adapter/schema_statements.rbs +50 -0
  98. data/sig/torque/postgresql/adapter.rbs +24 -0
  99. data/sig/torque/postgresql/arel/infix_operation.rbs +79 -0
  100. data/sig/torque/postgresql/arel/join_source.rbs +19 -0
  101. data/sig/torque/postgresql/arel/nodes.rbs +83 -0
  102. data/sig/torque/postgresql/arel/operations.rbs +28 -0
  103. data/sig/torque/postgresql/arel/select_manager.rbs +15 -0
  104. data/sig/torque/postgresql/arel/visitors.rbs +30 -0
  105. data/sig/torque/postgresql/arel.rbs +6 -0
  106. data/sig/torque/postgresql/associations/association_scope.rbs +28 -0
  107. data/sig/torque/postgresql/associations/belongs_to_many_association.rbs +51 -0
  108. data/sig/torque/postgresql/associations/builder/belongs_to_many.rbs +24 -0
  109. data/sig/torque/postgresql/associations/builder/has_many.rbs +15 -0
  110. data/sig/torque/postgresql/associations/builder.rbs +8 -0
  111. data/sig/torque/postgresql/associations/foreign_association.rbs +16 -0
  112. data/sig/torque/postgresql/associations/join_dependency.rbs +16 -0
  113. data/sig/torque/postgresql/associations/preloader/association.rbs +32 -0
  114. data/sig/torque/postgresql/associations/preloader/loader_query.rbs +20 -0
  115. data/sig/torque/postgresql/associations/preloader.rbs +8 -0
  116. data/sig/torque/postgresql/associations.rbs +17 -0
  117. data/sig/torque/postgresql/attributes/base.rbs +37 -0
  118. data/sig/torque/postgresql/attributes/builder/enum.rbs +38 -0
  119. data/sig/torque/postgresql/attributes/builder/full_text_search.rbs +27 -0
  120. data/sig/torque/postgresql/attributes/builder/period.rbs +106 -0
  121. data/sig/torque/postgresql/attributes/builder.rbs +19 -0
  122. data/sig/torque/postgresql/attributes/composite.rbs +28 -0
  123. data/sig/torque/postgresql/attributes/enum.rbs +69 -0
  124. data/sig/torque/postgresql/attributes/enum_set.rbs +75 -0
  125. data/sig/torque/postgresql/attributes/full_text_search.rbs +13 -0
  126. data/sig/torque/postgresql/attributes/lazy.rbs +17 -0
  127. data/sig/torque/postgresql/attributes/lquery.rbs +48 -0
  128. data/sig/torque/postgresql/attributes/ltree.rbs +46 -0
  129. data/sig/torque/postgresql/attributes/period.rbs +13 -0
  130. data/sig/torque/postgresql/attributes/simple_enum.rbs +39 -0
  131. data/sig/torque/postgresql/attributes/struct.rbs +38 -0
  132. data/sig/torque/postgresql/attributes.rbs +6 -0
  133. data/sig/torque/postgresql/autosave_association.rbs +16 -0
  134. data/sig/torque/postgresql/auxiliary_statement/recursive.rbs +22 -0
  135. data/sig/torque/postgresql/auxiliary_statement/settings.rbs +63 -0
  136. data/sig/torque/postgresql/auxiliary_statement.rbs +57 -0
  137. data/sig/torque/postgresql/base.rbs +41 -0
  138. data/sig/torque/postgresql/collector.rbs +7 -0
  139. data/sig/torque/postgresql/config.rbs +9 -0
  140. data/sig/torque/postgresql/function.rbs +21 -0
  141. data/sig/torque/postgresql/geometry_builder.rbs +20 -0
  142. data/sig/torque/postgresql/i18n.rbs +15 -0
  143. data/sig/torque/postgresql/inheritance/expander.rbs +23 -0
  144. data/sig/torque/postgresql/inheritance/record.rbs +20 -0
  145. data/sig/torque/postgresql/inheritance.rbs +47 -0
  146. data/sig/torque/postgresql/insert_all.rbs +22 -0
  147. data/sig/torque/postgresql/migration/command_recorder.rbs +30 -0
  148. data/sig/torque/postgresql/migration.rbs +6 -0
  149. data/sig/torque/postgresql/predicate_builder/arel_attribute_handler.rbs +15 -0
  150. data/sig/torque/postgresql/predicate_builder/array_handler.rbs +26 -0
  151. data/sig/torque/postgresql/predicate_builder/composite_handler.rbs +26 -0
  152. data/sig/torque/postgresql/predicate_builder/enumerator_lazy_handler.rbs +18 -0
  153. data/sig/torque/postgresql/predicate_builder/ltree_handler.rbs +36 -0
  154. data/sig/torque/postgresql/predicate_builder/regexp_handler.rbs +16 -0
  155. data/sig/torque/postgresql/predicate_builder/struct_handler.rbs +24 -0
  156. data/sig/torque/postgresql/predicate_builder.rbs +23 -0
  157. data/sig/torque/postgresql/predicate_table.rbs +24 -0
  158. data/sig/torque/postgresql/railtie.rbs +10 -0
  159. data/sig/torque/postgresql/reflection/abstract_reflection.rbs +22 -0
  160. data/sig/torque/postgresql/reflection/association_reflection.rbs +18 -0
  161. data/sig/torque/postgresql/reflection/belongs_to_many_reflection.rbs +30 -0
  162. data/sig/torque/postgresql/reflection/has_many_reflection.rbs +14 -0
  163. data/sig/torque/postgresql/reflection/runtime_reflection.rbs +18 -0
  164. data/sig/torque/postgresql/reflection/through_reflection.rbs +14 -0
  165. data/sig/torque/postgresql/reflection.rbs +11 -0
  166. data/sig/torque/postgresql/relation/auxiliary_statement.rbs +29 -0
  167. data/sig/torque/postgresql/relation/buckets.rbs +35 -0
  168. data/sig/torque/postgresql/relation/cross_join.rbs +63 -0
  169. data/sig/torque/postgresql/relation/distinct_on.rbs +16 -0
  170. data/sig/torque/postgresql/relation/inheritance.rbs +48 -0
  171. data/sig/torque/postgresql/relation/join_series.rbs +28 -0
  172. data/sig/torque/postgresql/relation/merger.rbs +23 -0
  173. data/sig/torque/postgresql/relation/segments.rbs +27 -0
  174. data/sig/torque/postgresql/relation.rbs +48 -0
  175. data/sig/torque/postgresql/schema_cache/bound_schema_reflection.rbs +14 -0
  176. data/sig/torque/postgresql/schema_cache/inheritance.rbs +12 -0
  177. data/sig/torque/postgresql/schema_cache/schema_reflection.rbs +14 -0
  178. data/sig/torque/postgresql/schema_cache.rbs +43 -0
  179. data/sig/torque/postgresql/table_name.rbs +20 -0
  180. data/sig/torque/postgresql/validations.rbs +13 -0
  181. data/sig/torque/postgresql/version.rbs +5 -0
  182. data/sig/torque/postgresql/versioned_commands/command_migration.rbs +54 -0
  183. data/sig/torque/postgresql/versioned_commands/generator.rbs +17 -0
  184. data/sig/torque/postgresql/versioned_commands/migration_context.rbs +30 -0
  185. data/sig/torque/postgresql/versioned_commands/migrator.rbs +19 -0
  186. data/sig/torque/postgresql/versioned_commands/schema_table.rbs +23 -0
  187. data/sig/torque/postgresql/versioned_commands.rbs +25 -0
  188. data/sig/torque/postgresql.rbs +26 -0
  189. data/skills/torque-postgresql/SKILL.md +70 -0
  190. data/spec/fixtures/migrations/20250101000008_create_trigger_audit_users_v1.sql +3 -0
  191. data/spec/initialize.rb +29 -2
  192. data/spec/mocks/cache_query.rb +13 -1
  193. data/spec/models/author.rb +1 -1
  194. data/spec/models/comment.rb +2 -0
  195. data/spec/models/place.rb +2 -0
  196. data/spec/models/profile.rb +31 -0
  197. data/spec/rbs_helper.rb +8 -0
  198. data/spec/schema.rb +35 -4
  199. data/spec/spec_helper.rb +1 -0
  200. data/spec/tests/arel_spec.rb +3 -3
  201. data/spec/tests/auxiliary_statement_spec.rb +12 -0
  202. data/spec/tests/buckets_spec.rb +122 -0
  203. data/spec/tests/composite_spec.rb +867 -0
  204. data/spec/tests/cross_join_spec.rb +185 -0
  205. data/spec/tests/distinct_on_spec.rb +5 -0
  206. data/spec/tests/enum_set_spec.rb +4 -0
  207. data/spec/tests/enum_spec.rb +20 -4
  208. data/spec/tests/geometric_builder_spec.rb +16 -5
  209. data/spec/tests/has_many_spec.rb +2 -1
  210. data/spec/tests/interval_spec.rb +6 -0
  211. data/spec/tests/ltree_spec.rb +886 -0
  212. data/spec/tests/predicate_builder_spec.rb +120 -0
  213. data/spec/tests/relation_spec.rb +25 -69
  214. data/spec/tests/segments_spec.rb +264 -0
  215. data/spec/tests/struct_spec.rb +1170 -0
  216. data/spec/tests/table_inheritance_spec.rb +1037 -56
  217. data/spec/tests/versioned_commands_spec.rb +145 -0
  218. 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
- @values ||= self == Enum ? nil : begin
45
- connection.enum_values(type_name).freeze
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