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,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Torque
4
+ module PostgreSQL
5
+ module Adapter
6
+ module OID
7
+ class Composite < ActiveModel::Type::Value
8
+ ARRAY = ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array
9
+ DATA = ARRAY::Data
10
+ ENCODER = PG::TextEncoder::Record.new
11
+ DECODER = PG::TextDecoder::Record.new
12
+
13
+ attr_reader :name
14
+
15
+ # The composite type behind the given type, if there is one, wrapped
16
+ # or not by an array or a delegator
17
+ def self.from(type)
18
+ type = OID.unwrap(type)
19
+ type = type.subtype if type.is_a?(ARRAY)
20
+ type if type.is_a?(self)
21
+ end
22
+
23
+ def self.create(row, type_map)
24
+ oid_klass = new(row['typname'])
25
+ type_map.register_type(row['oid'].to_i, oid_klass)
26
+ type_map.register_type(row['typarray'].to_i, ARRAY.new(oid_klass, row['typdelim']))
27
+ end
28
+
29
+ def initialize(name)
30
+ @name = name.to_s.freeze
31
+ end
32
+
33
+ # The class that represents the composite type, loaded in a lazy way.
34
+ # It cannot be memoized here because Rails freezes the types it
35
+ # resolves through the type map, so the lookup owns the cache
36
+ def klass
37
+ Attributes::Composite.lookup(name)
38
+ end
39
+
40
+ # The ordered list of columns of the composite type, as a hash of
41
+ # name and type
42
+ def columns
43
+ klass.columns
44
+ end
45
+
46
+ def type
47
+ :composite
48
+ end
49
+
50
+ def cast(value)
51
+ case value
52
+ when klass then value
53
+ when ::Hash then build(value)
54
+ when DATA then deserialize(value)
55
+ when ::String then deserialize(value)
56
+ when ::Array then build(columns.keys.zip(value).to_h)
57
+ else value
58
+ end
59
+ end
60
+
61
+ # A record that was already serialized comes back as the encoder and
62
+ # its values, which is how it is kept around for dirty tracking
63
+ def deserialize(value)
64
+ return if value.nil?
65
+
66
+ fields = value.is_a?(DATA) ? value.values : DECODER.decode(value)
67
+ instance = klass.new
68
+ set = attributes_of(instance)
69
+
70
+ columns.each_key.with_index do |attr_name, idx|
71
+ set.write_from_database(attr_name, fields[idx])
72
+ end
73
+
74
+ instance
75
+ end
76
+
77
+ # The record is handed over to the adapter as an encoder and its
78
+ # values, the same way arrays are, so that columns are type casted by
79
+ # the connection right before the record literal is built. Values come
80
+ # from the record itself, so that types declared by the class, like
81
+ # encrypted ones, are the ones being applied
82
+ def serialize(value)
83
+ return if value.nil?
84
+
85
+ set = attributes_of(cast(value))
86
+ values = columns.each_key.map { |attr_name| set[attr_name].value_for_database }
87
+
88
+ DATA.new(ENCODER, values)
89
+ end
90
+
91
+ def changed_in_place?(raw_old_value, new_value)
92
+ deserialize(raw_old_value) != new_value
93
+ end
94
+
95
+ def ==(other)
96
+ other.class == self.class && other.name == name
97
+ end
98
+ alias eql? ==
99
+
100
+ def hash
101
+ [self.class, name].hash
102
+ end
103
+
104
+ private
105
+
106
+ def build(attrs)
107
+ klass.new(**attrs.symbolize_keys)
108
+ end
109
+
110
+ def attributes_of(record)
111
+ record.instance_variable_get(:@attributes)
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
@@ -60,7 +60,7 @@ module Torque
60
60
  return value if value.is_a?(@klass)
61
61
  @klass.new(value)
62
62
  rescue Attributes::Enum::EnumError
63
- nil
63
+ value
64
64
  end
65
65
 
66
66
  end
@@ -42,8 +42,8 @@ module Torque
42
42
  return if value.blank?
43
43
  return value if value.is_a?(@klass)
44
44
  @klass.new(value)
45
- rescue Attributes::EnumSet::EnumSetError
46
- nil
45
+ rescue Attributes::Enum::EnumError
46
+ ::Array.wrap(value)
47
47
  end
48
48
 
49
49
  end
@@ -36,7 +36,7 @@ module Torque
36
36
  end
37
37
  parts_to_duration(parts.compact)
38
38
  when ::Array
39
- value.compact!
39
+ value = value.compact
40
40
  parts = CAST_PARTS.drop(6 - value.size).zip(value).to_h
41
41
  parts_to_duration(parts)
42
42
  when ::Hash
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Torque
4
4
  module PostgreSQL
5
- class Line < Struct.new(:slope, :intercept)
5
+ class Line < ::Struct.new(:slope, :intercept)
6
6
  alias c intercept
7
7
  alias c= intercept=
8
8
 
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'ltree'
4
+
5
+ module Torque
6
+ module PostgreSQL
7
+ module Adapter
8
+ module OID
9
+ # Same as a path, but the items are the pure parts of a pattern: +:any+
10
+ # for a star, a Range for a quantified star and an Array for a group of
11
+ # alternatives. Everything else is a label, written as it appears in SQL
12
+ class Lquery < Ltree
13
+ STAR = /\A\*\{(?<min>\d+)?(?<comma>,)?(?<max>\d+)?\}\z/
14
+
15
+ def type
16
+ :lquery
17
+ end
18
+
19
+ private
20
+
21
+ def value_class
22
+ LQuery
23
+ end
24
+
25
+ def items_of(entry, sanitize)
26
+ case entry
27
+ when ::Symbol then entry == :any ? [:any] : super
28
+ when ::Range then [entry]
29
+ when ::Array then [entry.flat_map { |item| items_of(item, sanitize) }]
30
+ else super
31
+ end
32
+ end
33
+
34
+ def compile(item)
35
+ case item
36
+ when ::Symbol then item == :any ? '*' : item.to_s
37
+ when ::Range then quantifier(item)
38
+ when ::Array then item.map { |entry| compile(entry) }.join('|')
39
+ else item.to_s
40
+ end
41
+ end
42
+
43
+ def quantifier(range)
44
+ min = range.begin
45
+ max = range.end
46
+ max -= 1 if max && range.exclude_end?
47
+
48
+ return '*' if min.nil? && max.nil?
49
+ return "*{#{min}}" if min == max
50
+
51
+ "*{#{min},#{max}}"
52
+ end
53
+
54
+ def parse(text)
55
+ return :any if text == '*'
56
+ return text.split('|') if text.include?('|') && !text.match?(/[!{]/)
57
+
58
+ match = text.match(STAR)
59
+ return text if match.nil?
60
+
61
+ min = match[:min]&.to_i
62
+ max = match[:comma] ? match[:max]&.to_i : min
63
+ min..max
64
+ end
65
+
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Torque
4
+ module PostgreSQL
5
+ module Adapter
6
+ module OID
7
+ # Moves a label path between the shapes the gem accepts, its pure list
8
+ # of items and the text form PostgreSQL stores. Nothing is validated
9
+ # here, that is left to PostgreSQL
10
+ class Ltree < ActiveModel::Type::Value
11
+
12
+ def type
13
+ :ltree
14
+ end
15
+
16
+ def cast(value)
17
+ return value if value.is_a?(value_class)
18
+ return if value.blank?
19
+
20
+ value_class.new(value)
21
+ end
22
+
23
+ def serialize(value)
24
+ path = cast(value)
25
+ path.map { |item| compile(item) }.join('.') unless path.nil?
26
+ end
27
+
28
+ def deserialize(value)
29
+ return if value.nil?
30
+
31
+ value_class.new(value, sanitize: false)
32
+ end
33
+
34
+ def items_for(value, sanitize: true)
35
+ ::Array.wrap(value).flat_map { |entry| items_of(entry, sanitize) }
36
+ end
37
+
38
+ def type_cast_for_schema(value)
39
+ serialize(value).inspect
40
+ end
41
+
42
+ def changed_in_place?(raw_old_value, new_value)
43
+ raw_old_value != serialize(new_value)
44
+ end
45
+
46
+ def ==(other)
47
+ other.class == self.class
48
+ end
49
+ alias eql? ==
50
+
51
+ def hash
52
+ self.class.hash
53
+ end
54
+
55
+ private
56
+
57
+ def value_class
58
+ LTree
59
+ end
60
+
61
+ def items_of(entry, sanitize)
62
+ return items_for(LTree.compatible(entry), sanitize: sanitize) if LTree.compatible?(entry)
63
+
64
+ case entry
65
+ when LTree, LQuery then entry.items
66
+ when ::Array then items_for(entry, sanitize: sanitize)
67
+ when ::ActiveRecord::Base then labels_of(record_label(entry), sanitize)
68
+ else labels_of(entry.to_s, sanitize)
69
+ end
70
+ end
71
+
72
+ def labels_of(text, sanitize)
73
+ text = sanitize(text) if sanitize
74
+ text.split('.').map { |item| parse(item) }
75
+ end
76
+
77
+ def record_label(record)
78
+ id = record.id
79
+ raise ArgumentError, <<~MSG.squish if id.nil?
80
+ Unable to use #{record.class.name} as a label because its
81
+ #{record.class.primary_key} is still empty.
82
+ MSG
83
+
84
+ raise ArgumentError, <<~MSG.squish if id.is_a?(::Array)
85
+ Unable to use #{record.class.name} as a label because it has a
86
+ composite primary key, which cannot be a single label.
87
+ MSG
88
+
89
+ id.to_s
90
+ end
91
+
92
+ def sanitize(text)
93
+ replacements = PostgreSQL.config.ltree.sanitize
94
+ return text if replacements.blank?
95
+
96
+ text.gsub(Regexp.union(replacements.keys), replacements)
97
+ end
98
+
99
+ def compile(item)
100
+ item.to_s
101
+ end
102
+
103
+ def parse(text)
104
+ text
105
+ end
106
+
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Torque
4
4
  module PostgreSQL
5
- class Segment < Struct.new(:point0, :point1)
5
+ class Segment < ::Struct.new(:point0, :point1)
6
6
  def x1=(value)
7
7
  self.point0 = new_point(value, y1)
8
8
  end
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Torque
4
+ module PostgreSQL
5
+ module Adapter
6
+ module OID
7
+ class Struct < ActiveRecord::Type::Json
8
+ attr_reader :klass, :type, :backfill
9
+
10
+ # A comparable representation of a struct-backed value, based on the
11
+ # values it exposes instead of the document it generates, so that
12
+ # non-deterministic types like encrypted attributes stay stable
13
+ def self.state(value)
14
+ case value
15
+ when nil then nil
16
+ when ::Array then value.map { |item| state(item) }
17
+ else value.respond_to?(:attributes) ? [value.class, value.attributes] : value
18
+ end
19
+ end
20
+
21
+ def initialize(klass, type: :jsonb, backfill: nil)
22
+ super()
23
+ @klass = klass
24
+ @type = type
25
+ @backfill = backfill
26
+ end
27
+
28
+ def cast(value)
29
+ case value
30
+ when klass then value
31
+ when ::Hash then build(value)
32
+ when ::String then cast(parse(value))
33
+ else value
34
+ end
35
+ end
36
+
37
+ def deserialize(value)
38
+ build(parse(value), from_database: true, blank: true)
39
+ end
40
+
41
+ def serialize(value)
42
+ case value
43
+ when klass then empty?(value) ? nil : super(document(value))
44
+ when nil then nil
45
+ else super
46
+ end
47
+ end
48
+
49
+ def changed?(old_value, new_value, _new_value_before_cast)
50
+ Struct.state(old_value) != Struct.state(new_value)
51
+ end
52
+
53
+ def changed_in_place?(raw_old_value, new_value)
54
+ Struct.state(without_backfill.deserialize(raw_old_value)) != Struct.state(new_value)
55
+ end
56
+
57
+ # The same type, but reading documents exactly as they are stored, so
58
+ # that backfilled properties are seen as changes
59
+ def without_backfill
60
+ return self if backfill.nil?
61
+ @without_backfill ||= self.class.new(klass, type: type)
62
+ end
63
+
64
+ def empty?(value)
65
+ document(value).empty?
66
+ end
67
+
68
+ # The value as it goes inside another document, which is a hash, and
69
+ # not the encoded JSON that a column receives
70
+ def document_for(value)
71
+ value.nil? ? nil : document(value)
72
+ end
73
+
74
+ # The hash that represents the value on the database, holding only the
75
+ # properties that were actually written to it. Properties backed by
76
+ # another struct are kept as documents, so that they are not stored as
77
+ # an encoded string inside this one
78
+ def document(value)
79
+ set = value.instance_variable_get(:@attributes)
80
+ set.keys.to_h do |name|
81
+ attribute = set[name]
82
+ type = OID.unwrap(attribute.type)
83
+
84
+ next [name, type.document_for(attribute.value)] if type.is_a?(Struct)
85
+ [name, attribute.value_for_database]
86
+ end
87
+ end
88
+
89
+ def blank_document
90
+ {}
91
+ end
92
+
93
+ def ==(other)
94
+ other.class == self.class && other.klass == klass && other.type == type
95
+ end
96
+ alias eql? ==
97
+
98
+ def hash
99
+ [self.class, klass, type].hash
100
+ end
101
+
102
+ private
103
+
104
+ def parse(value)
105
+ return value unless value.is_a?(::String)
106
+ ActiveSupport::JSON.decode(value) rescue nil
107
+ end
108
+
109
+ def build(data, from_database: false, blank: false)
110
+ instance = klass.new
111
+ blank!(instance) if blank
112
+ return instance unless data.is_a?(::Hash)
113
+
114
+ from_database ? load(instance, data) : instance.assign_attributes(data)
115
+ instance
116
+ end
117
+
118
+ # Reset the instance to the state of an empty document, keeping the
119
+ # class-level defaults that were marked to be backfilled
120
+ def blank!(instance)
121
+ return if backfill == true
122
+
123
+ keep = ::Array.wrap(backfill).map(&:to_s)
124
+ set = instance.instance_variable_get(:@attributes).map do |attribute|
125
+ next attribute if keep.include?(attribute.name)
126
+ ActiveModel::Attribute.uninitialized(attribute.name, attribute.type)
127
+ end
128
+
129
+ instance.instance_variable_set(:@attributes, set)
130
+ end
131
+
132
+ # Properties that the class doesn't declare are kept as they are
133
+ # stored, regardless of the class being strict about them or not
134
+ def load(instance, data)
135
+ default = ActiveModel::Type.default_value
136
+ set = instance.instance_variable_get(:@attributes)
137
+
138
+ data.each do |key, value|
139
+ if klass.attribute_names.include?(key = key.to_s)
140
+ set.write_from_database(key, value)
141
+ else
142
+ set[key] = ActiveModel::Attribute.from_database(key, value, default)
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Torque
4
+ module PostgreSQL
5
+ module Adapter
6
+ module OID
7
+ class StructList < Struct
8
+ def cast(value)
9
+ case value
10
+ when ::String then cast(parse(value))
11
+ when ::Array then value.map { |item| super(item) }
12
+ else value
13
+ end
14
+ end
15
+
16
+ def deserialize(value)
17
+ ::Array.wrap(parse(value)).map do |item|
18
+ build(item, from_database: true, blank: true)
19
+ end
20
+ end
21
+
22
+ def serialize(value)
23
+ case value
24
+ when ::Array then value.empty? ? nil : encode(value)
25
+ when nil then nil
26
+ else super
27
+ end
28
+ end
29
+
30
+ def document_for(value)
31
+ ::Array.wrap(value).map { |item| item.is_a?(klass) ? document(item) : item }
32
+ end
33
+
34
+ def empty?(value)
35
+ value.is_a?(::Array) ? value.empty? : super
36
+ end
37
+
38
+ def blank_document
39
+ []
40
+ end
41
+
42
+ private
43
+
44
+ def encode(items)
45
+ hashes = items.map { |item| item.is_a?(klass) ? document(item) : item }
46
+ ActiveSupport::JSON.encode(hashes)
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Torque
4
+ module PostgreSQL
5
+ module Adapter
6
+ module OID
7
+ class StructSet < ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array
8
+ def deserialize(value)
9
+ value.nil? ? [] : super
10
+ end
11
+
12
+ def serialize(value)
13
+ return if value.is_a?(::Array) && value.empty?
14
+ super
15
+ end
16
+
17
+ def changed_in_place?(raw_old_value, new_value)
18
+ Struct.state(without_backfill.deserialize(raw_old_value)) !=
19
+ Struct.state(new_value)
20
+ end
21
+
22
+ def without_backfill
23
+ return self if subtype.backfill.nil?
24
+ @without_backfill ||= self.class.new(subtype.without_backfill, delimiter)
25
+ end
26
+
27
+ def empty?(value)
28
+ value.is_a?(::Array) ? value.empty? : subtype.empty?(value)
29
+ end
30
+
31
+ def blank_document
32
+ []
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -1,2 +1,15 @@
1
1
  require_relative 'oid/array'
2
2
  require_relative 'oid/range'
3
+
4
+ module Torque
5
+ module PostgreSQL
6
+ module Adapter
7
+ module OID
8
+ def self.unwrap(type)
9
+ type = type.__getobj__ while type.is_a?(Delegator)
10
+ type
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -13,7 +13,7 @@ module Torque
13
13
 
14
14
  # Quotes type names for use in SQL queries.
15
15
  def quote_type_name(name, *args)
16
- QUOTED_TYPE_NAMES[args] ||= begin
16
+ QUOTED_TYPE_NAMES[[name.to_s, *args]] ||= begin
17
17
  name = name.to_s
18
18
  args << 'public' if args.empty? && !name.include?('.')
19
19
  quote_identifier_name(name, *args)
@@ -36,11 +36,20 @@ module Torque
36
36
  lookup_cast_type(column.sql_type)
37
37
  elsif column.is_a?(Column) && column.array?
38
38
  # When using +change_column_default+
39
- lookup_cast_type_from_column(column)
39
+ lookup_cast_type_for_column(column)
40
40
  end
41
41
 
42
42
  type.nil? ? super : quote(type.serialize(value.to_a))
43
43
  end
44
+
45
+ # Rails 8.1 dropped +lookup_cast_type_from_column+, and the sql_type of
46
+ # an array column does not carry the array part, so only the oid can
47
+ # resolve the real type
48
+ def lookup_cast_type_for_column(column)
49
+ return lookup_cast_type_from_column(column) unless AR810
50
+
51
+ type_map.lookup(column.oid, column.fmod, column.sql_type)
52
+ end
44
53
  end
45
54
  end
46
55
  end