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
@@ -18,6 +18,26 @@ module Torque
18
18
  names.each { |name| column(name, :virtual, **options) }
19
19
  end
20
20
 
21
+ # Add a composite type column to the table
22
+ def composite(*names, composite_type:, **options)
23
+ raise ArgumentError, "Missing column name(s) for composite" if names.empty?
24
+ names.each { |name| column(name, :composite, composite_type: composite_type, **options) }
25
+ end
26
+
27
+ # Add a label tree column to the table. Tree operators are only indexed
28
+ # by GiST, as in +index: { using: :gist }+, whose default operator class
29
+ # is already the right one for both a path and an array of paths
30
+ def ltree(*names, **options)
31
+ raise ArgumentError, "Missing column name(s) for ltree" if names.empty?
32
+ names.each { |name| column(name, :ltree, **options) }
33
+ end
34
+
35
+ # Add a label tree pattern column to the table
36
+ def lquery(*names, **options)
37
+ raise ArgumentError, "Missing column name(s) for lquery" if names.empty?
38
+ names.each { |name| column(name, :lquery, **options) }
39
+ end
40
+
21
41
  end
22
42
 
23
43
  module TableDefinition
@@ -49,6 +69,32 @@ module Torque
49
69
  end
50
70
  end
51
71
 
72
+ # Composite types are defined with the same DSL as tables, where declaring
73
+ # a column adds one, plus the operations that +change_table+ provides
74
+ class CompositeTypeDefinition < ActiveRecord::ConnectionAdapters::PostgreSQL::TableDefinition
75
+ attr_reader :changes, :removals, :renames
76
+
77
+ def initialize(*args, **xargs)
78
+ super
79
+
80
+ @changes = []
81
+ @removals = []
82
+ @renames = []
83
+ end
84
+
85
+ def change(name, type, **options)
86
+ @changes << new_column_definition(name, type, **options)
87
+ end
88
+
89
+ def remove(*names)
90
+ @removals.concat(names.map(&:to_s))
91
+ end
92
+
93
+ def rename(name, new_name)
94
+ @renames << [name.to_s, new_name.to_s]
95
+ end
96
+ end
97
+
52
98
  # Add exclusive support for versioned commands when importing from schema
53
99
  # dump. This ensures that such methods are not available in regular
54
100
  # migrations.
@@ -69,6 +115,11 @@ module Torque
69
115
  execute VersionedCommands.fetch_command(dir, :view, name, version)
70
116
  end
71
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
+
72
123
  end
73
124
 
74
125
  ActiveRecord::ConnectionAdapters::PostgreSQL::Table.include ColumnMethods
@@ -34,10 +34,35 @@ module Torque
34
34
  def types(stream) # :nodoc:
35
35
  super
36
36
 
37
+ composite_types(stream)
38
+
37
39
  versioned_commands(stream, :type)
38
40
  versioned_commands(stream, :function)
39
41
  end
40
42
 
43
+ # Dump user defined composite types, sorted by their dependencies
44
+ def composite_types(stream)
45
+ return unless PostgreSQL.config.composite.enabled
46
+
47
+ list = @connection.composite_types
48
+ list -= @versioned_commands.versions_of('type').map(&:first) \
49
+ if with_versioned_commands?
50
+ return if list.empty?
51
+
52
+ stream.puts " # Custom composite types defined in this database."
53
+ list.each do |name|
54
+ stream.puts " create_composite_type #{name.inspect}, force: :cascade do |t|"
55
+ @connection.columns(name).each do |column|
56
+ type, colspec = column_spec(column)
57
+ stream.print " t.#{type} #{column.name.inspect}"
58
+ stream.print ", #{format_colspec(colspec)}" if colspec.present?
59
+ stream.puts
60
+ end
61
+ stream.puts " end"
62
+ stream.puts
63
+ end
64
+ end
65
+
41
66
  def tables(stream) # :nodoc:
42
67
  around_tables(stream) { dump_tables(stream) }
43
68
  end
@@ -47,6 +72,7 @@ module Torque
47
72
 
48
73
  yield
49
74
  versioned_commands(stream, :view, true)
75
+ versioned_commands(stream, :trigger, true)
50
76
 
51
77
  functions(stream) if fx_functions_position == :end
52
78
  triggers(stream) if defined?(::Fx::SchemaDumper::Trigger)
@@ -126,6 +152,7 @@ module Torque
126
152
  def prepare_column_options(column)
127
153
  options = super
128
154
  parse_search_vector_options(column, options) if column.type == :tsvector
155
+ options[:composite_type] = column.sql_type.inspect if column.type == :composite
129
156
  options
130
157
  end
131
158
 
@@ -25,6 +25,100 @@ module Torque
25
25
  SQL
26
26
  end
27
27
 
28
+ # Creates a new composite type, with the columns being defined using
29
+ # the same DSL as +create_table+, although limited to the type itself,
30
+ # the size-related options, and no indexes or constraints
31
+ #
32
+ # Example:
33
+ # create_composite_type :address do |t|
34
+ # t.string "street"
35
+ # t.integer "number"
36
+ # end
37
+ def create_composite_type(name, options = {})
38
+ td = create_composite_type_definition(name)
39
+ yield td if block_given?
40
+
41
+ validate_composite_type!(name, td)
42
+ drop_type(name, force: options[:force], check: true, schema: options[:schema]) \
43
+ if options[:force]
44
+
45
+ columns = td.columns.map { |column| composite_column_definition(name, column) }
46
+ type_name = sanitize_name_with_schema(name, options.dup)
47
+
48
+ internal_exec_query(<<~SQL.squish, 'SCHEMA').tap { reload_type_map }
49
+ CREATE TYPE #{quote_type_name(type_name)} AS (#{columns.join(', ')})
50
+ SQL
51
+ end
52
+
53
+ # Changes an existing composite type, using the same DSL as
54
+ # +change_table+, although limited to what a type supports
55
+ #
56
+ # Example:
57
+ # change_composite_type :address do |t|
58
+ # t.string 'zipcode'
59
+ # t.change 'number', :bigint
60
+ # t.remove 'city'
61
+ # t.rename 'street', 'road'
62
+ # end
63
+ def change_composite_type(name, options = {})
64
+ td = create_composite_type_definition(name)
65
+ yield td
66
+
67
+ validate_composite_type!(name, td)
68
+ type_name = quote_type_name(sanitize_name_with_schema(name, options.dup))
69
+ actions = composite_type_actions(name, td)
70
+
71
+ execute("ALTER TYPE #{type_name} #{actions.join(', ')}") if actions.any?
72
+
73
+ td.renames.each do |from, to|
74
+ execute <<~SQL.squish
75
+ ALTER TYPE #{type_name}
76
+ RENAME ATTRIBUTE #{quote_column_name(from)} TO #{quote_column_name(to)}
77
+ SQL
78
+ end
79
+
80
+ reset_composite_type(name)
81
+ end
82
+
83
+ # Adds a single column to an existing composite type
84
+ def add_composite_column(type_name, column_name, type, options = {})
85
+ options = options.dup
86
+ schema = options.delete(:schema)
87
+
88
+ td = create_composite_type_definition(type_name)
89
+ td.column(column_name, type, **options)
90
+
91
+ alter_composite_type(type_name, schema, composite_type_actions(type_name, td))
92
+ end
93
+
94
+ # Removes a single column of an existing composite type. The +type+ is
95
+ # only needed to make the migration reversible
96
+ def remove_composite_column(type_name, column_name, type = nil, options = {})
97
+ action = "DROP ATTRIBUTE #{quote_column_name(column_name)}"
98
+ alter_composite_type(type_name, options[:schema], [action])
99
+ end
100
+
101
+ # Changes the type of a single column of an existing composite type
102
+ def change_composite_column(type_name, column_name, type, options = {})
103
+ options = options.dup
104
+ schema = options.delete(:schema)
105
+
106
+ td = create_composite_type_definition(type_name)
107
+ td.change(column_name, type, **options)
108
+
109
+ alter_composite_type(type_name, schema, composite_type_actions(type_name, td))
110
+ end
111
+
112
+ # Renames a single column of an existing composite type
113
+ def rename_composite_column(type_name, column_name, new_name, options = {})
114
+ action = <<~SQL.squish
115
+ RENAME ATTRIBUTE #{quote_column_name(column_name)}
116
+ TO #{quote_column_name(new_name)}
117
+ SQL
118
+
119
+ alter_composite_type(type_name, options[:schema], [action])
120
+ end
121
+
28
122
  # Creates a column that stores the underlying language of the record so
29
123
  # that a search vector can be created dynamically based on it. It uses
30
124
  # a `regconfig` type, so string conversions are mandatory
@@ -92,6 +186,28 @@ module Torque
92
186
  end
93
187
 
94
188
 
189
+ # Spread the features of the parent tables into the one being created,
190
+ # since PostgreSQL only propagates columns and their check constraints
191
+ def create_table(table_name, **options, &block)
192
+ sync = options.delete(:sync)
193
+ result = super
194
+ return result if sync.blank?
195
+
196
+ parents = Array.wrap(options[:inherits]).flatten.compact.map(&:to_s)
197
+ return result if parents.empty?
198
+
199
+ sync_inheritance_into(table_name.to_s, parents, sync_inheritance_selection(sync), false)
200
+ result
201
+ end
202
+
203
+ # A primary key that came from a parent table is described by the +sync+
204
+ # option while dumping, otherwise the dumper would describe the inherited
205
+ # column all over again and the load would break on the duplicate
206
+ def primary_key(table_name)
207
+ return super unless @_dump_mode
208
+ sync_inheritance_parent_primary_key?(table_name) ? nil : super
209
+ end
210
+
95
211
  # Add the schema option when extracting table options
96
212
  def table_options(table_name)
97
213
  options = super
@@ -108,6 +224,7 @@ module Torque
108
224
 
109
225
  tables = inherited_table_names(table_name)
110
226
  options[:inherits] = tables.one? ? tables.first : tables
227
+ options[:sync] = { primary_key: true } if sync_inheritance_parent_primary_key?(table_name)
111
228
  end
112
229
 
113
230
  options
@@ -137,6 +254,25 @@ module Torque
137
254
  super + [:schema, :inherits]
138
255
  end
139
256
 
257
+ # Add composite_type as one of the valid options for column definition
258
+ def valid_column_definition_options
259
+ super + [:composite_type]
260
+ end
261
+
262
+ # Maps the composite type through its required option, mirroring how
263
+ # enums are handled
264
+ def type_to_sql(type, composite_type: nil, **options)
265
+ return super(type, **options) unless type.to_s == 'composite'
266
+
267
+ raise ArgumentError, <<~MSG.squish if composite_type.nil?
268
+ composite_type is required for composite columns
269
+ MSG
270
+
271
+ sql = composite_type.to_s
272
+ sql = "#{sql}[]" if options[:array]
273
+ sql
274
+ end
275
+
140
276
  # Add proper support for schema load when using versioned commands
141
277
  def assume_migrated_upto_version(version)
142
278
  return super unless PostgreSQL.config.versioned_commands.enabled
@@ -195,6 +331,77 @@ module Torque
195
331
  Quoting::Name.new(schema.to_s, name.to_s)
196
332
  end
197
333
 
334
+ # A composite type is defined with the same DSL as a table, but only
335
+ # its columns are taken into account
336
+ def create_composite_type_definition(name)
337
+ CompositeTypeDefinition.new(self, name)
338
+ end
339
+
340
+ # Runs a single set of changes over an existing composite type
341
+ def alter_composite_type(name, schema, actions)
342
+ type_name = quote_type_name(sanitize_name_with_schema(name, schema: schema))
343
+ execute("ALTER TYPE #{type_name} #{actions.join(', ')}")
344
+ reset_composite_type(name)
345
+ end
346
+
347
+ # Every addition, change, and removal that a composite type can batch
348
+ # into a single statement. Renames are left out because PostgreSQL
349
+ # does not allow them to be combined with other actions
350
+ def composite_type_actions(name, td)
351
+ adds = td.columns.map do |column|
352
+ "ADD ATTRIBUTE #{composite_column_definition(name, column)}"
353
+ end
354
+
355
+ changes = td.changes.map do |column|
356
+ quoted = quote_column_name(column.name)
357
+ "ALTER ATTRIBUTE #{quoted} TYPE #{composite_column_type(name, column)}"
358
+ end
359
+
360
+ drops = td.removals.map { |name| "DROP ATTRIBUTE #{quote_column_name(name)}" }
361
+
362
+ adds + changes + drops
363
+ end
364
+
365
+ # A composite type only holds columns, so anything else that the DSL
366
+ # can produce is rejected
367
+ def validate_composite_type!(name, td)
368
+ raise ArgumentError, <<~MSG.squish if td.indexes.present?
369
+ Unable to define the composite type "#{name}" because it does
370
+ not support indexes.
371
+ MSG
372
+ end
373
+
374
+ # Build a single column of a composite type
375
+ def composite_column_definition(name, column)
376
+ "#{quote_column_name(column.name)} #{composite_column_type(name, column)}"
377
+ end
378
+
379
+ # Resolve the type of a column, making sure that only supported
380
+ # options were provided
381
+ def composite_column_type(name, column)
382
+ supported = %i[limit precision scale array enum_type composite_type]
383
+ options = column.options.dup
384
+ options.delete(:primary_key) unless options[:primary_key]
385
+
386
+ unsupported = options.keys - supported
387
+ raise ArgumentError, <<~MSG.squish if unsupported.present?
388
+ Unable to define the composite type "#{name}" because the
389
+ column "#{column.name}" contains unsupported options:
390
+ #{unsupported.map(&:inspect).join(', ')}.
391
+ MSG
392
+
393
+ type_to_sql(column.type, **options.slice(*supported))
394
+ end
395
+
396
+ # Members loaded by the class that handles the type are no longer
397
+ # valid once the type itself has changed
398
+ def reset_composite_type(name)
399
+ reload_type_map
400
+ return unless PostgreSQL.config.composite.enabled
401
+
402
+ Attributes::Composite.lookup(name.to_s.split('.').last).reset_columns!
403
+ end
404
+
198
405
  def quote_enum_values(name, values, options)
199
406
  prefix = options[:prefix]
200
407
  prefix = name if prefix === true
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'adapter/database_statements'
4
+ require_relative 'adapter/inheritance_statements'
4
5
  require_relative 'adapter/oid'
5
6
  require_relative 'adapter/quoting'
6
7
  require_relative 'adapter/schema_creation'
@@ -13,6 +14,7 @@ module Torque
13
14
  module Adapter
14
15
  include Quoting
15
16
  include DatabaseStatements
17
+ include InheritanceStatements
16
18
  include SchemaStatements
17
19
 
18
20
  # :nodoc:
@@ -31,11 +33,6 @@ module Torque
31
33
  )
32
34
  end
33
35
 
34
- # Add `inherits` and `schema` to the list of extracted table options
35
- def extract_table_options!(options)
36
- super.merge(options.extract!(:inherits, :schema))
37
- end
38
-
39
36
  # Allow filtered bulk insert by adding the where clause. This method is
40
37
  # only used by +InsertAll+, so it somewhat safe to override it
41
38
  def build_insert_sql(insert)
@@ -13,12 +13,125 @@ module Torque
13
13
  include ::Arel::Math
14
14
 
15
15
  def initialize(left, right, array = false)
16
- right = +right.to_s
16
+ right = right.to_s.dup
17
17
  right << '[]' if array
18
18
  super left, right
19
19
  end
20
20
  end
21
21
 
22
+ # Accessing a single column of a composite value, which behaves like an
23
+ # attribute of the composite type it belongs to
24
+ class Column < ::Arel::Nodes::Binary
25
+ include ::Arel::Expressions
26
+ include ::Arel::Predications
27
+ include ::Arel::AliasPredication
28
+ include ::Arel::OrderPredications
29
+ include ::Arel::Math
30
+
31
+ attr_reader :type_caster
32
+ alias name right
33
+
34
+ def initialize(source, name, type_caster)
35
+ @type_caster = type_caster
36
+ super(source, name.to_s)
37
+ end
38
+
39
+ def able_to_type_cast?
40
+ true
41
+ end
42
+
43
+ def type_cast_for_database(value)
44
+ type_caster.serialize(value)
45
+ end
46
+ end
47
+
48
+ # Accessing a single property of a document, at any depth, casted to
49
+ # the type that the class declares for it
50
+ class Property < ::Arel::Nodes::Binary
51
+ include ::Arel::Expressions
52
+ include ::Arel::Predications
53
+ include ::Arel::AliasPredication
54
+ include ::Arel::OrderPredications
55
+ include ::Arel::Math
56
+
57
+ attr_reader :type_caster, :cast
58
+
59
+ alias path right
60
+
61
+ # Reaching a property of another one simply adds up to the path, so
62
+ # that a single node always holds the whole way to it
63
+ def initialize(source, key, type_caster, cast = nil)
64
+ @type_caster = type_caster
65
+ @cast = cast
66
+
67
+ return super(source.left, source.path + [key.to_s]) if source.is_a?(Property)
68
+ super(source, [key.to_s])
69
+ end
70
+
71
+ def name
72
+ right.last
73
+ end
74
+
75
+ def able_to_type_cast?
76
+ true
77
+ end
78
+
79
+ def type_cast_for_database(value)
80
+ type_caster.serialize(value)
81
+ end
82
+ end
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
+
22
135
  class Ref < ::Arel::Nodes::Unary
23
136
  attr_reader :reference
24
137
  alias to_s expr
@@ -16,19 +16,57 @@ module Torque
16
16
  quote_array(o.expr, collector)
17
17
  end
18
18
 
19
- # Allow quoted arrays to get here
20
- def visit_Arel_Nodes_Casted(o, collector)
21
- value = o.value_for_database
22
- klass = ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array::Data
23
- return super unless value.is_a?(klass)
24
- quote_array(value.values, collector)
25
- end
26
-
27
19
  ## TORQUE VISITORS
28
20
  def visit_Torque_PostgreSQL_Arel_Nodes_Ref(o, collector)
29
21
  collector << quote_table_name(o.expr)
30
22
  end
31
23
 
24
+ # Access a single column of a composite value
25
+ def visit_Torque_PostgreSQL_Arel_Nodes_Column(o, collector)
26
+ collector << '('
27
+ visit(o.left, collector)
28
+ collector << ').' << quote_column_name(o.right)
29
+ end
30
+
31
+ # Access a single property of a document, as text, so that it can then
32
+ # be casted to whatever the property is supposed to be
33
+ def visit_Torque_PostgreSQL_Arel_Nodes_Property(o, collector)
34
+ collector << '('
35
+ visit(o.left, collector)
36
+ collector << ' #>> '
37
+ visit(::Arel.array(o.path), collector)
38
+ collector << ')'
39
+ collector << '::' << o.cast if o.cast
40
+ collector
41
+ end
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
+
32
70
  # Allow casting any node
33
71
  def visit_Torque_PostgreSQL_Arel_Nodes_Cast(o, collector)
34
72
  visit(o.left, collector) << '::' << o.right
@@ -36,6 +74,25 @@ module Torque
36
74
 
37
75
  private
38
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
+
39
96
  def quote_array(value, collector)
40
97
  value = value.map(&::Arel::Nodes.method(:build_quoted))
41
98
 
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Torque
4
+ module PostgreSQL
5
+ module Associations
6
+ module JoinDependency
7
+ def apply_column_aliases(relation)
8
+ result = super
9
+ return result unless relation.from_clause.empty?
10
+
11
+ relation._select!(root_record_class_marker) if cast_join_root?(relation)
12
+ relation._select!(-> { joined_record_class_markers })
13
+ result
14
+ end
15
+
16
+ private
17
+
18
+ def cast_join_root?(relation)
19
+ return false if relation.itself_only_value === true
20
+ return false unless join_root_alias
21
+
22
+ join_root.base_klass.physically_inheritances?
23
+ end
24
+
25
+ def root_record_class_marker
26
+ table = join_root.base_klass.arel_table
27
+ table['tableoid'].pg_cast(:regclass).as(record_class_column_name)
28
+ end
29
+
30
+ # The root is projected under its own name and picked up by the
31
+ # column sweep in #instantiate, which only ever feeds the root. Every
32
+ # other join part has to carry the marker inside its own set of
33
+ # aliases, since that is all #extract_record ever reads
34
+ def joined_record_class_markers
35
+ markers = join_root.each_with_index.map do |join_part, index|
36
+ next if index.zero? || !join_part.base_klass.physically_inheritances?
37
+ next if (columns = aliases.column_aliases(join_part)).blank?
38
+
39
+ column_alias = "#{columns.first.alias[/\At\d+/]}_r#{columns.size}"
40
+ columns << ::ActiveRecord::Associations::JoinDependency::Aliases::Column
41
+ .new(record_class_column_name, column_alias)
42
+
43
+ join_part.table['tableoid'].pg_cast(:regclass).as(column_alias)
44
+ end
45
+
46
+ markers.compact
47
+ end
48
+
49
+ def record_class_column_name
50
+ ::ActiveRecord::Relation._record_class_column_name
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -4,7 +4,10 @@ require_relative 'associations/foreign_association'
4
4
 
5
5
  require_relative 'associations/builder'
6
6
  require_relative 'associations/preloader'
7
+ require_relative 'associations/join_dependency'
7
8
 
8
9
  association_mod = Torque::PostgreSQL::Associations::ForeignAssociation
9
10
  ::ActiveRecord::Associations::HasManyAssociation.prepend(association_mod)
10
11
  ::ActiveRecord::Associations::BelongsToManyAssociation.prepend(association_mod)
12
+
13
+ ::ActiveRecord::Associations::JoinDependency.prepend(Torque::PostgreSQL::Associations::JoinDependency)