torque-postgresql 3.4.1 → 4.0.0

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 (94) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/torque/function_generator.rb +13 -0
  3. data/lib/generators/torque/templates/function.sql.erb +4 -0
  4. data/lib/generators/torque/templates/type.sql.erb +2 -0
  5. data/lib/generators/torque/templates/view.sql.erb +3 -0
  6. data/lib/generators/torque/type_generator.rb +13 -0
  7. data/lib/generators/torque/view_generator.rb +16 -0
  8. data/lib/torque/postgresql/adapter/database_statements.rb +111 -94
  9. data/lib/torque/postgresql/adapter/oid/array.rb +17 -0
  10. data/lib/torque/postgresql/adapter/oid/line.rb +2 -6
  11. data/lib/torque/postgresql/adapter/oid/range.rb +4 -4
  12. data/lib/torque/postgresql/adapter/oid.rb +1 -23
  13. data/lib/torque/postgresql/adapter/quoting.rb +13 -7
  14. data/lib/torque/postgresql/adapter/schema_creation.rb +7 -28
  15. data/lib/torque/postgresql/adapter/schema_definitions.rb +58 -0
  16. data/lib/torque/postgresql/adapter/schema_dumper.rb +136 -34
  17. data/lib/torque/postgresql/adapter/schema_overrides.rb +45 -0
  18. data/lib/torque/postgresql/adapter/schema_statements.rb +109 -49
  19. data/lib/torque/postgresql/arel/infix_operation.rb +15 -28
  20. data/lib/torque/postgresql/arel/nodes.rb +16 -2
  21. data/lib/torque/postgresql/arel/operations.rb +7 -1
  22. data/lib/torque/postgresql/arel/visitors.rb +7 -9
  23. data/lib/torque/postgresql/associations/association_scope.rb +23 -31
  24. data/lib/torque/postgresql/associations/belongs_to_many_association.rb +25 -0
  25. data/lib/torque/postgresql/associations/builder/belongs_to_many.rb +16 -0
  26. data/lib/torque/postgresql/attributes/builder/enum.rb +12 -9
  27. data/lib/torque/postgresql/attributes/builder/full_text_search.rb +109 -0
  28. data/lib/torque/postgresql/attributes/builder/period.rb +21 -21
  29. data/lib/torque/postgresql/attributes/builder.rb +49 -11
  30. data/lib/torque/postgresql/attributes/enum.rb +7 -7
  31. data/lib/torque/postgresql/attributes/enum_set.rb +7 -7
  32. data/lib/torque/postgresql/attributes/full_text_search.rb +19 -0
  33. data/lib/torque/postgresql/attributes/period.rb +2 -2
  34. data/lib/torque/postgresql/attributes.rb +0 -4
  35. data/lib/torque/postgresql/auxiliary_statement/recursive.rb +3 -3
  36. data/lib/torque/postgresql/base.rb +5 -11
  37. data/lib/torque/postgresql/collector.rb +1 -1
  38. data/lib/torque/postgresql/config.rb +129 -5
  39. data/lib/torque/postgresql/function.rb +94 -0
  40. data/lib/torque/postgresql/inheritance.rb +52 -36
  41. data/lib/torque/postgresql/predicate_builder/arel_attribute_handler.rb +33 -0
  42. data/lib/torque/postgresql/predicate_builder/array_handler.rb +47 -0
  43. data/lib/torque/postgresql/predicate_builder/enumerator_lazy_handler.rb +37 -0
  44. data/lib/torque/postgresql/predicate_builder/regexp_handler.rb +21 -0
  45. data/lib/torque/postgresql/predicate_builder.rb +35 -0
  46. data/lib/torque/postgresql/railtie.rb +137 -30
  47. data/lib/torque/postgresql/reflection/abstract_reflection.rb +12 -44
  48. data/lib/torque/postgresql/reflection/belongs_to_many_reflection.rb +4 -0
  49. data/lib/torque/postgresql/reflection/has_many_reflection.rb +4 -0
  50. data/lib/torque/postgresql/reflection/runtime_reflection.rb +1 -1
  51. data/lib/torque/postgresql/relation/auxiliary_statement.rb +7 -2
  52. data/lib/torque/postgresql/relation/buckets.rb +124 -0
  53. data/lib/torque/postgresql/relation/distinct_on.rb +7 -2
  54. data/lib/torque/postgresql/relation/inheritance.rb +22 -15
  55. data/lib/torque/postgresql/relation/join_series.rb +112 -0
  56. data/lib/torque/postgresql/relation/merger.rb +17 -3
  57. data/lib/torque/postgresql/relation.rb +24 -38
  58. data/lib/torque/postgresql/schema_cache.rb +6 -12
  59. data/lib/torque/postgresql/version.rb +1 -1
  60. data/lib/torque/postgresql/versioned_commands/command_migration.rb +146 -0
  61. data/lib/torque/postgresql/versioned_commands/generator.rb +57 -0
  62. data/lib/torque/postgresql/versioned_commands/migration_context.rb +83 -0
  63. data/lib/torque/postgresql/versioned_commands/migrator.rb +39 -0
  64. data/lib/torque/postgresql/versioned_commands/schema_table.rb +101 -0
  65. data/lib/torque/postgresql/versioned_commands.rb +161 -0
  66. data/lib/torque/postgresql.rb +2 -1
  67. data/spec/fixtures/migrations/20250101000001_create_users.rb +0 -0
  68. data/spec/fixtures/migrations/20250101000002_create_function_count_users_v1.sql +0 -0
  69. data/spec/fixtures/migrations/20250101000003_create_internal_users.rb +0 -0
  70. data/spec/fixtures/migrations/20250101000004_update_function_count_users_v2.sql +0 -0
  71. data/spec/fixtures/migrations/20250101000005_create_view_all_users_v1.sql +0 -0
  72. data/spec/fixtures/migrations/20250101000006_create_type_user_id_v1.sql +0 -0
  73. data/spec/fixtures/migrations/20250101000007_remove_function_count_users_v2.sql +0 -0
  74. data/spec/initialize.rb +67 -0
  75. data/spec/mocks/cache_query.rb +21 -21
  76. data/spec/mocks/create_table.rb +6 -26
  77. data/spec/schema.rb +17 -12
  78. data/spec/spec_helper.rb +11 -2
  79. data/spec/tests/arel_spec.rb +32 -7
  80. data/spec/tests/auxiliary_statement_spec.rb +3 -3
  81. data/spec/tests/belongs_to_many_spec.rb +72 -5
  82. data/spec/tests/enum_set_spec.rb +12 -11
  83. data/spec/tests/enum_spec.rb +4 -2
  84. data/spec/tests/full_text_seach_test.rb +280 -0
  85. data/spec/tests/function_spec.rb +42 -0
  86. data/spec/tests/has_many_spec.rb +21 -8
  87. data/spec/tests/interval_spec.rb +1 -7
  88. data/spec/tests/period_spec.rb +61 -61
  89. data/spec/tests/predicate_builder_spec.rb +132 -0
  90. data/spec/tests/relation_spec.rb +229 -0
  91. data/spec/tests/schema_spec.rb +6 -9
  92. data/spec/tests/table_inheritance_spec.rb +25 -26
  93. data/spec/tests/versioned_commands_spec.rb +513 -0
  94. metadata +64 -39
@@ -4,6 +4,23 @@ module Torque
4
4
  module PostgreSQL
5
5
  module Adapter
6
6
  module SchemaDumper
7
+ SEARCH_VECTOR_SCANNER = /
8
+ to_tsvector\(
9
+ ('[^']+'|[a-z][a-z0-9_]*)[^,]*,[^\(]*
10
+ \(?coalesce\(([a-z][a-z0-9_]*)[^\)]*\)\)?
11
+ (?:::[^\)]*\))?
12
+ (?:\s*,\s*'([A-D])')?
13
+ /ix
14
+
15
+ def initialize(*)
16
+ super
17
+
18
+ if with_versioned_commands?
19
+ @versioned_commands = VersionedCommands::SchemaTable.new(@connection.pool)
20
+ @ignore_tables << @versioned_commands.table_name
21
+ end
22
+ end
23
+
7
24
  def dump(stream) # :nodoc:
8
25
  @connection.dump_mode!
9
26
  super
@@ -12,17 +29,14 @@ module Torque
12
29
  stream
13
30
  end
14
31
 
15
- def extensions(stream) # :nodoc:
16
- super
17
- user_defined_schemas(stream)
18
- end
32
+ private
19
33
 
20
- # Translate +:enum_set+ into +:enum+
21
- def schema_type(column)
22
- column.type == :enum_set ? :enum : super
23
- end
34
+ def types(stream) # :nodoc:
35
+ super
24
36
 
25
- private
37
+ versioned_commands(stream, :type)
38
+ versioned_commands(stream, :function)
39
+ end
26
40
 
27
41
  def tables(stream) # :nodoc:
28
42
  around_tables(stream) { dump_tables(stream) }
@@ -32,6 +46,7 @@ module Torque
32
46
  functions(stream) if fx_functions_position == :beginning
33
47
 
34
48
  yield
49
+ versioned_commands(stream, :view, true)
35
50
 
36
51
  functions(stream) if fx_functions_position == :end
37
52
  triggers(stream) if defined?(::Fx::SchemaDumper::Trigger)
@@ -39,41 +54,46 @@ module Torque
39
54
 
40
55
  def dump_tables(stream)
41
56
  inherited_tables = @connection.inherited_tables
42
- sorted_tables = (@connection.tables - @connection.views).sort_by do |table_name|
43
- table_name.split(/(?:public)?\./).reverse
44
- end
57
+ sorted_tables = (@connection.tables - @connection.views).filter_map do |table_name|
58
+ name_parts = table_name.split(/(?:public)?\./).reverse.compact_blank
59
+ next if ignored?(table_name) || ignored?(name_parts.join('.'))
60
+
61
+ [table_name, name_parts]
62
+ end.sort_by(&:last).to_h
63
+
64
+ postponed = []
45
65
 
46
66
  stream.puts " # These are the common tables"
47
- (sorted_tables - inherited_tables.keys).each do |table_name|
48
- table(table_name, stream) unless ignored?(table_name)
67
+ sorted_tables.each do |table, (table_name, _)|
68
+ next postponed << table if inherited_tables.key?(table_name)
69
+
70
+ table(table, stream)
71
+ stream.puts # Ideally we would not do this in the last one
49
72
  end
50
73
 
51
- if inherited_tables.present?
74
+ if postponed.present?
52
75
  stream.puts " # These are tables that have inheritance"
53
- inherited_tables.each do |table_name, inherits|
54
- next if ignored?(table_name)
55
-
76
+ postponed.each do |table|
56
77
  sub_stream = StringIO.new
57
- table(table_name, sub_stream)
58
-
59
- # Add the inherits setting
60
- sub_stream.rewind
61
- inherits.map! { |parent| parent.to_s.sub(/\Apublic\./, '') }
62
- inherits = inherits.first if inherits.size === 1
63
- inherits = ", inherits: #{inherits.inspect} do |t|"
64
- table_dump = sub_stream.read.gsub(/ do \|t\|$/, inherits)
65
-
66
- # Ensure bodyless definitions
67
- table_dump.gsub!(/do \|t\|\n end/, '')
68
- stream.print table_dump
78
+ table(table, sub_stream)
79
+ stream.puts sub_stream.string.sub(/do \|t\|\n end/, '')
80
+ stream.puts
69
81
  end
70
82
  end
71
83
 
72
- # Dump foreign keys at the end to make sure all dependent tables exist.
84
+ # Fixes double new lines to single new lines
85
+ stream.pos -= 1
86
+
87
+ # dump foreign keys at the end to make sure all dependent tables exist.
73
88
  if @connection.supports_foreign_keys?
74
- sorted_tables.each do |tbl|
75
- foreign_keys(tbl, stream) unless ignored?(tbl)
89
+ foreign_keys_stream = StringIO.new
90
+ sorted_tables.each do |(tbl, *)|
91
+ foreign_keys(tbl, foreign_keys_stream)
76
92
  end
93
+
94
+ foreign_keys_string = foreign_keys_stream.string
95
+ stream.puts if foreign_keys_string.length > 0
96
+ stream.print foreign_keys_string
77
97
  end
78
98
  end
79
99
 
@@ -83,7 +103,8 @@ module Torque
83
103
  end
84
104
 
85
105
  # Dump user defined schemas
86
- def user_defined_schemas(stream)
106
+ def schemas(stream)
107
+ return super if !PostgreSQL.config.schemas.enabled
87
108
  return if (list = (@connection.user_defined_schemas - ['public'])).empty?
88
109
 
89
110
  stream.puts " # Custom schemas defined in this database."
@@ -91,6 +112,87 @@ module Torque
91
112
  stream.puts
92
113
  end
93
114
 
115
+ # Adjust the schema type for search vector
116
+ def schema_type_with_virtual(column)
117
+ column.virtual? && column.type == :tsvector ? :search_vector : super
118
+ end
119
+
120
+ # Adjust the schema type for search language
121
+ def schema_type(column)
122
+ column.sql_type == 'regconfig' ? :search_language : super
123
+ end
124
+
125
+ # Adjust table options to make the dump more readable
126
+ def prepare_column_options(column)
127
+ options = super
128
+ parse_search_vector_options(column, options) if column.type == :tsvector
129
+ options
130
+ end
131
+
132
+ # Parse the search vector operation into a readable format
133
+ def parse_search_vector_options(column, options)
134
+ settings = options[:as].scan(SEARCH_VECTOR_SCANNER)
135
+ return if settings.empty?
136
+
137
+ languages = settings.map(&:shift).uniq
138
+ return if languages.many?
139
+
140
+ language = languages.first
141
+ language = language[0] == "'" ? language[1..-2] : language.to_sym
142
+ columns = parse_search_vector_columns(settings)
143
+
144
+ options.except!(:as, :type)
145
+ options.merge!(language: language.inspect, columns: columns)
146
+ end
147
+
148
+ # Simplify the whole columns configuration to make it more manageable
149
+ def parse_search_vector_columns(settings)
150
+ return ":#{settings.first.first}" if settings.one?
151
+
152
+ settings = settings.sort_by(&:last)
153
+ weights = %w[A B C D]
154
+
155
+ columns = settings.each.with_index.reduce([]) do |acc, (setting, index)|
156
+ column, weight = setting
157
+ break if (weights[index] || 'D') != weight
158
+
159
+ acc << column
160
+ acc
161
+ end
162
+
163
+ return columns.map(&:to_sym).inspect if columns
164
+ settings.to_h.transform_values(&:inspect)
165
+ end
166
+
167
+ # Simply add all versioned commands to the stream
168
+ def versioned_commands(stream, type, add_newline = false)
169
+ return unless with_versioned_commands?
170
+
171
+ list = @versioned_commands.versions_of(type.to_s)
172
+ return if list.empty?
173
+
174
+ existing = list_existing_versioned_commands(type)
175
+
176
+ stream.puts if add_newline
177
+ stream.puts " # These are #{type.to_s.pluralize} managed by versioned commands"
178
+ list.each do |(name, version)|
179
+ next if existing.exclude?(name)
180
+
181
+ stream.puts " create_#{type} \"#{name}\", version: #{version}"
182
+ end
183
+ stream.puts unless add_newline
184
+ end
185
+
186
+ def list_existing_versioned_commands(type)
187
+ @connection.list_versioned_commands(type).each_with_object(Set.new) do |entry, set|
188
+ set << (entry.first == 'public' ? entry.last : entry.join('_'))
189
+ end
190
+ end
191
+
192
+ def with_versioned_commands?
193
+ PostgreSQL.config.versioned_commands.enabled
194
+ end
195
+
94
196
  def fx_functions_position
95
197
  return unless defined?(::Fx::SchemaDumper::Function)
96
198
  Fx.configuration.dump_functions_at_beginning_of_schema ? :beginning : :end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Torque
4
+ module PostgreSQL
5
+ module Adapter
6
+ module SchemaOverrides
7
+ # This adds better support for handling the quotation of table names
8
+ def quote_table_name(name)
9
+ ActiveRecord::ConnectionAdapters::PostgreSQL::Quoting::QUOTED_TABLE_NAMES.then do |m|
10
+ m[name] ||= quote_identifier_name(name)
11
+ end
12
+ end
13
+
14
+ %i[
15
+ table_exists? indexes index_exists? columns column_exists? primary_key
16
+ create_table change_table add_column add_columns remove_columns remove_column
17
+ change_column change_column_default change_column_null rename_column
18
+ add_index remove_index rename_index index_name_exists? foreign_keys
19
+ add_timestamps remove_timestamps change_table_comment change_column_comment
20
+ bulk_change_table
21
+
22
+ rename_table add_foreign_key remove_foreign_key foreign_key_exists?
23
+ ].each do |method_name|
24
+ define_method(method_name) do |table_name, *args, **options, &block|
25
+ table_name = sanitize_name_with_schema(table_name, options)
26
+ super(table_name, *args, **options, &block)
27
+ end
28
+ end
29
+
30
+ def drop_table(*table_names, **options)
31
+ table_names = table_names.map { |name| sanitize_name_with_schema(name, options.dup) }
32
+ super(*table_names, **options)
33
+ end
34
+
35
+ private
36
+
37
+ def validate_table_length!(table_name)
38
+ super(table_name.to_s)
39
+ end
40
+ end
41
+
42
+ include SchemaOverrides
43
+ end
44
+ end
45
+ end
@@ -4,42 +4,55 @@ module Torque
4
4
  module PostgreSQL
5
5
  module Adapter
6
6
  module SchemaStatements
7
-
8
- TableDefinition = ActiveRecord::ConnectionAdapters::PostgreSQL::TableDefinition
9
-
10
- # Create a new schema
11
- def create_schema(name, options = {})
12
- drop_schema(name, options) if options[:force]
13
-
14
- check = 'IF NOT EXISTS' if options.fetch(:check, true)
15
- execute("CREATE SCHEMA #{check} #{quote_schema_name(name.to_s)}")
16
- end
17
-
18
- # Drop an existing schema
19
- def drop_schema(name, options = {})
20
- force = options.fetch(:force, '').upcase
21
- check = 'IF EXISTS' if options.fetch(:check, true)
22
- execute("DROP SCHEMA #{check} #{quote_schema_name(name.to_s)} #{force}")
23
- end
24
-
25
- # Drops a type.
7
+ # Drops a type
26
8
  def drop_type(name, options = {})
27
9
  force = options.fetch(:force, '').upcase
28
10
  check = 'IF EXISTS' if options.fetch(:check, true)
29
- execute <<-SQL.squish
11
+ name = sanitize_name_with_schema(name, options)
12
+
13
+ internal_exec_query(<<-SQL.squish).tap { reload_type_map }
30
14
  DROP TYPE #{check}
31
- #{quote_type_name(name, options[:schema])} #{force}
15
+ #{quote_type_name(name)} #{force}
32
16
  SQL
33
17
  end
34
18
 
35
- # Renames a type.
19
+ # Renames a type
36
20
  def rename_type(type_name, new_name, options = {})
37
- execute <<-SQL.squish
38
- ALTER TYPE #{quote_type_name(type_name, options[:schema])}
21
+ type_name = sanitize_name_with_schema(type_name, options)
22
+ internal_exec_query(<<-SQL.squish).tap { reload_type_map }
23
+ ALTER TYPE #{quote_type_name(type_name)}
39
24
  RENAME TO #{Quoting::Name.new(nil, new_name.to_s).quoted}
40
25
  SQL
41
26
  end
42
27
 
28
+ # Creates a column that stores the underlying language of the record so
29
+ # that a search vector can be created dynamically based on it. It uses
30
+ # a `regconfig` type, so string conversions are mandatory
31
+ def add_search_language(table, name, options = {})
32
+ add_column(table, name, :regconfig, options)
33
+ end
34
+
35
+ # Creates a column and setup a search vector as a virtual column. The
36
+ # options are dev-friendly and controls how the vector function will be
37
+ # defined
38
+ #
39
+ # === Options
40
+ # [:columns]
41
+ # The list of columns that will be used to create the search vector.
42
+ # It can be a single column, an array of columns, or a hash as a
43
+ # combination of column name and weight (A, B, C, or D).
44
+ # [:language]
45
+ # Specify the language config to be used for the search vector. If a
46
+ # string is provided, then the value will be statically embedded. If a
47
+ # symbol is provided, then it will reference another column.
48
+ # [:stored]
49
+ # Specify if the value should be stored in the database. As of now,
50
+ # PostgreSQL only supports `true`, which will create a stored column.
51
+ def add_search_vector(table, name, columns, options = {})
52
+ options = Builder.search_vector_options(columns: columns, **options)
53
+ add_column(table, name, options.delete(:type), options)
54
+ end
55
+
43
56
  # Changes the enumerator by adding new values
44
57
  #
45
58
  # Example:
@@ -48,6 +61,7 @@ module Torque
48
61
  # add_enum_values 'status', ['baz'], after: 'foo'
49
62
  # add_enum_values 'status', ['baz'], prepend: true
50
63
  def add_enum_values(name, values, options = {})
64
+ name = sanitize_name_with_schema(name, options)
51
65
  before = options.fetch(:before, false)
52
66
  after = options.fetch(:after, false)
53
67
 
@@ -59,7 +73,7 @@ module Torque
59
73
  reference = "BEFORE #{before}" unless before == false
60
74
  reference = "AFTER #{after}" unless after == false
61
75
  execute <<-SQL.squish
62
- ALTER TYPE #{quote_type_name(name, options[:schema])}
76
+ ALTER TYPE #{quote_type_name(name)}
63
77
  ADD VALUE #{value} #{reference}
64
78
  SQL
65
79
 
@@ -77,34 +91,26 @@ module Torque
77
91
  SQL
78
92
  end
79
93
 
80
- # Rewrite the method that creates tables to easily accept extra options
81
- def create_table(table_name, **options, &block)
82
- table_name = "#{options[:schema]}.#{table_name}" if options[:schema].present?
83
-
84
- options[:id] = false if options[:inherits].present? &&
85
- options[:primary_key].blank? && options[:id].blank?
86
94
 
87
- super table_name, **options, &block
88
- end
95
+ # Add the schema option when extracting table options
96
+ def table_options(table_name)
97
+ options = super
89
98
 
90
- # Simply add the schema to the table name when changing a table
91
- def change_table(table_name, **options)
92
- table_name = "#{options[:schema]}.#{table_name}" if options[:schema].present?
93
- super table_name, **options
94
- end
99
+ if PostgreSQL.config.schemas.enabled
100
+ table, schema = table_name.split('.').reverse
101
+ if table.present? && schema.present? && schema != current_schema
102
+ options[:schema] = schema
103
+ end
104
+ end
95
105
 
96
- # Simply add the schema to the table name when dropping a table
97
- def drop_table(table_name, **options)
98
- table_name = "#{options[:schema]}.#{table_name}" if options[:schema].present?
99
- super table_name, **options
100
- end
106
+ if options[:options]&.start_with?('INHERITS (')
107
+ options.delete(:options)
101
108
 
102
- # Add the schema option when extracting table options
103
- def table_options(table_name)
104
- parts = table_name.split('.').reverse
105
- return super unless parts.size == 2 && parts[1] != 'public'
109
+ tables = inherited_table_names(table_name)
110
+ options[:inherits] = tables.one? ? tables.first : tables
111
+ end
106
112
 
107
- (super || {}).merge(schema: parts[1])
113
+ options
108
114
  end
109
115
 
110
116
  # When dumping the schema we need to add all schemas, not only those
@@ -112,7 +118,10 @@ module Torque
112
118
  def quoted_scope(name = nil, type: nil)
113
119
  return super unless name.nil?
114
120
 
115
- super.merge(schema: "ANY ('{#{user_defined_schemas.join(',')}}')")
121
+ scope = super
122
+ global = scope[:schema].start_with?('ANY (')
123
+ scope[:schema] = "ANY ('{#{user_defined_schemas.join(',')}}')"
124
+ scope
116
125
  end
117
126
 
118
127
  # Fix the query to include the schema on tables names when dumping
@@ -128,6 +137,51 @@ module Torque
128
137
  super + [:schema, :inherits]
129
138
  end
130
139
 
140
+ # Add proper support for schema load when using versioned commands
141
+ def assume_migrated_upto_version(version)
142
+ return super unless PostgreSQL.config.versioned_commands.enabled
143
+ return super if (commands = pool.migration_context.migration_commands).empty?
144
+
145
+ version = version.to_i
146
+ migration_context = pool.migration_context
147
+ migrated = migration_context.get_all_versions
148
+ versions = migration_context.migrations.map(&:version)
149
+
150
+ inserting = (versions - migrated).select { |v| v < version }
151
+ inserting << version unless migrated.include?(version)
152
+ return if inserting.empty?
153
+
154
+ duplicated = inserting.tally.filter_map { |v, count| v if count > 1 }
155
+ raise <<~MSG.squish if duplicated.present?
156
+ Duplicate migration #{duplicated.first}.
157
+ Please renumber your migrations to resolve the conflict.
158
+ MSG
159
+
160
+ VersionedCommands::SchemaTable.new(pool).create_table
161
+ execute insert_versions_sql(inserting)
162
+ end
163
+
164
+ # Add proper support for schema load when using versioned commands
165
+ def insert_versions_sql(versions)
166
+ return super unless PostgreSQL.config.versioned_commands.enabled
167
+
168
+ commands = pool.migration_context.migration_commands.select do |migration|
169
+ versions.include?(migration.version)
170
+ end
171
+
172
+ return super if commands.empty?
173
+
174
+ table = quote_table_name(VersionedCommands::SchemaTable.new(pool).table_name)
175
+
176
+ sql = super(versions - commands.map(&:version))
177
+ sql << "\nINSERT INTO #{table} (version, type, object_name) VALUES\n"
178
+ sql << commands.map do |m|
179
+ +"(#{quote(m.version)}, #{quote(m.type)}, #{quote(m.object_name)})"
180
+ end.join(",\n")
181
+ sql << ";"
182
+ sql
183
+ end
184
+
131
185
  private
132
186
 
133
187
  # Remove the schema from the sequence name
@@ -135,6 +189,12 @@ module Torque
135
189
  super(table_name.split('.').last, column_name, suffix)
136
190
  end
137
191
 
192
+ # Helper for supporting schema name in several methods
193
+ def sanitize_name_with_schema(name, options)
194
+ return name if (schema = options&.delete(:schema)).blank?
195
+ Quoting::Name.new(schema.to_s, name.to_s)
196
+ end
197
+
138
198
  def quote_enum_values(name, values, options)
139
199
  prefix = options[:prefix]
140
200
  prefix = name if prefix === true
@@ -3,39 +3,26 @@
3
3
  module Torque
4
4
  module PostgreSQL
5
5
  module Arel
6
- nodes = ::Arel::Nodes
7
- inflix = nodes::InfixOperation
8
- visitors = ::Arel::Visitors::PostgreSQL
9
- default_alias = :visit_Arel_Nodes_InfixOperation
10
-
11
6
  Math = Module.new
12
- INFLIX_OPERATION = {
13
- 'Overlaps' => :'&&',
14
- 'Contains' => :'@>',
15
- 'ContainedBy' => :'<@',
16
- 'HasKey' => :'?',
17
- 'HasAllKeys' => :'?&',
18
- 'HasAnyKeys' => :'?|',
19
- 'StrictlyLeft' => :'<<',
20
- 'StrictlyRight' => :'>>',
21
- 'DoesntRightExtend' => :'&<',
22
- 'DoesntLeftExtend' => :'&>',
23
- 'AdjacentTo' => :'-|-',
24
- }.freeze
25
7
 
26
- INFLIX_OPERATION.each do |operator_name, operator|
27
- next if nodes.const_defined?(operator_name)
8
+ def self.build_operations(operations)
9
+ default_alias = :visit_Arel_Nodes_InfixOperation
10
+
11
+ operations&.each do |name, operator|
12
+ klass_name = name.to_s.camelize
13
+ next if ::Arel::Nodes.const_defined?(klass_name)
28
14
 
29
- klass = Class.new(inflix)
30
- klass.send(:define_method, :initialize) { |*args| super(operator, *args) }
15
+ klass = Class.new(::Arel::Nodes::InfixOperation)
16
+ operator = (-operator).to_sym
17
+ klass.send(:define_method, :initialize) { |*args| super(operator, *args) }
31
18
 
32
- nodes.const_set(operator_name, klass)
33
- visitors.send(:alias_method, :"visit_Arel_Nodes_#{operator_name}", default_alias)
19
+ ::Arel::Nodes.const_set(klass_name, klass)
20
+ visitor = :"visit_Arel_Nodes_#{klass_name}"
21
+ ::Arel::Visitors::PostgreSQL.send(:alias_method, visitor, default_alias)
34
22
 
35
- # Don't worry about quoting here, if the right side is something that
36
- # doesn't need quoting, it will leave it as it is
37
- Math.send(:define_method, operator_name.underscore) do |other|
38
- klass.new(self, other)
23
+ # Don't worry about quoting here, if the right side is something that
24
+ # doesn't need quoting, it will leave it as it is
25
+ Math.send(:define_method, klass_name.underscore) { |other| klass.new(self, other) }
39
26
  end
40
27
  end
41
28
 
@@ -13,18 +13,32 @@ 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
17
17
  right << '[]' if array
18
18
  super left, right
19
19
  end
20
20
  end
21
21
 
22
+ class Ref < ::Arel::Nodes::Unary
23
+ attr_reader :reference
24
+ alias to_s expr
25
+
26
+ def initialize(expr, reference = nil)
27
+ @reference = reference
28
+ super expr
29
+ end
30
+
31
+ def as(other)
32
+ @reference&.as(other) || super
33
+ end
34
+ end
35
+
22
36
  end
23
37
 
24
38
  ::Arel.define_singleton_method(:array) do |*values, cast: nil|
25
39
  values = values.first if values.size.eql?(1) && values.first.is_a?(::Enumerable)
26
40
  result = ::Arel::Nodes.build_quoted(values)
27
- result = result.cast(cast, true) if cast.present?
41
+ result = result.pg_cast(cast, true) if cast.present?
28
42
  result
29
43
  end
30
44
 
@@ -6,10 +6,16 @@ module Torque
6
6
  module Operations
7
7
 
8
8
  # Create a cast operation
9
- def cast(type, array = false)
9
+ def pg_cast(type, array = false)
10
10
  Nodes::Cast.new(self, type, array)
11
11
  end
12
12
 
13
+ # Make sure to add proper support over AR's own +cast+ method while
14
+ # still allow attributes to be casted
15
+ def cast(type, array = false)
16
+ defined?(super) && !array ? super(type) : pg_cast(type, array)
17
+ end
18
+
13
19
  end
14
20
 
15
21
  ::Arel::Attributes::Attribute.include(Operations)
@@ -4,13 +4,6 @@ module Torque
4
4
  module PostgreSQL
5
5
  module Arel
6
6
  module Visitors
7
- # Enclose select manager with parenthesis
8
- # :TODO: Remove when checking the new version of Arel
9
- def visit_Arel_SelectManager(o, collector)
10
- collector << '('
11
- visit(o.ast, collector) << ')'
12
- end
13
-
14
7
  # Add ONLY modifier to query
15
8
  def visit_Arel_Nodes_JoinSource(o, collector)
16
9
  collector << 'ONLY ' if o.only?
@@ -26,11 +19,16 @@ module Torque
26
19
  # Allow quoted arrays to get here
27
20
  def visit_Arel_Nodes_Casted(o, collector)
28
21
  value = o.value_for_database
29
- return super unless value.is_a?(::Enumerable)
30
- quote_array(value, collector)
22
+ klass = ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array::Data
23
+ return super unless value.is_a?(klass)
24
+ quote_array(value.values, collector)
31
25
  end
32
26
 
33
27
  ## TORQUE VISITORS
28
+ def visit_Torque_PostgreSQL_Arel_Nodes_Ref(o, collector)
29
+ collector << quote_table_name(o.expr)
30
+ end
31
+
34
32
  # Allow casting any node
35
33
  def visit_Torque_PostgreSQL_Arel_Nodes_Cast(o, collector)
36
34
  visit(o.left, collector) << '::' << o.right