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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9e1560c305d73659df33b4dcc71dd57376ab72d898ef1848e54c8aaa5689584e
4
- data.tar.gz: 0fc8842730a9970f999b52c6a10feb728f16b79086b4389f114ccca12e24f5a9
3
+ metadata.gz: c79e4ae9bb15bd5111bdd7f99e93b2be6aeb8dbe8446005f1692b3d419a6a096
4
+ data.tar.gz: fc41a9e09f887d8d6d9dd3920e2bc3b2d51e3b17af48b76e875c37d7a37a992a
5
5
  SHA512:
6
- metadata.gz: 9985681094dc57004a8d823f5f13d5383d965304336d1b9e4a886fa812e8b9dfca5af93504a7b46ed58e7e508fd0d3c8048d367b5b8c36f42f9dc293f2f556bf
7
- data.tar.gz: 4b23fe90238cb6f0f7c424631e381c27b15bc932e69d46d68dfba9f96a39081d40b80a0ff61266ab1122c24f10242c26a1728e5e737ef847a49c403e8b3e9c88
6
+ metadata.gz: 7b98f253f6b1b47a1ed18a07e1850793b127847b144a0a75b61f3a96a439b6473117ab769f5a17db17820bd498a81d7adf06f73222c48020fff0b6505fa210e2
7
+ data.tar.gz: eeb9cbb8dfff58d3d3836e5e7f8199fbf77a7048b387dbde41de77a98ff1254784cf02f0c702d6513c4623b7724da8f70e1829e9d16be26fd0d6f55a9e8cd046
data/Rakefile CHANGED
@@ -4,8 +4,32 @@ rescue LoadError
4
4
  puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
5
  end
6
6
 
7
+ require 'bundler/gem_tasks'
7
8
  require 'rdoc/task'
8
9
 
10
+ DOCS_PATH = File.expand_path('docs', __dir__)
11
+
12
+ desc 'Compile the documentation site into docs/_site'
13
+ task :docs do
14
+ version = File.read(File.expand_path('lib/torque/postgresql/version.rb', __dir__))[/VERSION = '([^']+)'/, 1]
15
+ published = File.read(File.join(DOCS_PATH, '_config.yml'))[/^gem_version:\s*(\S+)/, 1]
16
+
17
+ unless published == version
18
+ abort <<~MESSAGE
19
+ docs/_config.yml says gem_version: #{published}, but the gem is #{version}.
20
+ The site prints that number in its header, so update _config.yml before building.
21
+ MESSAGE
22
+ end
23
+
24
+ env = { 'BUNDLE_GEMFILE' => File.join(DOCS_PATH, 'Gemfile') }
25
+ Dir.chdir(DOCS_PATH) { sh(env, 'bundle', 'exec', 'jekyll', 'build') }
26
+ end
27
+
28
+ # A gem cut from master carries freshly compiled docs; every other branch skips
29
+ # the cost. Publishing docs/_site stays a manual step.
30
+ branch = `git rev-parse --abbrev-ref HEAD 2>/dev/null`.strip
31
+ Rake::Task[:build].enhance([:docs]) if branch == 'master'
32
+
9
33
  RDoc::Task.new(:rdoc) do |rdoc|
10
34
  rdoc.rdoc_dir = 'rdoc'
11
35
  rdoc.title = 'Torque::Postgresql'
@@ -32,3 +56,31 @@ end
32
56
  require 'rspec/core/rake_task'
33
57
  RSpec::Core::RakeTask.new(:spec)
34
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
@@ -5,7 +5,7 @@ module Torque
5
5
  module Adapter
6
6
  module DatabaseStatements
7
7
 
8
- EXTENDED_DATABASE_TYPES = %i[enum enum_set interval]
8
+ EXTENDED_DATABASE_TYPES = %i[enum enum_set interval composite ltree lquery]
9
9
 
10
10
  # Switch between dump mode or not
11
11
  def dump_mode!
@@ -73,6 +73,11 @@ module Torque
73
73
  if PostgreSQL.config.interval.enabled
74
74
  m.register_type 'interval', OID::Interval.new
75
75
  end
76
+
77
+ if PostgreSQL.config.ltree.enabled
78
+ m.register_type 'ltree', OID::Ltree.new
79
+ m.register_type 'lquery', OID::Lquery.new
80
+ end
76
81
  end
77
82
 
78
83
  # :nodoc:
@@ -92,9 +97,10 @@ module Torque
92
97
 
93
98
  query = <<~SQL
94
99
  SELECT t.oid, t.typname, t.typelem, t.typdelim, t.typinput,
95
- r.rngsubtype, t.typtype, t.typbasetype, t.typarray
100
+ r.rngsubtype, t.typtype, t.typbasetype, t.typarray, c.relkind
96
101
  FROM pg_type as t
97
- LEFT JOIN pg_range as r ON oid = rngtypid
102
+ LEFT JOIN pg_range as r ON t.oid = rngtypid
103
+ LEFT JOIN pg_class as c ON c.oid = t.typrelid
98
104
  LEFT JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace
99
105
  WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')
100
106
  SQL
@@ -102,19 +108,55 @@ module Torque
102
108
  if oids
103
109
  query += " AND t.oid IN (%s)" % oids.join(", ")
104
110
  else
105
- query += " AND t.typtype IN ('e')"
111
+ conditions = []
112
+ conditions << "t.typtype IN ('e')" if PostgreSQL.config.enum.enabled
113
+ conditions << "(t.typtype = 'c' AND c.relkind = 'c')" \
114
+ if PostgreSQL.config.composite.enabled
115
+
116
+ query += " AND (#{conditions.join(' OR ')})"
106
117
  end
107
118
 
108
119
  options = { allow_retry: true, materialize_transactions: false }
109
120
  internal_execute(query, 'SCHEMA', **options).each do |row|
110
- if row['typtype'] == 'e' && PostgreSQL.config.enum.enabled
111
- OID::Enum.create(row, type_map)
121
+ if row['typtype'] == 'e'
122
+ OID::Enum.create(row, type_map) if PostgreSQL.config.enum.enabled
123
+ elsif row['typtype'] == 'c' && row['relkind'] == 'c'
124
+ OID::Composite.create(row, type_map) if PostgreSQL.config.composite.enabled
112
125
  end
113
126
  end
114
127
  end
115
128
 
116
129
  def torque_load_additional_types?
117
- PostgreSQL.config.enum.enabled
130
+ PostgreSQL.config.enum.enabled || PostgreSQL.config.composite.enabled
131
+ end
132
+
133
+ # Get the ordered list of columns of a composite type, as a hash of
134
+ # column name and its proper cast type
135
+ def composite_column_types(name)
136
+ rows = query(<<~SQL, 'SCHEMA')
137
+ SELECT a.attname, a.atttypid, a.atttypmod
138
+ FROM pg_attribute a
139
+ WHERE a.attrelid = #{quote(quote_type_name(name).to_s)}::regclass
140
+ AND a.attnum > 0 AND NOT a.attisdropped
141
+ ORDER BY a.attnum
142
+ SQL
143
+
144
+ rows.each_with_object({}) do |(attr_name, oid, fmod), hash|
145
+ hash[attr_name] = get_oid_type(oid.to_i, fmod.to_i, attr_name)
146
+ end
147
+ end
148
+
149
+ # Get the list of user-defined composite types, sorted so that types
150
+ # that depend on other composite types come later
151
+ def composite_types
152
+ dependencies = composite_type_dependencies
153
+ result = []
154
+
155
+ dependencies.each_key do |name|
156
+ sort_composite_type(name, dependencies, result)
157
+ end
158
+
159
+ result
118
160
  end
119
161
 
120
162
  # Gets a list of user defined types.
@@ -211,6 +253,15 @@ module Torque
211
253
  WHERE 1=1 AND c.relkind IN ('v', 'm')
212
254
  AND #{filter_by_schema.join(' AND ')};
213
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
214
265
  end
215
266
 
216
267
  select_rows(query, 'SCHEMA')
@@ -229,6 +280,46 @@ module Torque
229
280
  conditions
230
281
  end
231
282
 
283
+ private
284
+
285
+ # Every user-defined composite type associated with the list of other
286
+ # composite types that its columns rely on
287
+ def composite_type_dependencies
288
+ rows = select_rows(<<~SQL, 'SCHEMA')
289
+ SELECT t.typname, t.oid::int, COALESCE(elem.oid, mt.oid)::int AS column_oid
290
+ FROM pg_type t
291
+ JOIN pg_class c ON c.oid = t.typrelid AND c.relkind = 'c'
292
+ JOIN pg_namespace n ON n.oid = t.typnamespace
293
+ LEFT JOIN pg_attribute a ON a.attrelid = c.oid AND a.attnum > 0
294
+ AND NOT a.attisdropped
295
+ LEFT JOIN pg_type mt ON mt.oid = a.atttypid
296
+ LEFT JOIN pg_type elem ON elem.oid = mt.typelem AND mt.typelem <> 0
297
+ WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')
298
+ ORDER BY t.oid
299
+ SQL
300
+
301
+ names = rows.to_h { |name, oid, _| [oid, name] }
302
+ result = names.values.uniq.to_h { |name| [name, []] }
303
+
304
+ rows.each do |name, _oid, column_oid|
305
+ dependency = names[column_oid]
306
+ result[name] << dependency unless dependency.nil? || dependency == name
307
+ end
308
+
309
+ result
310
+ end
311
+
312
+ # Add the type to the list, but only after every type it depends on
313
+ def sort_composite_type(name, dependencies, result)
314
+ return if result.include?(name)
315
+
316
+ dependencies[name].each do |dependency|
317
+ sort_composite_type(dependency, dependencies, result)
318
+ end
319
+
320
+ result << name
321
+ end
322
+
232
323
  end
233
324
  end
234
325
  end
@@ -0,0 +1,297 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Torque
4
+ module PostgreSQL
5
+ module Adapter
6
+ module InheritanceStatements
7
+
8
+ # Copies created by a sync are named after this, which is how they are
9
+ # told apart from anything written by hand
10
+ SYNC_MARKER = 'sync_inh_'
11
+
12
+ SYNC_FEATURES = %i[
13
+ primary_key indexes unique_constraints exclusion_constraints foreign_keys
14
+ ].freeze
15
+
16
+ # PostgreSQL only propagates columns, their defaults and their check
17
+ # constraints to inherited tables. This copies over everything else that
18
+ # Rails has a DSL for, from +table_name+ down to its children
19
+ #
20
+ # Features default to +true+, so passing one as +false+ excludes it.
21
+ # Passing any of them as +true+ instead selects only those
22
+ #
23
+ # Example:
24
+ # sync_inheritance_features :activities
25
+ # sync_inheritance_features :activities, %i[activity_books]
26
+ # sync_inheritance_features :activities, primary_key: false
27
+ # sync_inheritance_features :activities, prune: true
28
+ def sync_inheritance_features(table_name, children = nil, prune: false, **features)
29
+ features = sync_inheritance_selection(features)
30
+
31
+ sync_inheritance_tree(table_name, children).each do |child, parents|
32
+ sync_inheritance_into(child, parents, features, prune)
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ # Resolves which tables to write to, and where each of them pulls from,
39
+ # with ancestors always coming before their own descendants
40
+ def sync_inheritance_tree(table_name, children)
41
+ table_name = table_name.to_s
42
+ dependencies = inherited_tables
43
+ descendants = sync_inheritance_descendants(dependencies, table_name)
44
+
45
+ targets = descendants
46
+ unless children.nil?
47
+ children = Array.wrap(children).map(&:to_s)
48
+ unknown = children - descendants
49
+ raise ArgumentError, <<~MSG.squish if unknown.any?
50
+ #{unknown.to_sentence} #{unknown.one? ? 'does' : 'do'} not inherit
51
+ from #{table_name}
52
+ MSG
53
+
54
+ targets = descendants & children
55
+ end
56
+
57
+ scope = [table_name] + descendants
58
+ targets.map { |child| [child, dependencies[child] & scope] }
59
+ end
60
+
61
+ # Walks the inheritance graph breadth-first, so that every table shows
62
+ # up only after all of its own ancestors
63
+ def sync_inheritance_descendants(dependencies, table_name)
64
+ children = {}
65
+ dependencies.each do |child, parents|
66
+ parents.each { |parent| (children[parent] ||= []) << child }
67
+ end
68
+
69
+ result = []
70
+ queue = (children[table_name] || []).dup
71
+
72
+ while (item = queue.shift)
73
+ next if result.include?(item)
74
+
75
+ result << item
76
+ queue.concat(children[item] || [])
77
+ end
78
+
79
+ result
80
+ end
81
+
82
+ # Turns what was asked for into a decision for every single feature
83
+ def sync_inheritance_selection(features)
84
+ features = {} if features == true
85
+ unknown = features.keys - SYNC_FEATURES
86
+ raise ArgumentError, <<~MSG.squish if unknown.any?
87
+ Unknown inheritance #{'feature'.pluralize(unknown.size)} #{unknown.to_sentence}
88
+ MSG
89
+
90
+ return SYNC_FEATURES.index_with(true) if features.blank?
91
+ return SYNC_FEATURES.index_with { |name| features.fetch(name, false) } \
92
+ if features.value?(true)
93
+
94
+ SYNC_FEATURES.index_with { |name| features.fetch(name, true) }
95
+ end
96
+
97
+ def sync_inheritance_into(child, parents, features, prune)
98
+ return if parents.blank?
99
+
100
+ sync_inheritance_primary_key(child, parents) if features[:primary_key]
101
+ sync_inheritance_unique_constraints(child, parents, prune) \
102
+ if features[:unique_constraints]
103
+ sync_inheritance_exclusion_constraints(child, parents, prune) \
104
+ if features[:exclusion_constraints]
105
+ sync_inheritance_indexes(child, parents, features, prune) if features[:indexes]
106
+ sync_inheritance_foreign_keys(child, parents, prune) if features[:foreign_keys]
107
+ end
108
+
109
+ # The only copy that cannot carry the marker, since PostgreSQL names it
110
+ # after the table and Rails has no way to dump that name back. That is
111
+ # why it is also the only one that is never pruned
112
+ def sync_inheritance_primary_key(child, parents)
113
+ return if primary_keys(child).present?
114
+
115
+ parent = parents.find { |item| primary_keys(item).present? }
116
+ return if parent.nil?
117
+
118
+ columns = primary_keys(parent).map { |column| quote_column_name(column) }
119
+ execute(<<~SQL.squish)
120
+ ALTER TABLE #{quote_table_name(child)} ADD PRIMARY KEY (#{columns.join(', ')})
121
+ SQL
122
+ end
123
+
124
+ def sync_inheritance_indexes(child, parents, features, prune)
125
+ existing = indexes(child)
126
+ expected = {}
127
+
128
+ parents.each do |parent|
129
+ skip = sync_inheritance_constraint_indexes(parent, features)
130
+ indexes(parent).each do |index|
131
+ next if skip.include?(index.name)
132
+
133
+ expected[sync_inheritance_name(child, parent, index.name)] ||= index
134
+ end
135
+ end
136
+
137
+ expected.each do |name, index|
138
+ next if existing.any? { |item| item.name == name || sync_inheritance_index?(item, index) }
139
+
140
+ add_index(child, index.columns, name: name, **sync_inheritance_index_options(index))
141
+ end
142
+
143
+ return unless prune
144
+
145
+ # An index that backs a constraint can only leave with it
146
+ guarded = sync_inheritance_constraint_indexes(child)
147
+ sync_inheritance_stale(existing, expected).each do |name|
148
+ remove_index(child, name: name) unless guarded.include?(name)
149
+ end
150
+ end
151
+
152
+ def sync_inheritance_unique_constraints(child, parents, prune)
153
+ existing = unique_constraints(child)
154
+ expected = {}
155
+
156
+ parents.each do |parent|
157
+ unique_constraints(parent).each do |item|
158
+ expected[sync_inheritance_name(child, parent, item.name)] ||= item
159
+ end
160
+ end
161
+
162
+ expected.each do |name, item|
163
+ next if existing.any? { |other| other.name == name || other.column == item.column }
164
+
165
+ add_unique_constraint(child, item.column, name: name,
166
+ nulls_not_distinct: item.nulls_not_distinct, deferrable: item.deferrable)
167
+ end
168
+
169
+ return unless prune
170
+ sync_inheritance_stale(existing, expected).each do |name|
171
+ remove_constraint(child, name)
172
+ end
173
+ end
174
+
175
+ def sync_inheritance_exclusion_constraints(child, parents, prune)
176
+ existing = exclusion_constraints(child)
177
+ expected = {}
178
+
179
+ parents.each do |parent|
180
+ exclusion_constraints(parent).each do |item|
181
+ expected[sync_inheritance_name(child, parent, item.name)] ||= item
182
+ end
183
+ end
184
+
185
+ expected.each do |name, item|
186
+ next if existing.any? { |other| other.name == name || sync_inheritance_exclusion?(other, item) }
187
+
188
+ add_exclusion_constraint(child, item.expression, name: name,
189
+ using: item.using, where: item.where, deferrable: item.deferrable)
190
+ end
191
+
192
+ return unless prune
193
+ sync_inheritance_stale(existing, expected).each do |name|
194
+ remove_constraint(child, name)
195
+ end
196
+ end
197
+
198
+ def sync_inheritance_foreign_keys(child, parents, prune)
199
+ existing = foreign_keys(child)
200
+ expected = {}
201
+
202
+ parents.each do |parent|
203
+ foreign_keys(parent).each do |item|
204
+ expected[sync_inheritance_name(child, parent, item.name)] ||= item
205
+ end
206
+ end
207
+
208
+ expected.each do |name, item|
209
+ next if existing.any? { |other| other.name == name || sync_inheritance_foreign_key?(other, item) }
210
+
211
+ options = item.options.slice(:column, :primary_key, :on_delete, :on_update, :deferrable)
212
+ add_foreign_key(child, item.to_table, name: name, **options)
213
+ end
214
+
215
+ return unless prune
216
+ sync_inheritance_stale(existing, expected).each do |name|
217
+ remove_constraint(child, name)
218
+ end
219
+ end
220
+
221
+ # Every copy is named after the marker plus a digest of where it came
222
+ # from and where it went, which keeps it identical across databases and
223
+ # always well below the identifier limit
224
+ def sync_inheritance_name(child, parent, source)
225
+ digest = OpenSSL::Digest::SHA256.hexdigest("#{child}/#{parent}/#{source}")
226
+ "#{SYNC_MARKER}#{digest.first(10)}"
227
+ end
228
+
229
+ def sync_inheritance_copy?(name)
230
+ name.to_s.start_with?(SYNC_MARKER)
231
+ end
232
+
233
+ # Copies whose source is gone from the parent, which is all that a
234
+ # prune is ever allowed to drop
235
+ def sync_inheritance_stale(existing, expected)
236
+ existing.filter_map do |item|
237
+ item.name if sync_inheritance_copy?(item.name) && !expected.key?(item.name)
238
+ end
239
+ end
240
+
241
+ # PostgreSQL names the index that backs a constraint after the
242
+ # constraint itself, and +indexes+ hands those back along with the
243
+ # regular ones. Adding the constraint recreates them, so they are only
244
+ # skipped when that constraint is being copied as well. Without any
245
+ # feature given, every one of them is listed
246
+ def sync_inheritance_constraint_indexes(table_name, features = nil)
247
+ names = []
248
+ names.concat(unique_constraints(table_name).map(&:name)) \
249
+ if features.nil? || features[:unique_constraints]
250
+ names.concat(exclusion_constraints(table_name).map(&:name)) \
251
+ if features.nil? || features[:exclusion_constraints]
252
+ names
253
+ end
254
+
255
+ def sync_inheritance_index_options(index)
256
+ {
257
+ unique: index.unique,
258
+ where: index.where,
259
+ using: index.using,
260
+ include: index.include,
261
+ nulls_not_distinct: index.nulls_not_distinct,
262
+ order: index.orders,
263
+ opclass: index.opclasses,
264
+ }.compact_blank
265
+ end
266
+
267
+ def sync_inheritance_index?(current, source)
268
+ current.columns == source.columns && current.unique == source.unique &&
269
+ current.where == source.where && current.using == source.using
270
+ end
271
+
272
+ def sync_inheritance_exclusion?(current, source)
273
+ current.expression == source.expression && current.using == source.using &&
274
+ current.where == source.where
275
+ end
276
+
277
+ def sync_inheritance_foreign_key?(current, source)
278
+ current.to_table == source.to_table &&
279
+ current.options[:column] == source.options[:column] &&
280
+ current.options[:primary_key] == source.options[:primary_key]
281
+ end
282
+
283
+ # Tells whether the primary key of the given table is the one it got
284
+ # from a parent, which is the only case the dumper has to describe
285
+ # through the +sync+ option
286
+ def sync_inheritance_parent_primary_key?(table_name)
287
+ keys = primary_keys(table_name)
288
+ return false if keys.empty?
289
+
290
+ parents = inherited_table_names(table_name)
291
+ parents.any? { |parent| primary_keys(parent) == keys }
292
+ end
293
+
294
+ end
295
+ end
296
+ end
297
+ end
@@ -8,6 +8,19 @@ module Torque
8
8
  def force_equality?(value)
9
9
  PostgreSQL.config.predicate_builder.handle_array_attributes ? false : super
10
10
  end
11
+
12
+ private
13
+
14
+ def type_cast_array(value, method)
15
+ return super unless path_subtype?
16
+ return subtype.public_send(method, value) unless value.is_a?(::Array)
17
+
18
+ value.map { |item| subtype.public_send(method, item) }
19
+ end
20
+
21
+ def path_subtype?
22
+ PostgreSQL.config.ltree.enabled && subtype.is_a?(Ltree)
23
+ end
11
24
  end
12
25
 
13
26
  ::ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array.prepend(Array)
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Torque
4
4
  module PostgreSQL
5
- class Box < Struct.new(:x1, :y1, :x2, :y2)
5
+ class Box < ::Struct.new(:x1, :y1, :x2, :y2)
6
6
  def points
7
7
  klass = Torque::PostgreSQL.config.geometry.point_class
8
8
  [
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Torque
4
4
  module PostgreSQL
5
- class Circle < Struct.new(:x, :y, :r)
5
+ class Circle < ::Struct.new(:x, :y, :r)
6
6
  alias radius r
7
7
  alias radius= r=
8
8