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,41 +4,148 @@ module Torque
4
4
  module PostgreSQL
5
5
  # = Torque PostgreSQL Railtie
6
6
  class Railtie < Rails::Railtie # :nodoc:
7
-
8
7
  # Get information from the running rails app
9
8
  initializer 'torque-postgresql' do |app|
10
- torque_config = Torque::PostgreSQL.config
11
- torque_config.eager_load = app.config.eager_load
12
-
13
- # Include enum on ActiveRecord::Base so it can have the correct enum
14
- # initializer
15
- Torque::PostgreSQL::Attributes::Enum.include_on(ActiveRecord::Base)
16
- Torque::PostgreSQL::Attributes::EnumSet.include_on(ActiveRecord::Base)
17
- Torque::PostgreSQL::Attributes::Period.include_on(ActiveRecord::Base)
18
-
19
- # Setup belongs_to_many association
20
- ActiveRecord::Base.belongs_to_many_required_by_default = torque_config.associations
21
- .belongs_to_many_required_by_default
22
-
23
- # Define a method to find enumaerators based on the namespace
24
- torque_config.enum.namespace.define_singleton_method(:const_missing) do |name|
25
- Torque::PostgreSQL::Attributes::Enum.lookup(name)
26
- end
9
+ ActiveSupport.on_load(:active_record_postgresqladapter) do
10
+ ActiveSupport.on_load(:active_record) do
11
+ torque_config = Torque::PostgreSQL.config
12
+ torque_config.eager_load = app.config.eager_load
27
13
 
28
- # Define a helper method to get a sample value
29
- torque_config.enum.namespace.define_singleton_method(:sample) do |name|
30
- Torque::PostgreSQL::Attributes::Enum.lookup(name).sample
31
- end
14
+ # TODO: Only load files that have their features enabled, like CTE
15
+
16
+ ar_type = ActiveRecord::Type
17
+
18
+ # Setup belongs_to_many association
19
+ ActiveRecord::Base.belongs_to_many_required_by_default =
20
+ torque_config.associations.belongs_to_many_required_by_default
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
+
33
+ ## Schemas Enabled Setup
34
+ if (config = torque_config.schemas).enabled
35
+ require_relative 'adapter/schema_overrides'
36
+ end
37
+
38
+ ## CTE Enabled Setup
39
+ if (config = torque_config.auxiliary_statement).enabled
40
+ require_relative 'auxiliary_statement'
41
+ require_relative 'relation/auxiliary_statement'
42
+ Relation.include(Relation::AuxiliaryStatement)
43
+
44
+ # Define the exposed constant for both types of auxiliary statements
45
+ if config.exposed_class.present?
46
+ *ns, name = config.exposed_class.split('::')
47
+ base = ns.present? ? ::Object.const_get(ns.join('::')) : ::Object
48
+ base.const_set(name, AuxiliaryStatement)
49
+
50
+ *ns, name = config.exposed_recursive_class.split('::')
51
+ base = ns.present? ? ::Object.const_get(ns.join('::')) : ::Object
52
+ base.const_set(name, AuxiliaryStatement::Recursive)
53
+ end
54
+ end
55
+
56
+ ## Enum Enabled Setup
57
+ if (config = torque_config.enum).enabled
58
+ require_relative 'adapter/oid/enum'
59
+ require_relative 'adapter/oid/enum_set'
60
+
61
+ require_relative 'attributes/enum'
62
+ require_relative 'attributes/enum_set'
63
+
64
+ Attributes::Enum.include_on(ActiveRecord::Base)
65
+ Attributes::EnumSet.include_on(ActiveRecord::Base)
66
+
67
+ ar_type.register(:enum, Adapter::OID::Enum, adapter: :postgresql)
68
+ ar_type.register(:enum_set, Adapter::OID::EnumSet, adapter: :postgresql)
69
+
70
+ if config.namespace == false
71
+ # TODO: Allow enum classes to exist without a namespace
72
+ config.namespace = PostgreSQL.const_set('Enum', Module.new)
73
+ else
74
+ config.namespace ||= ::Object.const_set('Enum', Module.new)
75
+
76
+ # Define a method to find enumerators based on the namespace
77
+ config.namespace.define_singleton_method(:const_missing) do |name|
78
+ Attributes::Enum.lookup(name)
79
+ end
80
+
81
+ # Define a helper method to get a sample value
82
+ config.namespace.define_singleton_method(:sample) do |name|
83
+ Attributes::Enum.lookup(name).sample
84
+ end
85
+ end
86
+ end
87
+
88
+ ## Geometry Enabled Setup
89
+ if (config = torque_config.geometry).enabled
90
+ require_relative 'adapter/oid/box'
91
+ require_relative 'adapter/oid/circle'
92
+ require_relative 'adapter/oid/line'
93
+ require_relative 'adapter/oid/segment'
94
+
95
+ ar_type.register(:box, Adapter::OID::Box, adapter: :postgresql)
96
+ ar_type.register(:circle, Adapter::OID::Circle, adapter: :postgresql)
97
+ ar_type.register(:line, Adapter::OID::Line, adapter: :postgresql)
98
+ ar_type.register(:segment, Adapter::OID::Segment, adapter: :postgresql)
99
+ end
100
+
101
+ ## Period Enabled Setup
102
+ if (config = torque_config.period).enabled
103
+ require_relative 'attributes/period'
104
+ Attributes::Period.include_on(ActiveRecord::Base)
105
+ end
106
+
107
+ ## Interval Enabled Setup
108
+ if (config = torque_config.interval).enabled
109
+ require_relative 'adapter/oid/interval'
110
+ ar_type.register(:interval, Adapter::OID::Interval, adapter: :postgresql)
111
+ end
112
+
113
+ ## Full Text Search Enabled Setup
114
+ if (config = torque_config.full_text_search).enabled
115
+ require_relative 'attributes/full_text_search'
116
+ Attributes::FullTextSearch.include_on(ActiveRecord::Base)
117
+ end
118
+
119
+ ## Arel Setup
120
+ PostgreSQL::Arel.build_operations(torque_config.arel.infix_operators)
121
+ if (mod = torque_config.arel.expose_function_helper_on&.to_s)
122
+ parent, _, name = mod.rpartition('::')
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)
138
+ end
32
139
 
33
- # Define the exposed constant for both types of auxiliary statements
34
- if torque_config.auxiliary_statement.exposed_class.present?
35
- *ns, name = torque_config.auxiliary_statement.exposed_class.split('::')
36
- base = ns.present? ? Object.const_get(ns.join('::')) : Object
37
- base.const_set(name, Torque::PostgreSQL::AuxiliaryStatement)
140
+ # Make sure to load all the types that are handled by this gem on
141
+ # each individual PG connection
142
+ adapter = ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
143
+ ActiveRecord::Base.connection_handler.each_connection_pool do |pool|
144
+ next unless pool.db_config.adapter_class.is_a?(adapter)
38
145
 
39
- *ns, name = torque_config.auxiliary_statement.exposed_recursive_class.split('::')
40
- base = ns.present? ? Object.const_get(ns.join('::')) : Object
41
- base.const_set(name, Torque::PostgreSQL::AuxiliaryStatement::Recursive)
146
+ pool.with_connection { |conn| conn.torque_load_additional_types }
147
+ end
148
+ end
42
149
  end
43
150
  end
44
151
  end
@@ -5,29 +5,25 @@ module Torque
5
5
  module Reflection
6
6
  module AbstractReflection
7
7
  AREL_ATTR = ::Arel::Attributes::Attribute
8
-
9
- ARR_NO_CAST = 'bigint'
10
- ARR_CAST = 'bigint[]'
8
+ AREL_NODE = ::Arel::Nodes::Node
11
9
 
12
10
  # Check if the foreign key actually exists
13
11
  def connected_through_array?
14
12
  false
15
13
  end
16
14
 
17
- # Fix where the join_scope method is the one now responsible for
18
- # building the join condition
15
+ # Connection through an array-like attribute is more complex then just
16
+ # a simple eq. This needs to go through the channel that handles larger
17
+ # situations
19
18
  def join_scope(table, foreign_table, foreign_klass)
20
19
  return super unless connected_through_array?
21
20
 
22
- predicate_builder = predicate_builder(table)
21
+ table_md = ActiveRecord::TableMetadata.new(klass, table)
22
+ predicate_builder = klass.predicate_builder.with(table_md)
23
23
  scope_chain_items = join_scopes(table, predicate_builder)
24
24
  klass_scope = klass_join_scope(table, predicate_builder)
25
25
 
26
26
  klass_scope.where!(build_id_constraint_between(table, foreign_table))
27
- klass_scope.where!(type => foreign_klass.polymorphic_name) if type
28
- klass_scope.where!(klass.send(:type_condition, table)) \
29
- if klass.finder_needs_type_condition?
30
-
31
27
  scope_chain_items.inject(klass_scope, &:merge!)
32
28
  end
33
29
 
@@ -40,43 +36,15 @@ module Torque
40
36
  result
41
37
  end
42
38
 
43
- # Build the id constraint checking if both types are perfect matching.
44
- # The klass attribute (left side) will always be a column attribute
45
- def build_id_constraint(klass_attr, source_attr)
46
- return klass_attr.eq(source_attr) unless connected_through_array?
47
-
48
- # Klass and key are associated with the reflection Class
49
- klass_type = klass.columns_hash[join_keys.key.to_s]
50
-
51
- # Apply an ANY operation which checks if the single value on the left
52
- # side exists in the array on the right side
53
- if source_attr.is_a?(AREL_ATTR)
54
- any_value = [klass_attr, source_attr]
55
- any_value.reverse! if klass_type.try(:array?)
56
- return any_value.shift.eq(::Arel::Nodes::NamedFunction.new('ANY', any_value))
57
- end
58
-
59
- # If the left side is not an array, just use the IN condition
60
- return klass_attr.in(source_attr) unless klass_type.try(:array)
61
-
62
- # Build the overlap condition (array && array) ensuring that the right
63
- # side has the same type as the left side
64
- source_attr = ::Arel::Nodes.build_quoted(Array.wrap(source_attr))
65
- klass_attr.overlaps(source_attr.cast(klass_type.sql_type_metadata.sql_type))
66
- end
67
-
68
- # TODO: Deprecate this method
69
- def join_keys
70
- OpenStruct.new(key: join_primary_key, foreign_key: join_foreign_key)
71
- end
72
-
73
39
  private
74
40
 
41
+ # This one is a lot simpler, now that we have a predicate builder that
42
+ # knows exactly what to do with 2 array-like attributes
75
43
  def build_id_constraint_between(table, foreign_table)
76
- klass_attr = table[join_primary_key]
77
- source_attr = foreign_table[join_foreign_key]
78
-
79
- build_id_constraint(klass_attr, source_attr)
44
+ PredicateBuilder::ArelAttributeHandler.call(
45
+ table[join_primary_key],
46
+ foreign_table[join_foreign_key],
47
+ )
80
48
  end
81
49
  end
82
50
 
@@ -44,6 +44,10 @@ module Torque
44
44
  foreign_key
45
45
  end
46
46
 
47
+ def array_attribute
48
+ active_record.arel_table[foreign_key]
49
+ end
50
+
47
51
  private
48
52
 
49
53
  def derive_primary_key
@@ -7,6 +7,10 @@ module Torque
7
7
  def connected_through_array?
8
8
  options[:array]
9
9
  end
10
+
11
+ def array_attribute
12
+ klass.arel_table[foreign_key]
13
+ end
10
14
  end
11
15
 
12
16
  ::ActiveRecord::Reflection::HasManyReflection.include(HasManyReflection)
@@ -5,7 +5,7 @@ module Torque
5
5
  module Reflection
6
6
  module RuntimeReflection
7
7
  delegate :klass, :active_record, :connected_through_array?, :macro, :name,
8
- :build_id_constraint, to: :@reflection
8
+ :array_attribute, to: :@reflection
9
9
  end
10
10
 
11
11
  ::ActiveRecord::Reflection::RuntimeReflection.include(RuntimeReflection)
@@ -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)
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Torque
4
+ module PostgreSQL
5
+ module Relation
6
+ module Buckets
7
+
8
+ # :nodoc:
9
+ def buckets_value
10
+ @values.fetch(:buckets, nil)
11
+ end
12
+ # :nodoc:
13
+ def buckets_value=(value)
14
+ assert_modifiable!
15
+ @values[:buckets] = value
16
+ end
17
+
18
+ # Specifies how to bucket records. It works for both the calculations
19
+ # or just putting records into groups. For example:
20
+ #
21
+ # User.buckets(:created_at, [1.year.ago, 1.month.ago, 1.week.ago])
22
+ # # Returns all users grouped by created_at in the given time ranges
23
+ #
24
+ # User.buckets(:age, 0..100, step: 10).count
25
+ # # Counts all users grouped by age buckets of 10 years
26
+ def buckets(*value, **xargs)
27
+ spawn.buckets!(*value, **xargs)
28
+ end
29
+
30
+ # Like #buckets, but modifies relation in place.
31
+ def buckets!(attribute, values, count: nil, cast: nil, as: nil)
32
+ raise ArgumentError, <<~MSG.squish if !values.is_a?(Array) && !values.is_a?(Range)
33
+ Buckets must be an array or a range.
34
+ MSG
35
+
36
+ count ||= 1 if values.is_a?(Range)
37
+ attribute = arel_table[attribute] unless ::Arel.arel_node?(attribute)
38
+ self.buckets_value = [attribute, values, count, cast, as]
39
+ self
40
+ end
41
+
42
+ # When performing calculations with buckets, this method add a grouping
43
+ # clause to the query by the bucket values, and then adjust the keys
44
+ # to match provided values
45
+ def calculate(*)
46
+ return super if buckets_value.blank?
47
+
48
+ raise ArgumentError, <<~MSG.squish if group_values.present?
49
+ Cannot calculate with buckets when there are already group values.
50
+ MSG
51
+
52
+ keys = buckets_keys
53
+ self.group_values = [FN.group_by(build_buckets_node, :bucket)]
54
+ super.transform_keys { |key| keys[key - 1] }
55
+ end
56
+
57
+ module Initializer
58
+ # Hook into the output of records to make sure we group by the buckets
59
+ def records
60
+ return super if buckets_value.blank?
61
+
62
+ keys = buckets_keys
63
+ col = buckets_column
64
+ super.group_by do |record|
65
+ val = (record[col] || 0) - 1
66
+ keys[val] if val >= 0 && val < keys.size
67
+ end
68
+ end
69
+ end
70
+
71
+ private
72
+
73
+ # Hook arel build to add the column
74
+ def build_arel(*)
75
+ return super if buckets_value.blank? || select_values.present?
76
+
77
+ self.select_extra_values += [build_buckets_node.as(buckets_column)]
78
+ super
79
+ end
80
+
81
+ # Build the Arel node for the buckets function
82
+ def build_buckets_node
83
+ attribute, values, count, cast, * = buckets_value
84
+
85
+ if values.is_a?(Range)
86
+ FN.width_bucket(
87
+ attribute,
88
+ FN.bind_type(values.begin, name: 'bucket_start', cast: 'numeric'),
89
+ FN.bind_type(values.end, name: 'bucket_end', cast: 'numeric'),
90
+ FN.bind_type(count, name: 'bucket_count', cast: 'integer'),
91
+ )
92
+ else
93
+ FN.width_bucket(attribute, ::Arel.array(values, cast: cast))
94
+ end
95
+ end
96
+
97
+ # Returns the column used for buckets, if any
98
+ def buckets_column
99
+ buckets_value.last&.to_s || 'bucket'
100
+ end
101
+
102
+ # Transform a range into the proper keys for buckets
103
+ def buckets_keys
104
+ keys = buckets_value.second
105
+ return keys unless keys.is_a?(Range)
106
+
107
+ left = nil
108
+ step = buckets_value.third
109
+ step = (keys.end - keys.begin).fdiv(step)
110
+ step = step.to_i if step.to_i == step
111
+ keys.step(step).each_with_object([]) do |right, result|
112
+ next left = right if left.nil?
113
+
114
+ start, left = left, right
115
+ result << Range.new(start, left, true)
116
+ end
117
+ end
118
+
119
+ end
120
+
121
+ Initializer.include(Buckets::Initializer)
122
+ end
123
+ end
124
+ end
@@ -6,9 +6,14 @@ module Torque
6
6
  module DistinctOn
7
7
 
8
8
  # :nodoc:
9
- def distinct_on_values; get_value(:distinct_on); end
9
+ def distinct_on_values
10
+ @values.fetch(:distinct_on, FROZEN_EMPTY_ARRAY)
11
+ end
10
12
  # :nodoc:
11
- def distinct_on_values=(value); set_value(:distinct_on, value); end
13
+ def distinct_on_values=(value)
14
+ assert_modifiable!
15
+ @values[:distinct_on] = value
16
+ end
12
17
 
13
18
  # Specifies whether the records should be unique or not by a given set
14
19
  # of fields. For example:
@@ -5,17 +5,25 @@ module Torque
5
5
  module Relation
6
6
  module Inheritance
7
7
 
8
- # REGCLASS = ::Arel.sql('tableoid').cast('regclass')
9
-
10
8
  # :nodoc:
11
- def cast_records_value; get_value(:cast_records); end
9
+ def cast_records_values
10
+ @values.fetch(:cast_records, FROZEN_EMPTY_ARRAY)
11
+ end
12
12
  # :nodoc:
13
- def cast_records_value=(value); set_value(:cast_records, value); end
13
+ def cast_records_values=(value)
14
+ assert_modifiable!
15
+ @values[:cast_records] = value
16
+ end
14
17
 
15
18
  # :nodoc:
16
- def itself_only_value; get_value(:itself_only); end
19
+ def itself_only_value
20
+ @values.fetch(:itself_only, nil)
21
+ end
17
22
  # :nodoc:
18
- def itself_only_value=(value); set_value(:itself_only, value); end
23
+ def itself_only_value=(value)
24
+ assert_modifiable!
25
+ @values[:itself_only] = value
26
+ end
19
27
 
20
28
  delegate :quote_table_name, :quote_column_name, to: :connection
21
29
 
@@ -46,9 +54,9 @@ module Torque
46
54
 
47
55
  # Like #cast_records, but modifies relation in place
48
56
  def cast_records!(*types, **options)
49
- where!(regclass.cast(:varchar).in(types.map(&:table_name))) if options[:filter]
57
+ where!(regclass.pg_cast(:varchar).in(types.map(&:table_name))) if options[:filter]
50
58
  self.select_extra_values += [regclass.as(_record_class_attribute.to_s)]
51
- self.cast_records_value = (types.present? ? types : model.casted_dependents.values)
59
+ self.cast_records_values = (types.present? ? types : model.casted_dependents.values)
52
60
  self
53
61
  end
54
62
 
@@ -64,23 +72,22 @@ module Torque
64
72
 
65
73
  # Build all necessary data for inheritances
66
74
  def build_inheritances(arel)
67
- return unless self.cast_records_value.present?
75
+ return if self.cast_records_values.empty?
68
76
 
69
77
  mergeable = inheritance_mergeable_attributes
70
78
 
71
- columns = build_inheritances_joins(arel, self.cast_records_value)
79
+ columns = build_inheritances_joins(arel, self.cast_records_values)
72
80
  columns = columns.map do |column, arel_tables|
73
81
  next arel_tables.first[column] if arel_tables.size == 1
74
82
 
75
83
  if mergeable.include?(column)
76
- list = arel_tables.each_with_object(column).map(&:[])
77
- ::Arel::Nodes::NamedFunction.new('COALESCE', list).as(column)
84
+ FN.coalesce(*arel_tables.each_with_object(column).map(&:[])).as(column)
78
85
  else
79
86
  arel_tables.map { |table| table[column].as("#{table.left.name}__#{column}") }
80
87
  end
81
88
  end
82
89
 
83
- columns.push(build_auto_caster_marker(arel, self.cast_records_value))
90
+ columns.push(build_auto_caster_marker(arel, self.cast_records_values))
84
91
  self.select_extra_values += columns.flatten if columns.any?
85
92
  end
86
93
 
@@ -105,12 +112,12 @@ module Torque
105
112
  end
106
113
 
107
114
  def build_auto_caster_marker(arel, types)
108
- attribute = regclass.cast(:varchar).in(types.map(&:table_name))
115
+ attribute = regclass.pg_cast(:varchar).in(types.map(&:table_name))
109
116
  attribute.as(self.class._auto_cast_attribute.to_s)
110
117
  end
111
118
 
112
119
  def regclass
113
- arel_table['tableoid'].cast(:regclass)
120
+ arel_table['tableoid'].pg_cast(:regclass)
114
121
  end
115
122
 
116
123
  end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Torque
4
+ module PostgreSQL
5
+ module Relation
6
+ module JoinSeries
7
+
8
+ # Create the proper arel join
9
+ class << self
10
+ def build(relation, range, with: nil, as: :series, step: nil, time_zone: nil, mode: :inner, &block)
11
+ validate_build!(range, step)
12
+
13
+ args = [bind_value(range.begin), bind_value(range.end)]
14
+ args << bind_value(step) if step
15
+ args << bind_value(time_zone) if time_zone
16
+
17
+ result = Arel::Nodes::Ref.new(as.to_s)
18
+ func = FN.generate_series(*args).as(as.to_s)
19
+ condition = build_join_on(result, relation, with, &block)
20
+ arel_join(mode).new(func, func.create_on(condition))
21
+ end
22
+
23
+ private
24
+
25
+ # Make sure we have a viable range
26
+ def validate_build!(range, step)
27
+ raise ArgumentError, <<~MSG.squish unless range.is_a?(Range)
28
+ Value must be a Range.
29
+ MSG
30
+
31
+ raise ArgumentError, <<~MSG.squish if range.begin.nil?
32
+ Beginless Ranges are not supported.
33
+ MSG
34
+
35
+ raise ArgumentError, <<~MSG.squish if range.end.nil?
36
+ Endless Ranges are not supported.
37
+ MSG
38
+
39
+ raise ArgumentError, <<~MSG.squish if !range.begin.is_a?(Numeric) && step.nil?
40
+ missing keyword: :step
41
+ MSG
42
+ end
43
+
44
+ # Creates the proper bind value
45
+ def bind_value(value)
46
+ case value
47
+ when Integer
48
+ FN.bind_type(value, :integer, name: 'series', cast: 'integer')
49
+ when Float
50
+ FN.bind_type(value, :float, name: 'series', cast: 'numeric')
51
+ when String
52
+ FN.bind_type(value, :string, name: 'series', cast: 'text')
53
+ when ActiveSupport::TimeWithZone
54
+ FN.bind_type(value, :time, name: 'series', cast: 'timestamptz')
55
+ when Time
56
+ FN.bind_type(value, :time, name: 'series', cast: 'timestamp')
57
+ when DateTime
58
+ FN.bind_type(value, :datetime, name: 'series', cast: 'timestamp')
59
+ when ActiveSupport::Duration
60
+ type = Adapter::OID::Interval.new
61
+ FN.bind_type(value, type, name: 'series', cast: 'interval')
62
+ when Date then bind_value(value.to_time(:utc))
63
+ when ::Arel::Attributes::Attribute then value
64
+ else
65
+ raise ArgumentError, "Unsupported value type: #{value.class}"
66
+ end
67
+ end
68
+
69
+ # Get the class of the join on arel
70
+ def arel_join(mode)
71
+ case mode.to_sym
72
+ when :inner then ::Arel::Nodes::InnerJoin
73
+ when :left then ::Arel::Nodes::OuterJoin
74
+ when :right then ::Arel::Nodes::RightOuterJoin
75
+ when :full then ::Arel::Nodes::FullOuterJoin
76
+ else
77
+ raise ArgumentError, <<-MSG.squish
78
+ The '#{mode}' is not implemented as a join type.
79
+ MSG
80
+ end
81
+ end
82
+
83
+ # Build the join on clause
84
+ def build_join_on(result, relation, with)
85
+ raise ArgumentError, <<~MSG.squish if with.nil? && !block_given?
86
+ missing keyword: :with
87
+ MSG
88
+
89
+ return yield(result, relation.arel_table) if block_given?
90
+
91
+ result.eq(with.is_a?(Symbol) ? relation.arel_table[with.to_s] : with)
92
+ end
93
+ end
94
+
95
+ # Creates a new join based on PG +generate_series()+ function. It is
96
+ # based on ranges, supports numbers and dates (as per PG documentation),
97
+ # custom stepping, time zones, and more. This simply coordinates the
98
+ # initialization of the the proper join
99
+ def join_series(range, **xargs, &block)
100
+ spawn.join_series!(range, **xargs, &block)
101
+ end
102
+
103
+ # Like #join_series, but modifies relation in place.
104
+ def join_series!(range, **xargs, &block)
105
+ self.joins_values |= [JoinSeries.build(self, range, **xargs, &block)]
106
+ self
107
+ end
108
+
109
+ end
110
+ end
111
+ end
112
+ end