torque-postgresql 4.0.0.rc1 → 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 (47) 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 +48 -10
  9. data/lib/torque/postgresql/adapter/schema_definitions.rb +22 -0
  10. data/lib/torque/postgresql/adapter/schema_dumper.rb +47 -1
  11. data/lib/torque/postgresql/adapter/schema_statements.rb +45 -0
  12. data/lib/torque/postgresql/arel/nodes.rb +14 -0
  13. data/lib/torque/postgresql/arel/visitors.rb +4 -0
  14. data/lib/torque/postgresql/attributes/builder/full_text_search.rb +16 -28
  15. data/lib/torque/postgresql/base.rb +2 -1
  16. data/lib/torque/postgresql/config.rb +35 -1
  17. data/lib/torque/postgresql/function.rb +33 -0
  18. data/lib/torque/postgresql/railtie.rb +26 -1
  19. data/lib/torque/postgresql/relation/auxiliary_statement.rb +7 -2
  20. data/lib/torque/postgresql/relation/buckets.rb +124 -0
  21. data/lib/torque/postgresql/relation/distinct_on.rb +7 -2
  22. data/lib/torque/postgresql/relation/inheritance.rb +18 -8
  23. data/lib/torque/postgresql/relation/join_series.rb +112 -0
  24. data/lib/torque/postgresql/relation/merger.rb +17 -3
  25. data/lib/torque/postgresql/relation.rb +18 -28
  26. data/lib/torque/postgresql/version.rb +1 -1
  27. data/lib/torque/postgresql/versioned_commands/command_migration.rb +146 -0
  28. data/lib/torque/postgresql/versioned_commands/generator.rb +57 -0
  29. data/lib/torque/postgresql/versioned_commands/migration_context.rb +83 -0
  30. data/lib/torque/postgresql/versioned_commands/migrator.rb +39 -0
  31. data/lib/torque/postgresql/versioned_commands/schema_table.rb +101 -0
  32. data/lib/torque/postgresql/versioned_commands.rb +161 -0
  33. data/spec/fixtures/migrations/20250101000001_create_users.rb +0 -0
  34. data/spec/fixtures/migrations/20250101000002_create_function_count_users_v1.sql +0 -0
  35. data/spec/fixtures/migrations/20250101000003_create_internal_users.rb +0 -0
  36. data/spec/fixtures/migrations/20250101000004_update_function_count_users_v2.sql +0 -0
  37. data/spec/fixtures/migrations/20250101000005_create_view_all_users_v1.sql +0 -0
  38. data/spec/fixtures/migrations/20250101000006_create_type_user_id_v1.sql +0 -0
  39. data/spec/fixtures/migrations/20250101000007_remove_function_count_users_v2.sql +0 -0
  40. data/spec/initialize.rb +9 -0
  41. data/spec/schema.rb +2 -4
  42. data/spec/spec_helper.rb +6 -1
  43. data/spec/tests/full_text_seach_test.rb +30 -2
  44. data/spec/tests/relation_spec.rb +229 -0
  45. data/spec/tests/schema_spec.rb +4 -1
  46. data/spec/tests/versioned_commands_spec.rb +513 -0
  47. metadata +33 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4a341f61802bc246d4004168a49fc5f67156b7dc3c962321a28e73fc8288e0db
4
- data.tar.gz: ca658f344ed87f29b94eb1e115a13b4ef6d4d954c450b6f4e9892f7788b35e39
3
+ metadata.gz: d80bb198f5645c35915440f05a4ad083830221de27983c4c7b936c394e7bcbbb
4
+ data.tar.gz: a73a8de3a63b806e08f42109a3bb703e7ce19a20ae55a317c7fb763215612396
5
5
  SHA512:
6
- metadata.gz: 4e83150ee16db03f72e39d85c33932f38f43351501e673133e42f7df98208cacfe34e9eb0b505ede452fae872b770c5de9c8cdded5927b5d527fc9b6a7757dc7
7
- data.tar.gz: 91c9b79d9698c720c5ed77a775f9758069ac41c5fd625c7870180e85a8f4ed16a4998c4b93ff8de04eb6e828b689046801b9a3e7c76efeef22dc0a91b9083797
6
+ metadata.gz: 42a366f328d284ed37c601e0007a32f65b6615f69185b44fb82e0b943ab77a9e5b810189e00f2f200cf9e9b22e373bf33798c5496c76cfbfcd236bcc8bb923fe
7
+ data.tar.gz: 45385a880da9922eeb72c4d7e2adb29c26081865747c080973d409e61866a027cccd14a4276c8e0f95daf34d4d59c74a0a1c7cd7aecbb611763734cd12d3e064
@@ -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 FunctionGenerator < Rails::Generators::Base
8
+ include Torque::PostgreSQL::VersionedCommands::Generator
9
+
10
+ alias create_function_file create_migration_file
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,4 @@
1
+ CREATE OR REPLACE FUNCTION <%= name %>()
2
+ RETURNS void AS $$
3
+ -- Function body goes here
4
+ $$ LANGUAGE sql;
@@ -0,0 +1,2 @@
1
+ DROP TYPE IF EXISTS <%= name %>;
2
+ CREATE TYPE <%= name %>;
@@ -0,0 +1,3 @@
1
+ <%= "DROP MATERIALIZED VIEW IF EXISTS #{name};\n" if options[:materialized] %>CREATE <%= options[:materialized] ? 'MATERIALIZED' : 'OR REPLACE' %> VIEW <%= name %> AS (
2
+ -- View body goes here
3
+ );
@@ -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 TypeGenerator < Rails::Generators::Base
8
+ include Torque::PostgreSQL::VersionedCommands::Generator
9
+
10
+ alias create_type_file create_migration_file
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'torque/postgresql/versioned_commands/generator'
4
+
5
+ module Torque
6
+ module Generators
7
+ class ViewGenerator < Rails::Generators::Base
8
+ include Torque::PostgreSQL::VersionedCommands::Generator
9
+
10
+ class_option :materialized, type: :boolean, aliases: %i(--m), default: false,
11
+ desc: 'Use materialized view instead of regular view'
12
+
13
+ alias create_view_file create_migration_file
14
+ end
15
+ end
16
+ end
@@ -154,19 +154,10 @@ module Torque
154
154
 
155
155
  # Build the query for allowed schemas
156
156
  def user_defined_schemas_sql
157
- conditions = []
158
- conditions << <<-SQL.squish if schemas_blacklist.any?
159
- nspname NOT LIKE ALL (ARRAY['#{schemas_blacklist.join("', '")}'])
160
- SQL
161
-
162
- conditions << <<-SQL.squish if schemas_whitelist.any?
163
- nspname LIKE ANY (ARRAY['#{schemas_whitelist.join("', '")}'])
164
- SQL
165
-
166
157
  <<-SQL.squish
167
158
  SELECT nspname
168
159
  FROM pg_catalog.pg_namespace
169
- WHERE 1=1 AND #{conditions.join(' AND ')}
160
+ WHERE 1=1 AND #{filter_by_schema.join(' AND ')}
170
161
  ORDER BY oid
171
162
  SQL
172
163
  end
@@ -191,6 +182,53 @@ module Torque
191
182
  SQL
192
183
  end
193
184
 
185
+ # Get all possible schema entries that can be created via versioned
186
+ # commands of the provided type. Mostly for covering removals and not
187
+ # dump them
188
+ def list_versioned_commands(type)
189
+ query =
190
+ case type
191
+ when :function
192
+ <<-SQL.squish
193
+ SELECT n.nspname AS schema, p.proname AS name
194
+ FROM pg_catalog.pg_proc p
195
+ INNER JOIN pg_namespace n ON n.oid = p.pronamespace
196
+ WHERE 1=1 AND #{filter_by_schema.join(' AND ')};
197
+ SQL
198
+ when :type
199
+ <<-SQL.squish
200
+ SELECT n.nspname AS schema, t.typname AS name
201
+ FROM pg_type t
202
+ INNER JOIN pg_namespace n ON n.oid = t.typnamespace
203
+ WHERE 1=1 AND t.typtype NOT IN ('e')
204
+ AND #{filter_by_schema.join(' AND ')};
205
+ SQL
206
+ when :view
207
+ <<-SQL.squish
208
+ SELECT n.nspname AS schema, c.relname AS name
209
+ FROM pg_class c
210
+ INNER JOIN pg_namespace n ON n.oid = c.relnamespace
211
+ WHERE 1=1 AND c.relkind IN ('v', 'm')
212
+ AND #{filter_by_schema.join(' AND ')};
213
+ SQL
214
+ end
215
+
216
+ select_rows(query, 'SCHEMA')
217
+ end
218
+
219
+ # Build the condition for filtering by schema
220
+ def filter_by_schema
221
+ conditions = []
222
+ conditions << <<-SQL.squish if schemas_blacklist.any?
223
+ nspname NOT LIKE ALL (ARRAY['#{schemas_blacklist.join("', '")}'])
224
+ SQL
225
+
226
+ conditions << <<-SQL.squish if schemas_whitelist.any?
227
+ nspname LIKE ANY (ARRAY['#{schemas_whitelist.join("', '")}'])
228
+ SQL
229
+ conditions
230
+ end
231
+
194
232
  end
195
233
  end
196
234
  end
@@ -49,6 +49,28 @@ module Torque
49
49
  end
50
50
  end
51
51
 
52
+ # Add exclusive support for versioned commands when importing from schema
53
+ # dump. This ensures that such methods are not available in regular
54
+ # migrations.
55
+ module Definition
56
+
57
+ def create_function(name, version:, dir: pool.migrations_paths)
58
+ return super unless VersionedCommands.valid_type?(:function)
59
+ execute VersionedCommands.fetch_command(dir, :function, name, version)
60
+ end
61
+
62
+ def create_type(name, version:, dir: pool.migrations_paths)
63
+ return super unless VersionedCommands.valid_type?(:type)
64
+ execute VersionedCommands.fetch_command(dir, :type, name, version)
65
+ end
66
+
67
+ def create_view(name, version:, dir: pool.migrations_paths)
68
+ return super unless VersionedCommands.valid_type?(:view)
69
+ execute VersionedCommands.fetch_command(dir, :view, name, version)
70
+ end
71
+
72
+ end
73
+
52
74
  ActiveRecord::ConnectionAdapters::PostgreSQL::Table.include ColumnMethods
53
75
  ActiveRecord::ConnectionAdapters::PostgreSQL::TableDefinition.include TableDefinition
54
76
  end
@@ -12,6 +12,15 @@ module Torque
12
12
  (?:\s*,\s*'([A-D])')?
13
13
  /ix
14
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
+
15
24
  def dump(stream) # :nodoc:
16
25
  @connection.dump_mode!
17
26
  super
@@ -22,6 +31,13 @@ module Torque
22
31
 
23
32
  private
24
33
 
34
+ def types(stream) # :nodoc:
35
+ super
36
+
37
+ versioned_commands(stream, :type)
38
+ versioned_commands(stream, :function)
39
+ end
40
+
25
41
  def tables(stream) # :nodoc:
26
42
  around_tables(stream) { dump_tables(stream) }
27
43
  end
@@ -30,6 +46,7 @@ module Torque
30
46
  functions(stream) if fx_functions_position == :beginning
31
47
 
32
48
  yield
49
+ versioned_commands(stream, :view, true)
33
50
 
34
51
  functions(stream) if fx_functions_position == :end
35
52
  triggers(stream) if defined?(::Fx::SchemaDumper::Trigger)
@@ -70,7 +87,7 @@ module Torque
70
87
  # dump foreign keys at the end to make sure all dependent tables exist.
71
88
  if @connection.supports_foreign_keys?
72
89
  foreign_keys_stream = StringIO.new
73
- sorted_tables.each do |tbl|
90
+ sorted_tables.each do |(tbl, *)|
74
91
  foreign_keys(tbl, foreign_keys_stream)
75
92
  end
76
93
 
@@ -147,6 +164,35 @@ module Torque
147
164
  settings.to_h.transform_values(&:inspect)
148
165
  end
149
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
+
150
196
  def fx_functions_position
151
197
  return unless defined?(::Fx::SchemaDumper::Function)
152
198
  Fx.configuration.dump_functions_at_beginning_of_schema ? :beginning : :end
@@ -137,6 +137,51 @@ module Torque
137
137
  super + [:schema, :inherits]
138
138
  end
139
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
+
140
185
  private
141
186
 
142
187
  # Remove the schema from the sequence name
@@ -19,6 +19,20 @@ module Torque
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,6 +25,10 @@ module Torque
25
25
  end
26
26
 
27
27
  ## TORQUE VISITORS
28
+ def visit_Torque_PostgreSQL_Arel_Nodes_Ref(o, collector)
29
+ collector << quote_table_name(o.expr)
30
+ end
31
+
28
32
  # Allow casting any node
29
33
  def visit_Torque_PostgreSQL_Arel_Nodes_Cast(o, collector)
30
34
  visit(o.left, collector) << '::' << o.right
@@ -6,7 +6,7 @@ module Torque
6
6
  module Builder
7
7
  class FullTextSearch
8
8
  attr_accessor :klass, :attribute, :options, :klass_module,
9
- :default_rank, :default_order, :default_language
9
+ :default_rank, :default_mode, :default_order, :default_language
10
10
 
11
11
  def initialize(klass, attribute, options = {})
12
12
  @klass = klass
@@ -14,6 +14,7 @@ module Torque
14
14
  @options = options
15
15
 
16
16
  @default_rank = options[:with_rank] == true ? 'rank' : options[:with_rank]&.to_s
17
+ @default_mode = options[:mode] || PostgreSQL.config.full_text_search.default_mode
17
18
 
18
19
  @default_order =
19
20
  case options[:order]
@@ -53,30 +54,6 @@ module Torque
53
54
  end
54
55
 
55
56
  # Creates a class method as the scope that builds the full text search
56
- #
57
- # def full_text_search(value, order: :asc, rank: :rank, language: 'english', phrase: true)
58
- # attr = arel_table["search_vector"]
59
- # fn = ::Torque::PostgreSQL::FN
60
- #
61
- # lang = language.to_s if !language.is_a?(::Symbol)
62
- # lang ||= arel_table[language.to_s].pg_cast(:regconfig) if has_attribute?(language)
63
- # lang ||= public_send(language) if respond_to?(language)
64
- #
65
- # raise ArgumentError, <<~MSG.squish if lang.nil?
66
- # Unable to determine language from #{language.inspect}.
67
- # MSG
68
- #
69
- # value = fn.bind(:value, value.to_s, attr.type_caster)
70
- # lang = fn.bind(:lang, lang, attr.type_caster) if lang.is_a?(::String)
71
- #
72
- # query = fn.public_send(phrase ? :phraseto_tsquery : :to_tsquery, lang, value)
73
- # ranker = fn.ts_rank(attr, query) if rank || order
74
- #
75
- # result = where(fn.infix(:"@@", attr, query))
76
- # result = result.order(ranker.public_send(order == :desc ? :desc : :asc)) if order
77
- # result.select_extra_values += [ranker.as(rank == true ? 'rank' : rank.to_s)] if rank
78
- # result
79
- # end
80
57
  def add_scope_to_module
81
58
  klass_module.module_eval <<-RUBY, __FILE__, __LINE__ + 1
82
59
  def #{scope_name}(value#{scope_args})
@@ -87,14 +64,25 @@ module Torque
87
64
  lang ||= arel_table[language.to_s] if has_attribute?(language)
88
65
  lang ||= public_send(language) if respond_to?(language)
89
66
 
90
- raise ::ArgumentError, <<~MSG.squish if lang.nil?
67
+ function = {
68
+ default: :to_tsquery,
69
+ phrase: :phraseto_tsquery,
70
+ plain: :plainto_tsquery,
71
+ web: :websearch_to_tsquery,
72
+ }[mode.to_sym]
73
+
74
+ raise ::ArgumentError, <<~MSG.squish if lang.blank?
91
75
  Unable to determine language from \#{language.inspect}.
92
76
  MSG
93
77
 
78
+ raise ::ArgumentError, <<~MSG.squish if function.nil?
79
+ Invalid mode \#{mode.inspect} for full text search.
80
+ MSG
81
+
94
82
  value = fn.bind(:value, value.to_s, attr.type_caster)
95
83
  lang = fn.bind(:lang, lang, attr.type_caster) if lang.is_a?(::String)
96
84
 
97
- query = fn.public_send(phrase ? :phraseto_tsquery : :to_tsquery, lang, value)
85
+ query = fn.public_send(function, lang, value)
98
86
  ranker = fn.ts_rank(attr, query) if rank || order
99
87
 
100
88
  result = where(fn.infix(:"@@", attr, query))
@@ -111,7 +99,7 @@ module Torque
111
99
  args << ", order: #{default_order.inspect}"
112
100
  args << ", rank: #{default_rank.inspect}"
113
101
  args << ", language: #{default_language.inspect}"
114
- args << ", phrase: true"
102
+ args << ", mode: :#{default_mode}"
115
103
  args
116
104
  end
117
105
  end
@@ -17,7 +17,8 @@ module Torque
17
17
  end
18
18
 
19
19
  class_methods do
20
- delegate :distinct_on, :with, :itself_only, :cast_records, to: :all
20
+ delegate :distinct_on, :with, :itself_only, :cast_records, :join_series,
21
+ :buckets, to: :all
21
22
 
22
23
  # Make sure that table name is an instance of TableName class
23
24
  def reset_table_name
@@ -22,6 +22,12 @@ module Torque
22
22
  # same configuration is set to true
23
23
  config.eager_load = false
24
24
 
25
+ # Add support for joining any query/association with a generated series
26
+ config.join_series = true
27
+
28
+ # Add support for querying and calculating histogram buckets
29
+ config.buckets = true
30
+
25
31
  # Set a list of irregular model name when associated with table names
26
32
  config.irregular_models = {}
27
33
  def config.irregular_models=(hash)
@@ -290,6 +296,14 @@ module Torque
290
296
  # Defines the default language when generating search vector columns
291
297
  fts.default_language = 'english'
292
298
 
299
+ # Defines the default mode to be used when generating full text search
300
+ # queries. It can be one of the following:
301
+ # - :default (to_tsquery)
302
+ # - :phrase (phraseto_tsquery)
303
+ # - :plain (plainto_tsquery)
304
+ # - :web (websearch_to_tsquery)
305
+ fts.default_mode = :phrase
306
+
293
307
  # Defines the default index type to be used when creating search vector.
294
308
  # It still requires that the column requests an index
295
309
  fts.default_index_type = :gin
@@ -303,7 +317,7 @@ module Torque
303
317
  builder.enabled = %i[regexp arel_attribute enumerator_lazy]
304
318
 
305
319
  # When active, values provided to array attributes will be handled more
306
- # efficiently. It will use the +ANY+ operator on a equality check and
320
+ # friendly. It will use the +ANY+ operator on a equality check and
307
321
  # overlaps when the given value is an array
308
322
  builder.handle_array_attributes = false
309
323
 
@@ -318,5 +332,25 @@ module Torque
318
332
 
319
333
  end
320
334
 
335
+ # Configure versioned commands features
336
+ config.nested(:versioned_commands) do |vs|
337
+
338
+ # This is a feature that developers must explicitly opt-in. It is designed
339
+ # in a way that prevents a large impact on Rails' original migrations
340
+ # behavior. But, it is still a feature that everyone may not need, and
341
+ # some may complain about the additional schema table, which also uses
342
+ # inheritance
343
+ vs.enabled = false
344
+
345
+ # Define the list of commands that are going to be versioned by this
346
+ # method
347
+ vs.types = %i[function type view]
348
+
349
+ # The name of the table that will inherit from +schema_migrations+ and
350
+ # store the list of versioned commands that have been executed
351
+ vs.table_name = 'schema_versioned_commands'
352
+
353
+ end
354
+
321
355
  end
322
356
  end
@@ -25,6 +25,14 @@ module Torque
25
25
  bind(arel_attribute.name, value, arel_attribute.type_caster)
26
26
  end
27
27
 
28
+ # A facilitator to create a bind param with a specific type
29
+ def bind_type(value, type = nil, name: 'value', cast: nil)
30
+ type ||= ruby_type_to_model_type(value)
31
+ type = ActiveModel::Type.lookup(type) if type.is_a?(Symbol)
32
+ result = bind(name, value, type)
33
+ cast ? result.pg_cast(cast) : result
34
+ end
35
+
28
36
  # A facilitator to create an infix operation
29
37
  def infix(op, left, right)
30
38
  ::Arel::Nodes::InfixOperation.new(op, left, right)
@@ -38,6 +46,12 @@ module Torque
38
46
  args.reduce { |left, right| infix(:"||", left, right) }
39
47
  end
40
48
 
49
+ # A simple helper to trick Rails into producing the right SQL for
50
+ # grouping operations
51
+ def group_by(arel, name)
52
+ Arel::Nodes::Ref.new(name.to_s, arel)
53
+ end
54
+
41
55
  # As of now, this indicates that it supports any direct calls, since
42
56
  # the idea is to simply map to an Arel function with the same name,
43
57
  # without checking if it actually exists
@@ -53,6 +67,25 @@ module Torque
53
67
  ::Arel::Nodes::NamedFunction.new(name.to_s.upcase, args)
54
68
  end
55
69
 
70
+ private
71
+
72
+ def ruby_type_to_model_type(value)
73
+ case value
74
+ when Integer then :integer
75
+ when Float then :float
76
+ when String then :string
77
+ when Time, ActiveSupport::TimeWithZone then :time
78
+ when TrueClass, FalseClass then :boolean
79
+ when DateTime then :datetime
80
+ when Date then :date
81
+ when BigDecimal then :decimal
82
+ when ActiveSupport::Duration
83
+ Adapter::OID::Interval.new
84
+ else
85
+ raise ArgumentError, "Cannot infer type from value: #{value.inspect}."
86
+ end
87
+ end
88
+
56
89
  end
57
90
  end
58
91
 
@@ -19,6 +19,17 @@ module Torque
19
19
  ActiveRecord::Base.belongs_to_many_required_by_default =
20
20
  torque_config.associations.belongs_to_many_required_by_default
21
21
 
22
+ ## General features
23
+ if torque_config.join_series
24
+ require_relative 'relation/join_series'
25
+ Relation.include(Relation::JoinSeries)
26
+ end
27
+
28
+ if torque_config.buckets
29
+ require_relative 'relation/buckets'
30
+ Relation.include(Relation::Buckets)
31
+ end
32
+
22
33
  ## Schemas Enabled Setup
23
34
  if (config = torque_config.schemas).enabled
24
35
  require_relative 'adapter/schema_overrides'
@@ -109,7 +120,21 @@ module Torque
109
120
  PostgreSQL::Arel.build_operations(torque_config.arel.infix_operators)
110
121
  if (mod = torque_config.arel.expose_function_helper_on&.to_s)
111
122
  parent, _, name = mod.rpartition('::')
112
- parent.constantize.const_set(name, PostgreSQL::FN)
123
+ parent = parent ? parent.constantize : ::Object
124
+
125
+ raise ArgumentError, <<~MSG.squish if parent.const_defined?(name)
126
+ Unable to expose Arel function helper on #{mod} because the constant
127
+ #{name} is already defined on #{parent}. Please choose a different name.
128
+ MSG
129
+
130
+ parent.const_set(name, PostgreSQL::FN)
131
+ end
132
+
133
+ ## Versioned Commands Setup
134
+ if (config = torque_config.versioned_commands).enabled
135
+ require_relative 'versioned_commands'
136
+
137
+ ActiveRecord::Schema::Definition.include(Adapter::Definition)
113
138
  end
114
139
 
115
140
  # Make sure to load all the types that are handled by this gem on
@@ -6,9 +6,14 @@ module Torque
6
6
  module AuxiliaryStatement
7
7
 
8
8
  # :nodoc:
9
- def auxiliary_statements_values; get_value(:auxiliary_statements); end
9
+ def auxiliary_statements_values
10
+ @values.fetch(:auxiliary_statements, FROZEN_EMPTY_ARRAY)
11
+ end
10
12
  # :nodoc:
11
- def auxiliary_statements_values=(value); set_value(:auxiliary_statements, value); end
13
+ def auxiliary_statements_values=(value)
14
+ assert_modifiable!
15
+ @values[:auxiliary_statements] = value
16
+ end
12
17
 
13
18
  # Set use of an auxiliary statement
14
19
  def with(*args, **settings)