torque-postgresql 4.1.0 → 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 (194) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +28 -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 +9 -0
  7. data/lib/torque/postgresql/adapter/oid/array.rb +0 -4
  8. data/lib/torque/postgresql/adapter/oid/composite.rb +2 -1
  9. data/lib/torque/postgresql/adapter/oid/enum.rb +1 -1
  10. data/lib/torque/postgresql/adapter/oid/enum_set.rb +2 -2
  11. data/lib/torque/postgresql/adapter/oid/interval.rb +1 -1
  12. data/lib/torque/postgresql/adapter/oid/lquery.rb +45 -28
  13. data/lib/torque/postgresql/adapter/oid/ltree.rb +67 -9
  14. data/lib/torque/postgresql/adapter/oid/struct.rb +1 -1
  15. data/lib/torque/postgresql/adapter/oid.rb +13 -0
  16. data/lib/torque/postgresql/adapter/schema_definitions.rb +5 -0
  17. data/lib/torque/postgresql/adapter/schema_dumper.rb +1 -0
  18. data/lib/torque/postgresql/arel/nodes.rb +51 -0
  19. data/lib/torque/postgresql/arel/visitors.rb +46 -0
  20. data/lib/torque/postgresql/attributes/base.rb +10 -7
  21. data/lib/torque/postgresql/attributes/composite.rb +1 -5
  22. data/lib/torque/postgresql/attributes/enum.rb +1 -6
  23. data/lib/torque/postgresql/attributes/lquery.rb +67 -127
  24. data/lib/torque/postgresql/attributes/ltree.rb +35 -83
  25. data/lib/torque/postgresql/base.rb +9 -1
  26. data/lib/torque/postgresql/config.rb +21 -3
  27. data/lib/torque/postgresql/geometry_builder.rb +4 -10
  28. data/lib/torque/postgresql/inheritance.rb +5 -5
  29. data/lib/torque/postgresql/predicate_builder/arel_attribute_handler.rb +1 -1
  30. data/lib/torque/postgresql/predicate_builder/array_handler.rb +16 -8
  31. data/lib/torque/postgresql/predicate_builder/composite_handler.rb +18 -5
  32. data/lib/torque/postgresql/predicate_builder/ltree_handler.rb +86 -18
  33. data/lib/torque/postgresql/predicate_builder/struct_handler.rb +29 -9
  34. data/lib/torque/postgresql/predicate_builder.rb +38 -11
  35. data/lib/torque/postgresql/railtie.rb +17 -0
  36. data/lib/torque/postgresql/relation/auxiliary_statement.rb +7 -3
  37. data/lib/torque/postgresql/relation/buckets.rb +2 -1
  38. data/lib/torque/postgresql/relation/cross_join.rb +193 -0
  39. data/lib/torque/postgresql/relation/inheritance.rb +1 -2
  40. data/lib/torque/postgresql/relation/join_series.rb +1 -0
  41. data/lib/torque/postgresql/relation/merger.rb +9 -0
  42. data/lib/torque/postgresql/relation/segments.rb +140 -0
  43. data/lib/torque/postgresql/relation.rb +23 -4
  44. data/lib/torque/postgresql/version.rb +1 -1
  45. data/lib/torque/postgresql/versioned_commands/command_migration.rb +23 -0
  46. data/lib/torque/postgresql/versioned_commands.rb +39 -0
  47. data/lib/torque/postgresql.rb +0 -2
  48. data/sig/generators/torque/function_generator.rbs +9 -0
  49. data/sig/generators/torque/skill_generator.rbs +7 -0
  50. data/sig/generators/torque/trigger_generator.rbs +9 -0
  51. data/sig/generators/torque/type_generator.rbs +9 -0
  52. data/sig/generators/torque/view_generator.rbs +9 -0
  53. data/sig/manifest.yaml +7 -0
  54. data/sig/torque/postgresql/adapter/database_statements.rbs +42 -0
  55. data/sig/torque/postgresql/adapter/inheritance_statements.rbs +36 -0
  56. data/sig/torque/postgresql/adapter/oid/array.rbs +20 -0
  57. data/sig/torque/postgresql/adapter/oid/box.rbs +28 -0
  58. data/sig/torque/postgresql/adapter/oid/circle.rbs +33 -0
  59. data/sig/torque/postgresql/adapter/oid/composite.rbs +35 -0
  60. data/sig/torque/postgresql/adapter/oid/enum.rbs +28 -0
  61. data/sig/torque/postgresql/adapter/oid/enum_set.rbs +19 -0
  62. data/sig/torque/postgresql/adapter/oid/interval.rbs +20 -0
  63. data/sig/torque/postgresql/adapter/oid/line.rbs +33 -0
  64. data/sig/torque/postgresql/adapter/oid/lquery.rbs +21 -0
  65. data/sig/torque/postgresql/adapter/oid/ltree.rbs +32 -0
  66. data/sig/torque/postgresql/adapter/oid/range.rbs +28 -0
  67. data/sig/torque/postgresql/adapter/oid/segment.rbs +38 -0
  68. data/sig/torque/postgresql/adapter/oid/struct.rbs +41 -0
  69. data/sig/torque/postgresql/adapter/oid/struct_list.rbs +20 -0
  70. data/sig/torque/postgresql/adapter/oid/struct_set.rbs +18 -0
  71. data/sig/torque/postgresql/adapter/oid.rbs +9 -0
  72. data/sig/torque/postgresql/adapter/quoting.rbs +20 -0
  73. data/sig/torque/postgresql/adapter/schema_creation.rbs +13 -0
  74. data/sig/torque/postgresql/adapter/schema_definitions.rbs +55 -0
  75. data/sig/torque/postgresql/adapter/schema_dumper.rbs +37 -0
  76. data/sig/torque/postgresql/adapter/schema_overrides.rbs +46 -0
  77. data/sig/torque/postgresql/adapter/schema_statements.rbs +50 -0
  78. data/sig/torque/postgresql/adapter.rbs +24 -0
  79. data/sig/torque/postgresql/arel/infix_operation.rbs +79 -0
  80. data/sig/torque/postgresql/arel/join_source.rbs +19 -0
  81. data/sig/torque/postgresql/arel/nodes.rbs +83 -0
  82. data/sig/torque/postgresql/arel/operations.rbs +28 -0
  83. data/sig/torque/postgresql/arel/select_manager.rbs +15 -0
  84. data/sig/torque/postgresql/arel/visitors.rbs +30 -0
  85. data/sig/torque/postgresql/arel.rbs +6 -0
  86. data/sig/torque/postgresql/associations/association_scope.rbs +28 -0
  87. data/sig/torque/postgresql/associations/belongs_to_many_association.rbs +51 -0
  88. data/sig/torque/postgresql/associations/builder/belongs_to_many.rbs +24 -0
  89. data/sig/torque/postgresql/associations/builder/has_many.rbs +15 -0
  90. data/sig/torque/postgresql/associations/builder.rbs +8 -0
  91. data/sig/torque/postgresql/associations/foreign_association.rbs +16 -0
  92. data/sig/torque/postgresql/associations/join_dependency.rbs +16 -0
  93. data/sig/torque/postgresql/associations/preloader/association.rbs +32 -0
  94. data/sig/torque/postgresql/associations/preloader/loader_query.rbs +20 -0
  95. data/sig/torque/postgresql/associations/preloader.rbs +8 -0
  96. data/sig/torque/postgresql/associations.rbs +17 -0
  97. data/sig/torque/postgresql/attributes/base.rbs +37 -0
  98. data/sig/torque/postgresql/attributes/builder/enum.rbs +38 -0
  99. data/sig/torque/postgresql/attributes/builder/full_text_search.rbs +27 -0
  100. data/sig/torque/postgresql/attributes/builder/period.rbs +106 -0
  101. data/sig/torque/postgresql/attributes/builder.rbs +19 -0
  102. data/sig/torque/postgresql/attributes/composite.rbs +28 -0
  103. data/sig/torque/postgresql/attributes/enum.rbs +69 -0
  104. data/sig/torque/postgresql/attributes/enum_set.rbs +75 -0
  105. data/sig/torque/postgresql/attributes/full_text_search.rbs +13 -0
  106. data/sig/torque/postgresql/attributes/lazy.rbs +17 -0
  107. data/sig/torque/postgresql/attributes/lquery.rbs +48 -0
  108. data/sig/torque/postgresql/attributes/ltree.rbs +46 -0
  109. data/sig/torque/postgresql/attributes/period.rbs +13 -0
  110. data/sig/torque/postgresql/attributes/simple_enum.rbs +39 -0
  111. data/sig/torque/postgresql/attributes/struct.rbs +38 -0
  112. data/sig/torque/postgresql/attributes.rbs +6 -0
  113. data/sig/torque/postgresql/autosave_association.rbs +16 -0
  114. data/sig/torque/postgresql/auxiliary_statement/recursive.rbs +22 -0
  115. data/sig/torque/postgresql/auxiliary_statement/settings.rbs +63 -0
  116. data/sig/torque/postgresql/auxiliary_statement.rbs +57 -0
  117. data/sig/torque/postgresql/base.rbs +41 -0
  118. data/sig/torque/postgresql/collector.rbs +7 -0
  119. data/sig/torque/postgresql/config.rbs +9 -0
  120. data/sig/torque/postgresql/function.rbs +21 -0
  121. data/sig/torque/postgresql/geometry_builder.rbs +20 -0
  122. data/sig/torque/postgresql/i18n.rbs +15 -0
  123. data/sig/torque/postgresql/inheritance/expander.rbs +23 -0
  124. data/sig/torque/postgresql/inheritance/record.rbs +20 -0
  125. data/sig/torque/postgresql/inheritance.rbs +47 -0
  126. data/sig/torque/postgresql/insert_all.rbs +22 -0
  127. data/sig/torque/postgresql/migration/command_recorder.rbs +30 -0
  128. data/sig/torque/postgresql/migration.rbs +6 -0
  129. data/sig/torque/postgresql/predicate_builder/arel_attribute_handler.rbs +15 -0
  130. data/sig/torque/postgresql/predicate_builder/array_handler.rbs +26 -0
  131. data/sig/torque/postgresql/predicate_builder/composite_handler.rbs +26 -0
  132. data/sig/torque/postgresql/predicate_builder/enumerator_lazy_handler.rbs +18 -0
  133. data/sig/torque/postgresql/predicate_builder/ltree_handler.rbs +36 -0
  134. data/sig/torque/postgresql/predicate_builder/regexp_handler.rbs +16 -0
  135. data/sig/torque/postgresql/predicate_builder/struct_handler.rbs +24 -0
  136. data/sig/torque/postgresql/predicate_builder.rbs +23 -0
  137. data/sig/torque/postgresql/predicate_table.rbs +24 -0
  138. data/sig/torque/postgresql/railtie.rbs +10 -0
  139. data/sig/torque/postgresql/reflection/abstract_reflection.rbs +22 -0
  140. data/sig/torque/postgresql/reflection/association_reflection.rbs +18 -0
  141. data/sig/torque/postgresql/reflection/belongs_to_many_reflection.rbs +30 -0
  142. data/sig/torque/postgresql/reflection/has_many_reflection.rbs +14 -0
  143. data/sig/torque/postgresql/reflection/runtime_reflection.rbs +18 -0
  144. data/sig/torque/postgresql/reflection/through_reflection.rbs +14 -0
  145. data/sig/torque/postgresql/reflection.rbs +11 -0
  146. data/sig/torque/postgresql/relation/auxiliary_statement.rbs +29 -0
  147. data/sig/torque/postgresql/relation/buckets.rbs +35 -0
  148. data/sig/torque/postgresql/relation/cross_join.rbs +63 -0
  149. data/sig/torque/postgresql/relation/distinct_on.rbs +16 -0
  150. data/sig/torque/postgresql/relation/inheritance.rbs +48 -0
  151. data/sig/torque/postgresql/relation/join_series.rbs +28 -0
  152. data/sig/torque/postgresql/relation/merger.rbs +23 -0
  153. data/sig/torque/postgresql/relation/segments.rbs +27 -0
  154. data/sig/torque/postgresql/relation.rbs +48 -0
  155. data/sig/torque/postgresql/schema_cache/bound_schema_reflection.rbs +14 -0
  156. data/sig/torque/postgresql/schema_cache/inheritance.rbs +12 -0
  157. data/sig/torque/postgresql/schema_cache/schema_reflection.rbs +14 -0
  158. data/sig/torque/postgresql/schema_cache.rbs +43 -0
  159. data/sig/torque/postgresql/table_name.rbs +20 -0
  160. data/sig/torque/postgresql/validations.rbs +13 -0
  161. data/sig/torque/postgresql/version.rbs +5 -0
  162. data/sig/torque/postgresql/versioned_commands/command_migration.rbs +54 -0
  163. data/sig/torque/postgresql/versioned_commands/generator.rbs +17 -0
  164. data/sig/torque/postgresql/versioned_commands/migration_context.rbs +30 -0
  165. data/sig/torque/postgresql/versioned_commands/migrator.rbs +19 -0
  166. data/sig/torque/postgresql/versioned_commands/schema_table.rbs +23 -0
  167. data/sig/torque/postgresql/versioned_commands.rbs +25 -0
  168. data/sig/torque/postgresql.rbs +26 -0
  169. data/skills/torque-postgresql/SKILL.md +70 -0
  170. data/spec/fixtures/migrations/20250101000008_create_trigger_audit_users_v1.sql +3 -0
  171. data/spec/initialize.rb +9 -0
  172. data/spec/mocks/cache_query.rb +1 -1
  173. data/spec/rbs_helper.rb +8 -0
  174. data/spec/schema.rb +2 -0
  175. data/spec/spec_helper.rb +1 -0
  176. data/spec/tests/arel_spec.rb +1 -3
  177. data/spec/tests/auxiliary_statement_spec.rb +12 -0
  178. data/spec/tests/buckets_spec.rb +122 -0
  179. data/spec/tests/composite_spec.rb +67 -0
  180. data/spec/tests/cross_join_spec.rb +185 -0
  181. data/spec/tests/distinct_on_spec.rb +5 -0
  182. data/spec/tests/enum_set_spec.rb +4 -0
  183. data/spec/tests/enum_spec.rb +20 -4
  184. data/spec/tests/geometric_builder_spec.rb +16 -5
  185. data/spec/tests/has_many_spec.rb +2 -1
  186. data/spec/tests/interval_spec.rb +6 -0
  187. data/spec/tests/ltree_spec.rb +398 -64
  188. data/spec/tests/predicate_builder_spec.rb +120 -0
  189. data/spec/tests/relation_spec.rb +25 -69
  190. data/spec/tests/segments_spec.rb +264 -0
  191. data/spec/tests/struct_spec.rb +202 -0
  192. data/spec/tests/table_inheritance_spec.rb +10 -0
  193. data/spec/tests/versioned_commands_spec.rb +145 -0
  194. metadata +153 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 386d035c1a009d6c67f130c50baa9e0db1c417aa79b6190834780855ca92d2ee
4
- data.tar.gz: 468a10d17d4ac97b4e83914be5a539e1b2ca78cbb54ae35e9c80fbee41fda18f
3
+ metadata.gz: c79e4ae9bb15bd5111bdd7f99e93b2be6aeb8dbe8446005f1692b3d419a6a096
4
+ data.tar.gz: fc41a9e09f887d8d6d9dd3920e2bc3b2d51e3b17af48b76e875c37d7a37a992a
5
5
  SHA512:
6
- metadata.gz: 503443151d99b82e2e76f1d6ed34b6a314e0b8cdd9cd576933879a7a807d33cecd926c48d3809a01fd785d8777d5ae0135c46fb0d9b4c4c347b4457e807cb6ba
7
- data.tar.gz: a7bce5728938390a5bb285185c592dfcaf44f5322fb30b46fbc08ba5edeaf7a30d932177f91a685afa35153ff2ed8c5200edeb88058d571f2402f8c9a204b32e
6
+ metadata.gz: 7b98f253f6b1b47a1ed18a07e1850793b127847b144a0a75b61f3a96a439b6473117ab769f5a17db17820bd498a81d7adf06f73222c48020fff0b6505fa210e2
7
+ data.tar.gz: eeb9cbb8dfff58d3d3836e5e7f8199fbf77a7048b387dbde41de77a98ff1254784cf02f0c702d6513c4623b7724da8f70e1829e9d16be26fd0d6f55a9e8cd046
data/Rakefile CHANGED
@@ -56,3 +56,31 @@ end
56
56
  require 'rspec/core/rake_task'
57
57
  RSpec::Core::RakeTask.new(:spec)
58
58
  task default: :spec
59
+
60
+ RBS_TEST_ENV = {
61
+ 'RBS_TEST' => '1',
62
+ 'RBS_TEST_TARGET' => 'Torque::PostgreSQL::*,Torque::Generators::*',
63
+ 'RBS_TEST_SKIP' => '::Torque::PostgreSQL::Attributes::Lazy,::Torque::PostgreSQL::Adapter::OID::Range::Comparison',
64
+ 'RBS_TEST_OPT' => '-I sig',
65
+ 'RBS_TEST_LOGLEVEL' => ENV.fetch('RBS_TEST_LOGLEVEL', 'warn'),
66
+ }.freeze
67
+
68
+ namespace :rbs do
69
+ desc 'Install the third-party signatures listed in rbs_collection.yaml'
70
+ task :collection do
71
+ sh 'bundle', 'exec', 'rbs', 'collection', 'install'
72
+ end
73
+
74
+ desc 'Validate sig/ against the collection and the local stubs'
75
+ task validate: :collection do
76
+ sh 'bundle', 'exec', 'rbs', '-I', 'sig', 'validate', '--exit-error-on-syntax-error'
77
+ end
78
+
79
+ desc 'Run the spec suite with runtime type checking of every signature'
80
+ task test: :collection do
81
+ sh RBS_TEST_ENV, 'bundle', 'exec', 'rspec'
82
+ end
83
+ end
84
+
85
+ desc 'Validate the signatures and run the typed spec suite'
86
+ task rbs: %w[rbs:validate rbs:test]
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators/base'
4
+
5
+ module Torque
6
+ module Generators
7
+ class SkillGenerator < Rails::Generators::Base
8
+ source_root File.expand_path('../../../skills', __dir__)
9
+
10
+ desc 'Copies the torque-postgresql agent skill into .claude/skills'
11
+
12
+ def copy_skill
13
+ directory 'torque-postgresql', '.claude/skills/torque-postgresql'
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,3 @@
1
+ CREATE OR REPLACE TRIGGER <%= name %>
2
+ BEFORE INSERT ON table_name
3
+ FOR EACH ROW EXECUTE FUNCTION function_name();
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'torque/postgresql/versioned_commands/generator'
4
+
5
+ module Torque
6
+ module Generators
7
+ class TriggerGenerator < Rails::Generators::Base
8
+ include Torque::PostgreSQL::VersionedCommands::Generator
9
+
10
+ alias create_trigger_file create_migration_file
11
+ end
12
+ end
13
+ end
@@ -253,6 +253,15 @@ module Torque
253
253
  WHERE 1=1 AND c.relkind IN ('v', 'm')
254
254
  AND #{filter_by_schema.join(' AND ')};
255
255
  SQL
256
+ when :trigger
257
+ <<-SQL.squish
258
+ SELECT n.nspname AS schema, t.tgname AS name
259
+ FROM pg_trigger t
260
+ INNER JOIN pg_class c ON c.oid = t.tgrelid
261
+ INNER JOIN pg_namespace n ON n.oid = c.relnamespace
262
+ WHERE 1=1 AND NOT t.tgisinternal
263
+ AND #{filter_by_schema.join(' AND ')};
264
+ SQL
256
265
  end
257
266
 
258
267
  select_rows(query, 'SCHEMA')
@@ -11,10 +11,6 @@ module Torque
11
11
 
12
12
  private
13
13
 
14
- # A label path is an Array of labels, so the regular recursion would
15
- # take each of its labels as another dimension of the column. Paths
16
- # are a single value to PostgreSQL, which means that the entries of
17
- # the array are never a dimension of their own
18
14
  def type_cast_array(value, method)
19
15
  return super unless path_subtype?
20
16
  return subtype.public_send(method, value) unless value.is_a?(::Array)
@@ -13,8 +13,9 @@ module Torque
13
13
  attr_reader :name
14
14
 
15
15
  # The composite type behind the given type, if there is one, wrapped
16
- # or not by an array
16
+ # or not by an array or a delegator
17
17
  def self.from(type)
18
+ type = OID.unwrap(type)
18
19
  type = type.subtype if type.is_a?(ARRAY)
19
20
  type if type.is_a?(self)
20
21
  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
@@ -1,50 +1,67 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'ltree'
4
+
3
5
  module Torque
4
6
  module PostgreSQL
5
7
  module Adapter
6
8
  module OID
7
- class Lquery < ActiveModel::Type::Value
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/
8
14
 
9
15
  def type
10
16
  :lquery
11
17
  end
12
18
 
13
- def cast(value)
14
- return if value.blank?
15
- return value if value.is_a?(LQuery)
19
+ private
16
20
 
17
- LQuery.new(value)
18
- end
21
+ def value_class
22
+ LQuery
23
+ end
19
24
 
20
- def deserialize(value)
21
- return if value.nil?
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
22
33
 
23
- LQuery.load(value)
24
- end
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
25
42
 
26
- def serialize(value)
27
- cast(value)&.to_s
28
- end
43
+ def quantifier(range)
44
+ min = range.begin
45
+ max = range.end
46
+ max -= 1 if max && range.exclude_end?
29
47
 
30
- def type_cast_for_schema(value)
31
- cast(value).to_s.inspect
32
- end
48
+ return '*' if min.nil? && max.nil?
49
+ return "*{#{min}}" if min == max
33
50
 
34
- def changed_in_place?(raw_old_value, new_value)
35
- raw_old_value != serialize(new_value)
36
- end
51
+ "*{#{min},#{max}}"
52
+ end
37
53
 
38
- # Types resolved through the type map are deduplicated and frozen, so
39
- # they need to be comparable
40
- def ==(other)
41
- other.is_a?(self.class)
42
- end
43
- alias eql? ==
54
+ def parse(text)
55
+ return :any if text == '*'
56
+ return text.split('|') if text.include?('|') && !text.match?(/[!{]/)
44
57
 
45
- def hash
46
- self.class.hash
47
- end
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
48
65
 
49
66
  end
50
67
  end
@@ -4,6 +4,9 @@ module Torque
4
4
  module PostgreSQL
5
5
  module Adapter
6
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
7
10
  class Ltree < ActiveModel::Type::Value
8
11
 
9
12
  def type
@@ -11,34 +14,37 @@ module Torque
11
14
  end
12
15
 
13
16
  def cast(value)
17
+ return value if value.is_a?(value_class)
14
18
  return if value.blank?
15
- return value if value.is_a?(LTree)
16
19
 
17
- LTree.new(value)
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?
18
26
  end
19
27
 
20
28
  def deserialize(value)
21
29
  return if value.nil?
22
30
 
23
- LTree.load(value)
31
+ value_class.new(value, sanitize: false)
24
32
  end
25
33
 
26
- def serialize(value)
27
- cast(value)&.to_s
34
+ def items_for(value, sanitize: true)
35
+ ::Array.wrap(value).flat_map { |entry| items_of(entry, sanitize) }
28
36
  end
29
37
 
30
38
  def type_cast_for_schema(value)
31
- cast(value).to_s.inspect
39
+ serialize(value).inspect
32
40
  end
33
41
 
34
42
  def changed_in_place?(raw_old_value, new_value)
35
43
  raw_old_value != serialize(new_value)
36
44
  end
37
45
 
38
- # Types resolved through the type map are deduplicated and frozen, so
39
- # they need to be comparable
40
46
  def ==(other)
41
- other.is_a?(self.class)
47
+ other.class == self.class
42
48
  end
43
49
  alias eql? ==
44
50
 
@@ -46,6 +52,58 @@ module Torque
46
52
  self.class.hash
47
53
  end
48
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
+
49
107
  end
50
108
  end
51
109
  end
@@ -79,7 +79,7 @@ module Torque
79
79
  set = value.instance_variable_get(:@attributes)
80
80
  set.keys.to_h do |name|
81
81
  attribute = set[name]
82
- type = attribute.type
82
+ type = OID.unwrap(attribute.type)
83
83
 
84
84
  next [name, type.document_for(attribute.value)] if type.is_a?(Struct)
85
85
  [name, attribute.value_for_database]
@@ -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
@@ -115,6 +115,11 @@ module Torque
115
115
  execute VersionedCommands.fetch_command(dir, :view, name, version)
116
116
  end
117
117
 
118
+ def create_trigger(name, version:, dir: pool.migrations_paths)
119
+ return super unless VersionedCommands.valid_type?(:trigger)
120
+ execute VersionedCommands.fetch_command(dir, :trigger, name, version)
121
+ end
122
+
118
123
  end
119
124
 
120
125
  ActiveRecord::ConnectionAdapters::PostgreSQL::Table.include ColumnMethods
@@ -72,6 +72,7 @@ module Torque
72
72
 
73
73
  yield
74
74
  versioned_commands(stream, :view, true)
75
+ versioned_commands(stream, :trigger, true)
75
76
 
76
77
  functions(stream) if fx_functions_position == :end
77
78
  triggers(stream) if defined?(::Fx::SchemaDumper::Trigger)
@@ -81,6 +81,57 @@ module Torque
81
81
  end
82
82
  end
83
83
 
84
+ # A cross join, which PostgreSQL needs spelled out because an inner
85
+ # join without a condition is a syntax error
86
+ class CrossJoin < ::Arel::Nodes::Join
87
+ def initialize(left, right = nil)
88
+ super
89
+ end
90
+ end
91
+
92
+ # An aliased FROM item. +definitions+ maps a column name to the SQL
93
+ # type a function has to be told to return, +columns+ are the plain
94
+ # names the alias exposes, +ordinality+ names the appended counter,
95
+ # and +lateral+ is only needed when the source is a subquery
96
+ class FromItem < ::Arel::Nodes::Binary
97
+ attr_reader :columns, :definitions, :ordinality, :lateral
98
+
99
+ alias name right
100
+
101
+ def initialize(source, name, columns: nil, definitions: nil, ordinality: nil, lateral: false)
102
+ @columns = Array.wrap(columns).presence&.freeze
103
+ @definitions = definitions.presence&.freeze
104
+ @ordinality = ordinality&.to_s
105
+ @lateral = lateral
106
+
107
+ super(source, name.to_s)
108
+ end
109
+
110
+ # The definitions can only be moved into the source when the counter
111
+ # forces the +ROWS FROM+ form, since PostgreSQL refuses a column
112
+ # definition list next to +WITH ORDINALITY+
113
+ def rows_from?
114
+ ordinality.present? && definitions.present?
115
+ end
116
+
117
+ # What the alias exposes, as name => type, where a +nil+ type means
118
+ # the name stands on its own. Types are only spelled out here when
119
+ # they have not been moved into the +ROWS FROM+ source.
120
+ #
121
+ # The list is all or nothing, because PostgreSQL matches it
122
+ # positionally: a partial list silently renames the columns it does
123
+ # reach instead of naming the ones it does not
124
+ def alias_columns
125
+ return {} if columns.nil? && definitions.nil?
126
+
127
+ plain = rows_from? ? definitions.keys : columns
128
+ result = plain.nil? ? definitions : plain.to_h { |name| [name.to_s, nil] }
129
+
130
+ return result if ordinality.blank?
131
+ result.merge(ordinality => nil)
132
+ end
133
+ end
134
+
84
135
  class Ref < ::Arel::Nodes::Unary
85
136
  attr_reader :reference
86
137
  alias to_s expr
@@ -40,6 +40,33 @@ module Torque
40
40
  collector
41
41
  end
42
42
 
43
+ # A cross join only ever carries its source
44
+ def visit_Torque_PostgreSQL_Arel_Nodes_CrossJoin(o, collector)
45
+ collector << 'CROSS JOIN '
46
+ visit(o.left, collector)
47
+ end
48
+
49
+ # Every part here is written only when it is actually needed, since
50
+ # LATERAL is a noise word on functions and ROWS FROM is only forced
51
+ # by the counter
52
+ def visit_Torque_PostgreSQL_Arel_Nodes_FromItem(o, collector)
53
+ collector << 'LATERAL ' if o.lateral
54
+
55
+ collector << 'ROWS FROM (' if o.rows_from?
56
+ visit(o.left, collector)
57
+ collector = visit_definitions(o.definitions, collector) << ')' if o.rows_from?
58
+
59
+ collector << ' WITH ORDINALITY' if o.ordinality.present?
60
+ collector << ' ' << quote_table_name(o.name)
61
+
62
+ columns = o.alias_columns
63
+ return collector if columns.blank?
64
+
65
+ collector << '('
66
+ visit_alias_columns(columns, collector)
67
+ collector << ')'
68
+ end
69
+
43
70
  # Allow casting any node
44
71
  def visit_Torque_PostgreSQL_Arel_Nodes_Cast(o, collector)
45
72
  visit(o.left, collector) << '::' << o.right
@@ -47,6 +74,25 @@ module Torque
47
74
 
48
75
  private
49
76
 
77
+ # The column definition list a function has to be told to return
78
+ def visit_definitions(definitions, collector)
79
+ collector << ' AS ('
80
+ visit_alias_columns(definitions, collector)
81
+ collector << ')'
82
+ end
83
+
84
+ # A name on its own when it has no type, otherwise the name and the
85
+ # type it is being given
86
+ def visit_alias_columns(columns, collector)
87
+ columns.each_with_index do |(name, type), index|
88
+ collector << ', ' unless index.zero?
89
+ collector << quote_column_name(name)
90
+ collector << ' ' << type if type.present?
91
+ end
92
+
93
+ collector
94
+ end
95
+
50
96
  def quote_array(value, collector)
51
97
  value = value.map(&::Arel::Nodes.method(:build_quoted))
52
98
 
@@ -19,11 +19,11 @@ module Torque
19
19
 
20
20
  include SimpleEnum
21
21
  include ActiveRecord::Store
22
- # Rails 8.1 moved normalization to Active Model, which is where these
23
- # classes belong anyway
24
22
  include(AR810 ? ActiveModel::Attributes::Normalization : ActiveRecord::Normalization)
25
23
  include ActiveRecord::Encryption::EncryptableRecord
26
24
 
25
+ class_attribute :filter_attributes, instance_accessor: false, default: []
26
+
27
27
  class << self
28
28
  # These classes are not backed by a table, so nothing here has the
29
29
  # extra information that a column would provide
@@ -43,6 +43,14 @@ module Torque
43
43
 
44
44
  super
45
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
46
54
  end
47
55
 
48
56
  # Plain access to any property, declared or not
@@ -72,11 +80,6 @@ module Torque
72
80
  attributes.symbolize_keys
73
81
  end
74
82
 
75
- def inspect
76
- entries = attributes.map { |key, value| "#{key}: #{value.inspect}" }
77
- "#<#{self.class.name} #{entries.join(', ')}>"
78
- end
79
-
80
83
  def type_for_attribute(name, &block)
81
84
  self.class.type_for_attribute(name, &block)
82
85
  end
@@ -48,7 +48,7 @@ module Torque
48
48
  @load_columns_monitor.synchronize do
49
49
  next if defined?(@columns)
50
50
 
51
- columns = connection.composite_column_types(type_name)
51
+ columns = ActiveRecord::Base.with_connection { |c| c.composite_column_types(type_name) }
52
52
  columns.each do |attr_name, attr_type|
53
53
  attribute(attr_name, attr_type) unless attribute_names.include?(attr_name)
54
54
  end
@@ -89,10 +89,6 @@ module Torque
89
89
  klass.type_name = name
90
90
  klass
91
91
  end
92
-
93
- def connection
94
- ActiveRecord::Base.connection
95
- end
96
92
  end
97
93
  end
98
94
  end
@@ -51,7 +51,7 @@ module Torque
51
51
 
52
52
  @load_values_monitor.synchronize do
53
53
  next if defined?(@values)
54
- @values = connection.enum_values(type_name).freeze
54
+ @values = ::ActiveRecord::Base.with_connection { |c| c.enum_values(type_name) }.freeze
55
55
  end
56
56
 
57
57
  @values
@@ -115,11 +115,6 @@ module Torque
115
115
  valid?(method_name) ? new(method_name.to_s) : super
116
116
  end
117
117
 
118
- # Get a connection based on its name
119
- def connection
120
- ::ActiveRecord::Base.connection
121
- end
122
-
123
118
  end
124
119
 
125
120
  # Override string initializer to check for a valid value