torque-postgresql 1.0.1 → 1.1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 1929f5d804c0491cc94149dde4af2c7ef7e57434
4
- data.tar.gz: f76c493623230594387b9b007574e4c79b778c6f
3
+ metadata.gz: a161d5be06adc9e93a5888b4b16b1a07b6f4a613
4
+ data.tar.gz: 51c1aae61c09bfa681e02daeb523551da9ef1139
5
5
  SHA512:
6
- metadata.gz: 04a0d8aac7f5e29c80f2ab609b7e703524a6a04737291f68dcad0f47fc2967613771056342deb2bb4f94a731961731c94f7c8260b1041fb18e41feed47e10c7f
7
- data.tar.gz: 964b8e4bed0f9a9b60ff83bd935dc41f6fd063592044d728e4aae4e7d56c6f28435e14961ce71d3b320a1b646bfbcc5181a820fac0777922d7189ad43418ae0b
6
+ metadata.gz: 99892a8ca40bf830e81accb0d4f15d87fbcb44697b95b4f9854a81b2e5d733c80c1a23f7312009407741ea11e7a6cb4d29acae8b03284226d618fadfbaa6a9da
7
+ data.tar.gz: 53e5888be0f1c89d2e30c85acc2ad350e34ad5efe6b6d899315a4cd18affe9366978d5cfd7e4ddad7843c0856ca11f7243f106d5d4ad2f538a86e2c11d5551ce
@@ -4,7 +4,7 @@ module Torque
4
4
  module OID
5
5
  class Enum < ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Enum
6
6
 
7
- attr_reader :name, :klass
7
+ attr_reader :name, :klass, :set_klass, :enum_klass
8
8
 
9
9
  def self.create(row, type_map)
10
10
  name = row['typname']
@@ -13,6 +13,7 @@ module Torque
13
13
 
14
14
  oid_klass = Enum.new(name)
15
15
  oid_set_klass = EnumSet.new(name, oid_klass.klass)
16
+ oid_klass.instance_variable_set(:@set_klass, oid_set_klass)
16
17
 
17
18
  type_map.register_type(oid, oid_klass)
18
19
  type_map.register_type(arr_oid, oid_set_klass)
@@ -21,6 +22,8 @@ module Torque
21
22
  def initialize(name)
22
23
  @name = name
23
24
  @klass = Attributes::Enum.lookup(name)
25
+
26
+ @enum_klass = self
24
27
  end
25
28
 
26
29
  def hash
@@ -42,6 +45,12 @@ module Torque
42
45
  cast_value(value).to_sym.inspect
43
46
  end
44
47
 
48
+ def ==(other)
49
+ self.class == other.class &&
50
+ other.klass == klass &&
51
+ other.type == type
52
+ end
53
+
45
54
  private
46
55
 
47
56
  def cast_value(value)
@@ -3,12 +3,11 @@ module Torque
3
3
  module Adapter
4
4
  module OID
5
5
  class EnumSet < Enum
6
-
7
- attr_reader :enum_klass
8
-
9
6
  def initialize(name, enum_klass)
10
7
  @name = name + '[]'
11
8
  @klass = Attributes::EnumSet.lookup(name, enum_klass)
9
+
10
+ @set_klass = self
12
11
  @enum_klass = enum_klass
13
12
  end
14
13
 
@@ -16,10 +15,18 @@ module Torque
16
15
  :enum_set
17
16
  end
18
17
 
18
+ def deserialize(value)
19
+ return unless value.present?
20
+ value = value[1..-2].split(',') if value.is_a?(String)
21
+ cast_value(value)
22
+ end
23
+
19
24
  def serialize(value)
20
25
  return if value.blank?
21
26
  value = cast_value(value)
22
- value.map(&:to_s) unless value.blank?
27
+
28
+ return if value.blank?
29
+ "{#{value.map(&:to_s).join(',')}}"
23
30
  end
24
31
 
25
32
  # Always use symbol values for schema dumper
@@ -33,7 +40,7 @@ module Torque
33
40
  return if value.blank?
34
41
  return value if value.is_a?(@klass)
35
42
  @klass.new(value)
36
- rescue Attributes::EnumSet::EnumError
43
+ rescue Attributes::EnumSet::EnumSetError
37
44
  nil
38
45
  end
39
46
 
@@ -25,6 +25,13 @@ module Torque
25
25
  end
26
26
  end
27
27
 
28
+ def map(value) # :nodoc:
29
+ return value unless value.respond_to?(:first)
30
+ from = yield(value.first)
31
+ to = yield(value.last)
32
+ cast_custom(from, to)
33
+ end
34
+
28
35
  private
29
36
 
30
37
  def cast_custom(from, to)
@@ -18,7 +18,7 @@ module Torque
18
18
  end
19
19
 
20
20
  def quote_default_expression(value, column)
21
- if value.is_a?(::Enumerable)
21
+ if value.class <= Array
22
22
  quote(value) + '::' + column.sql_type
23
23
  else
24
24
  super
@@ -9,7 +9,7 @@ module Torque
9
9
  def drop_type(name, options = {})
10
10
  force = options.fetch(:force, '').upcase
11
11
  check = 'IF EXISTS' if options.fetch(:check, true)
12
- execute <<-SQL
12
+ execute <<-SQL.squish
13
13
  DROP TYPE #{check}
14
14
  #{quote_type_name(name, options[:schema])} #{force}
15
15
  SQL
@@ -17,7 +17,7 @@ module Torque
17
17
 
18
18
  # Renames a type.
19
19
  def rename_type(type_name, new_name)
20
- execute <<-SQL
20
+ execute <<-SQL.squish
21
21
  ALTER TYPE #{quote_type_name(type_name)}
22
22
  RENAME TO #{Quoting::Name.new(nil, new_name.to_s).quoted}
23
23
  SQL
@@ -32,7 +32,7 @@ module Torque
32
32
  # create_enum 'status', ['foo', 'bar'], force: true
33
33
  def create_enum(name, values, options = {})
34
34
  drop_type(name, options) if options[:force]
35
- execute <<-SQL
35
+ execute <<-SQL.squish
36
36
  CREATE TYPE #{quote_type_name(name, options[:schema])} AS ENUM
37
37
  (#{quote_enum_values(name, values, options).join(', ')})
38
38
  SQL
@@ -56,7 +56,7 @@ module Torque
56
56
  quote_enum_values(name, values, options).each do |value|
57
57
  reference = "BEFORE #{before}" unless before == false
58
58
  reference = "AFTER #{after}" unless after == false
59
- execute <<-SQL
59
+ execute <<-SQL.squish
60
60
  ALTER TYPE #{quote_type_name(name, options[:schema])}
61
61
  ADD VALUE #{value} #{reference}
62
62
  SQL
@@ -1,6 +1,4 @@
1
- require_relative 'attributes/type_map'
2
1
  require_relative 'attributes/lazy'
3
-
4
2
  require_relative 'attributes/builder'
5
3
 
6
4
  require_relative 'attributes/enum'
@@ -17,24 +15,6 @@ module Torque
17
15
  class_attribute :enum_save_on_bang, instance_accessor: true
18
16
  self.enum_save_on_bang = Torque::PostgreSQL.config.enum.save_on_bang
19
17
  end
20
-
21
- module ClassMethods
22
-
23
- private
24
-
25
- # If the attributes are not loaded,
26
- def method_missing(method_name, *args, &block)
27
- return super unless define_attribute_methods
28
- self.send(method_name, *args, &block)
29
- end
30
-
31
- # Use local type map to identify attribute decorator
32
- def define_attribute_method(attribute)
33
- type = attribute_types[attribute]
34
- super unless TypeMap.lookup(type, self, attribute)
35
- end
36
-
37
- end
38
18
  end
39
19
 
40
20
  ActiveRecord::Base.include Attributes
@@ -1,2 +1,30 @@
1
1
  require_relative 'builder/enum'
2
2
  require_relative 'builder/period'
3
+
4
+ module Torque
5
+ module PostgreSQL
6
+ module Attributes
7
+ module Builder
8
+ def self.include_on(klass, method_name, builder_klass, &block)
9
+ klass.define_singleton_method(method_name) do |*args, **options|
10
+ return unless connection.table_exists?(table_name)
11
+
12
+ args.each do |attribute|
13
+ begin
14
+ # Generate methods on self class
15
+ builder = builder_klass.new(self, attribute, options)
16
+ builder.conflicting?
17
+ builder.build
18
+
19
+ # Additional settings for the builder
20
+ instance_exec(builder, &block) if block.present?
21
+ rescue Interrupt
22
+ # Not able to build the attribute, maybe pending migrations
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -3,15 +3,18 @@ module Torque
3
3
  module Attributes
4
4
  module Builder
5
5
  class Enum
6
+ VALID_TYPES = %i[enum enum_set].freeze
7
+
6
8
  attr_accessor :klass, :attribute, :subtype, :options, :values, :enum_module
7
9
 
8
10
  # Start a new builder of methods for enum values on ActiveRecord::Base
9
- def initialize(klass, attribute, subtype, options)
11
+ def initialize(klass, attribute, options)
10
12
  @klass = klass
11
13
  @attribute = attribute.to_s
12
- @subtype = subtype
14
+ @subtype = klass.attribute_types[@attribute]
13
15
  @options = options
14
16
 
17
+ raise Interrupt unless subtype.respond_to?(:klass)
15
18
  @values = subtype.klass.values
16
19
 
17
20
  if @options[:only]
@@ -49,7 +52,7 @@ module Torque
49
52
  # Check if any of the methods that will be created get in conflict
50
53
  # with the base class methods
51
54
  def conflicting?
52
- return false if options[:force] == true
55
+ return if options[:force] == true
53
56
  attributes = attribute.pluralize
54
57
 
55
58
  dangerous?(attributes, true)
@@ -60,11 +63,9 @@ module Torque
60
63
  values_methods.each do |attr, list|
61
64
  list.map(&method(:dangerous?))
62
65
  end
63
-
64
- return false
65
66
  rescue Interrupt => err
66
67
  raise ArgumentError, <<-MSG.squish
67
- #{subtype.class.name} was not able to generate requested
68
+ Enum #{subtype.name} was not able to generate requested
68
69
  methods because the method #{err} already exists in
69
70
  #{klass.name}.
70
71
  MSG
@@ -2,8 +2,10 @@ module Torque
2
2
  module PostgreSQL
3
3
  module Attributes
4
4
  module Builder
5
- # TODO: Allow methods to have nil in order to not include that specific method
5
+ # TODO: Allow documenting by building the methods outside and importing
6
+ # only the raw string
6
7
  class Period
8
+ DIRECT_ACCESS_REGEX = /_?%s_?/
7
9
  SUPPORTED_TYPES = %i[daterange tsrange tstzrange].freeze
8
10
  CURRENT_GETTERS = {
9
11
  daterange: 'Date.today',
@@ -17,58 +19,77 @@ module Torque
17
19
  tstzrange: :timestamp,
18
20
  }.freeze
19
21
 
20
- attr_accessor :klass, :attribute, :options, :type, :arel_attribute, :default,
21
- :current_getter, :type_caster, :default_sql, :threshold, :dynamic_threshold,
22
- :period_module
22
+ attr_accessor :klass, :attribute, :options, :type, :default, :current_getter,
23
+ :type_caster, :threshold, :dynamic_threshold, :klass_module, :instance_module
23
24
 
24
25
  # Start a new builder of methods for period values on
25
26
  # ActiveRecord::Base
26
- def initialize(klass, attribute, _, options)
27
+ def initialize(klass, attribute, options)
27
28
  @klass = klass
28
29
  @attribute = attribute.to_s
29
30
  @options = options
30
31
  @type = klass.attribute_types[@attribute].type
31
32
 
32
- @arel_attribute = klass.arel_table[@attribute]
33
+ raise ArgumentError, <<-MSG.squish unless SUPPORTED_TYPES.include?(type)
34
+ Period cannot be generated for #{attribute} because its type
35
+ #{type} is not supported. Only #{SUPPORTED_TYPES.join(', ')} are supported.
36
+ MSG
37
+
33
38
  @current_getter = CURRENT_GETTERS[type]
34
39
  @type_caster = TYPE_CASTERS[type]
35
40
 
36
- @default = options[:pessimistic].blank?
37
- @default_sql = ::Arel.sql(klass.connection.quote(@default))
41
+ @default = options[:pessimistic].blank?
42
+ end
38
43
 
39
- @threshold = options[:threshold].presence
44
+ # Check if can identify a threshold field
45
+ def threshold
46
+ @threshold ||= begin
47
+ option = options[:threshold]
48
+ return if option.eql?(false)
40
49
 
41
- raise ArgumentError, <<-MSG.squish unless SUPPORTED_TYPES.include?(type)
42
- Period cannot be generated for #{attribute} because its type
43
- #{type} is not supported. Only #{SUPPORTED_TYPES.join(', ')} are supported.
44
- MSG
50
+ unless option.eql?(true)
51
+ return option.is_a?(String) ? option.to_sym : option
52
+ end
53
+
54
+ attributes = klass.attribute_names
55
+ default_name = Torque::PostgreSQL.config.period.auto_threshold.to_s
56
+ raise ArgumentError, <<-MSG.squish unless attributes.include?(default_name)
57
+ Unable to find the #{default_name} to use as threshold for period
58
+ features for #{attribute} in #{klass.name} model.
59
+ MSG
60
+
61
+ check_type = klass.attribute_types[default_name].type
62
+ raise ArgumentError, <<-MSG.squish unless check_type.eql?(:interval)
63
+ The #{default_name} has the wrong type to be used as threshold.
64
+ Expected :interval got #{check_type.inspect} in #{klass.name} model.
65
+ MSG
66
+
67
+ default_name.to_sym
68
+ end
45
69
  end
46
70
 
47
71
  # Generate all the method names
48
72
  def method_names
49
- @method_names ||= options.fetch(:methods, {}).symbolize_keys
50
- .reverse_merge(default_method_names)
73
+ @method_names ||= default_method_names.merge(options.fetch(:methods, {}))
51
74
  end
52
75
 
53
76
  # Get the list of methods associated withe the class
54
77
  def klass_method_names
55
- @klass_method_names ||= method_names.to_a[0..20].to_h
78
+ @klass_method_names ||= method_names.to_a[0..22].to_h
56
79
  end
57
80
 
58
81
  # Get the list of methods associated withe the instances
59
82
  def instance_method_names
60
- @instance_method_names ||= method_names.to_a[21..27].to_h
83
+ @instance_method_names ||= method_names.to_a[23..29].to_h
61
84
  end
62
85
 
63
86
  # Check if any of the methods that will be created get in conflict
64
87
  # with the base class methods
65
88
  def conflicting?
66
- return false if options[:force] == true
89
+ return if options[:force] == true
67
90
 
68
91
  klass_method_names.values.each { |name| dangerous?(name, true) }
69
92
  instance_method_names.values.each { |name| dangerous?(name) }
70
-
71
- return false
72
93
  rescue Interrupt => err
73
94
  raise ArgumentError, <<-MSG.squish
74
95
  #{subtype.class.name} was not able to generate requested
@@ -79,99 +100,84 @@ module Torque
79
100
 
80
101
  # Create all methods needed
81
102
  def build
82
- @period_module = Module.new
83
-
84
- build_singleton_methods
85
- build_instance_methods
86
-
87
- klass.include period_module
88
- end
89
-
90
- # When the time has a threshold, then the real attribute is complex
91
- def real_arel_attribute
92
- return arel_attribute unless threshold.present?
93
-
94
- left = named_function(:lower, arel_attribute) - threshold_value
95
- right = named_function(:upper, arel_attribute) + threshold_value
96
-
97
- if type.eql?(:daterange)
98
- left = left.cast(:date)
99
- right = right.cast(:date)
103
+ @klass_module = Module.new
104
+ @instance_module = Module.new
105
+
106
+ value_args = ['value']
107
+ left_right_args = ['left', 'right = nil']
108
+
109
+ ## Klass methods
110
+ build_method_helper :klass, :current_on, value_args # 00
111
+ build_method_helper :klass, :current # 01
112
+ build_method_helper :klass, :not_current # 02
113
+ build_method_helper :klass, :containing, value_args # 03
114
+ build_method_helper :klass, :not_containing, value_args # 04
115
+ build_method_helper :klass, :overlapping, left_right_args # 05
116
+ build_method_helper :klass, :not_overlapping, left_right_args # 06
117
+ build_method_helper :klass, :starting_after, value_args # 07
118
+ build_method_helper :klass, :starting_before, value_args # 08
119
+ build_method_helper :klass, :finishing_after, value_args # 09
120
+ build_method_helper :klass, :finishing_before, value_args # 10
121
+
122
+ if threshold.present?
123
+ build_method_helper :klass, :real_containing, value_args # 11
124
+ build_method_helper :klass, :real_overlapping, left_right_args # 12
125
+ build_method_helper :klass, :real_starting_after, value_args # 13
126
+ build_method_helper :klass, :real_starting_before, value_args # 14
127
+ build_method_helper :klass, :real_finishing_after, value_args # 15
128
+ build_method_helper :klass, :real_finishing_before, value_args # 16
100
129
  end
101
130
 
102
- @real_arel_attribute ||= named_function(type, left, right)
103
- end
131
+ unless type.eql?(:daterange)
132
+ build_method_helper :klass, :containing_date, value_args # 17
133
+ build_method_helper :klass, :not_containing_date, value_args # 18
134
+ build_method_helper :klass, :overlapping_date, left_right_args # 19
135
+ build_method_helper :klass, :not_overlapping_date, left_right_args # 20
104
136
 
105
- # Create an arel named function
106
- def named_function(name, *args)
107
- ::Arel::Nodes::NamedFunction.new(name.to_s, args)
108
- end
109
-
110
- # Create an arel version of +nullif+ function
111
- def arel_nullif(*args)
112
- named_function('nullif', args)
113
- end
114
-
115
- # Create an arel version of +coalesce+ function
116
- def arel_coalesce(*args)
117
- named_function('coalesce', args)
118
- end
119
-
120
- # Create an arel version of the type with the following values
121
- def arel_convert_to_type(left, right = nil, set_type = nil)
122
- named_function(set_type || type, [left, right || left])
123
- end
137
+ if threshold.present?
138
+ build_method_helper :klass, :real_containing_date, value_args # 21
139
+ build_method_helper :klass, :real_overlapping_date, left_right_args # 22
140
+ end
141
+ end
124
142
 
125
- # Convert timestamp range to date range format
126
- def arel_daterange
127
- named_function(
128
- :daterange,
129
- named_function(:lower, real_arel_attribute).cast(:date),
130
- named_function(:upper, real_arel_attribute).cast(:date),
131
- )
132
- end
143
+ ## Instance methods
144
+ build_method_helper :instance, :current? # 23
145
+ build_method_helper :instance, :current_on?, value_args # 24
146
+ build_method_helper :instance, :start # 25
147
+ build_method_helper :instance, :finish # 26
133
148
 
134
- # Create an arel version of an empty value for the range
135
- def arel_empty_value
136
- arel_convert_to_type(::Arel.sql('NULL'))
137
- end
149
+ if threshold.present?
150
+ build_method_helper :instance, :real # 27
151
+ build_method_helper :instance, :real_start # 28
152
+ build_method_helper :instance, :real_finish # 29
153
+ end
138
154
 
139
- # Create an arel not condition
140
- def arel_not(value)
141
- named_function(:not, value)
155
+ klass.extend klass_module
156
+ klass.include instance_module
142
157
  end
143
158
 
144
- # Get the main arel condition to check the value
145
- def arel_check_condition(type, value)
146
- value = ::Arel.sql(klass.connection.quote(value))
159
+ def build_method_helper(type, key, args = [])
160
+ method_name = method_names[key]
161
+ return if method_name.nil?
147
162
 
148
- checker = arel_nullif(real_arel_attribute, arel_empty_value)
149
- checker = checker.public_send(type, value.cast(type_caster))
150
- arel_coalesce(checker, default_sql)
151
- end
163
+ method_content = send("#{type}_#{key}")
164
+ method_content = define_string_method(method_name, method_content, args)
152
165
 
153
- # Check how to provide the threshold value
154
- def threshold_value
155
- @threshold_value ||= begin
156
- case threshold
157
- when Symbol, String
158
- klass.arel_table[threshold]
159
- when ActiveSupport::Duration
160
- ::Arel.sql("'#{threshold.to_i} seconds'").cast(:interval)
161
- when Numeric
162
- value = threshold.to_i.to_s
163
- value << type_caster.eql?(:date) ? ' days' : ' seconds'
164
- ::Arel.sql("'#{value}'").cast(:interval)
165
- end
166
- end
166
+ source_module = send("#{type}_module")
167
+ source_module.class_eval(method_content)
167
168
  end
168
169
 
169
170
  private
170
171
 
171
172
  # Generates the default method names
172
173
  def default_method_names
173
- Torque::PostgreSQL.config.period.method_names.transform_values do |value|
174
- format(value, attribute)
174
+ list = Torque::PostgreSQL.config.period.method_names.dup
175
+
176
+ if options.fetch(:prefixed, true)
177
+ list.transform_values { |value| format(value, attribute) }
178
+ else
179
+ list = list.merge(Torque::PostgreSQL.config.period.direct_method_names)
180
+ list.transform_values { |value| value.gsub(DIRECT_ACCESS_REGEX, '') }
175
181
  end
176
182
  end
177
183
 
@@ -188,262 +194,316 @@ module Torque
188
194
  end
189
195
  end
190
196
 
191
- # Build model methods
192
- def build_singleton_methods
193
- attr = attribute
194
- builder = self
197
+ ## BUILDER HELPERS
198
+ def define_string_method(name, body, args = [])
199
+ headline = "def #{name}"
200
+ headline += "(#{args.join(', ')})"
201
+ [headline, body, 'end'].join("\n")
202
+ end
195
203
 
196
- # TODO: Rewrite these as string
197
- klass.scope method_names[:current_on], ->(value) do
198
- where(builder.arel_check_condition(:contains, value))
199
- end
204
+ def arel_attribute
205
+ @arel_attribute ||= "arel_attribute(#{attribute.inspect})"
206
+ end
200
207
 
201
- klass.scope method_names[:current], -> do
202
- public_send(builder.method_names[:current_on], eval(builder.current_getter))
203
- end
208
+ def arel_default_sql
209
+ @arel_default_sql ||= arel_sql_quote(@default.inspect)
210
+ end
204
211
 
205
- klass.scope method_names[:not_current], -> do
206
- current_value = eval(builder.current_getter)
207
- where.not(builder.arel_check_condition(:contains, current_value))
212
+ def arel_sql_quote(value)
213
+ "::Arel.sql(connection.quote(#{value}))"
214
+ end
215
+
216
+ # Check how to provide the threshold value
217
+ def arel_threshold_value
218
+ @arel_threshold_value ||= begin
219
+ case threshold
220
+ when Symbol, String
221
+ "arel_attribute('#{threshold}')"
222
+ when ActiveSupport::Duration
223
+ value = "'#{threshold.to_i} seconds'"
224
+ "::Arel.sql(\"#{value}\").cast(:interval)"
225
+ when Numeric
226
+ value = threshold.to_i.to_s
227
+ value << type_caster.eql?(:date) ? ' days' : ' seconds'
228
+ value = "'#{value}'"
229
+ "::Arel.sql(\"#{value}\").cast(:interval)"
230
+ end
208
231
  end
232
+ end
233
+
234
+ # Start at version of the value
235
+ def arel_start_at
236
+ @arel_start_at ||= arel_named_function('lower', arel_attribute)
237
+ end
238
+
239
+ # Finish at version of the value
240
+ def arel_finish_at
241
+ @arel_finish_at ||= arel_named_function('upper', arel_attribute)
242
+ end
209
243
 
210
- klass.scope method_names[:containing], ->(value) do
211
- value = arel_table[value] if value.is_a?(Symbol)
212
- value = ::Arel.sql(connection.quote(value)) unless value.respond_to?(:cast)
213
- where(builder.arel_attribute.contains(value))
244
+ # Start at version of the value with threshold
245
+ def arel_real_start_at
246
+ return arel_start_at unless threshold.present?
247
+ @arel_real_start_at ||= begin
248
+ result = "(#{arel_start_at} - #{arel_threshold_value})"
249
+ result << '.cast(:date)' if type.eql?(:daterange)
250
+ result
214
251
  end
252
+ end
215
253
 
216
- klass.scope method_names[:not_containing], ->(value) do
217
- value = arel_table[value] if value.is_a?(Symbol)
218
- value = ::Arel.sql(connection.quote(value)) unless value.respond_to?(:cast)
219
- where.not(builder.arel_attribute.contains(value))
254
+ # Finish at version of the value with threshold
255
+ def arel_real_finish_at
256
+ return arel_finish_at unless threshold.present?
257
+ @arel_real_finish_at ||= begin
258
+ result = "(#{arel_finish_at} + #{arel_threshold_value})"
259
+ result << '.cast(:date)' if type.eql?(:daterange)
260
+ result
220
261
  end
262
+ end
221
263
 
222
- klass.scope method_names[:overlapping], ->(value, right = nil) do
223
- value = arel_table[value] if value.is_a?(Symbol)
264
+ # When the time has a threshold, then the real attribute is complex
265
+ def arel_real_attribute
266
+ return arel_attribute unless threshold.present?
267
+ @arel_real_attribute ||= arel_named_function(
268
+ type, arel_real_start_at, arel_real_finish_at,
269
+ )
270
+ end
224
271
 
225
- if right.present?
226
- value = ::Arel.sql(connection.quote(value))
227
- right = ::Arel.sql(connection.quote(right))
228
- value = builder.arel_convert_to_type(value, right)
229
- elsif !value.respond_to?(:cast)
230
- value = ::Arel.sql(connection.quote(value))
231
- end
272
+ # Create an arel version of the type with the following values
273
+ def arel_convert_to_type(left, right = nil, set_type = nil)
274
+ arel_named_function(set_type || type, left, right || left)
275
+ end
232
276
 
233
- where(builder.arel_attribute.overlaps(value))
234
- end
277
+ # Create an arel named function
278
+ def arel_named_function(name, *args)
279
+ result = "::Arel::Nodes::NamedFunction.new(#{name.to_s.inspect}"
280
+ result << ', [' << args.join(', ') << ']' if args.present?
281
+ result << ')'
282
+ end
235
283
 
236
- klass.scope method_names[:not_overlapping], ->(value, right = nil) do
237
- value = arel_table[value] if value.is_a?(Symbol)
284
+ # Create an arel version of +nullif+ function
285
+ def arel_nullif(*args)
286
+ arel_named_function('nullif', *args)
287
+ end
238
288
 
239
- if right.present?
240
- value = ::Arel.sql(connection.quote(value))
241
- right = ::Arel.sql(connection.quote(right))
242
- value = builder.arel_convert_to_type(value, right)
243
- elsif !value.respond_to?(:cast)
244
- value = ::Arel.sql(connection.quote(value))
245
- end
289
+ # Create an arel version of +coalesce+ function
290
+ def arel_coalesce(*args)
291
+ arel_named_function('coalesce', *args)
292
+ end
246
293
 
247
- where.not(builder.arel_attribute.overlaps(value))
248
- end
294
+ # Create an arel version of an empty value for the range
295
+ def arel_empty_value
296
+ arel_convert_to_type('::Arel.sql(\'NULL\')')
297
+ end
249
298
 
250
- klass.scope method_names[:starting_after], ->(value) do
251
- value = arel_table[value] if value.is_a?(Symbol)
252
- value = ::Arel.sql(connection.quote(value)) \
253
- unless value.is_a?(::Arel::Attributes::Attribute)
299
+ # Convert timestamp range to date range format
300
+ def arel_daterange(real = false)
301
+ arel_named_function(
302
+ 'daterange',
303
+ (real ? arel_real_start_at : arel_start_at) + '.cast(:date)',
304
+ (real ? arel_real_finish_at : arel_finish_at) + '.cast(:date)',
305
+ '::Arel.sql("\'[]\'")',
306
+ )
307
+ end
254
308
 
255
- where(builder.named_function(:lower, builder.arel_attribute).gt(value))
256
- end
309
+ def arel_check_condition(type)
310
+ checker = arel_nullif(arel_real_attribute, arel_empty_value)
311
+ checker << ".#{type}(value.cast(#{type_caster.inspect}))"
312
+ arel_coalesce(checker, arel_default_sql)
313
+ end
257
314
 
258
- klass.scope method_names[:starting_before], ->(value) do
259
- value = arel_table[value] if value.is_a?(Symbol)
260
- value = ::Arel.sql(connection.quote(value)) \
261
- unless value.is_a?(::Arel::Attributes::Attribute)
315
+ def arel_formatting_value(condition = nil, value = 'value', cast: nil)
316
+ [
317
+ "#{value} = arel_table[#{value}] if #{value}.is_a?(Symbol)",
318
+ "unless #{value}.respond_to?(:cast)",
319
+ " #{value} = ::Arel.sql(connection.quote(#{value}))",
320
+ (" #{value} = #{value}.cast(#{cast.inspect})" if cast),
321
+ 'end',
322
+ condition,
323
+ ].compact.join("\n")
324
+ end
262
325
 
263
- where(builder.named_function(:lower, builder.arel_attribute).lt(value))
264
- end
326
+ def arel_formatting_left_right(condition, set_type = nil, cast: nil)
327
+ [
328
+ arel_formatting_value(nil, 'left', cast: cast),
329
+ '',
330
+ 'if right.present?',
331
+ ' ' + arel_formatting_value(nil, 'right', cast: cast),
332
+ " value = #{arel_convert_to_type('left', 'right', set_type)}",
333
+ 'else',
334
+ ' value = left',
335
+ 'end',
336
+ '',
337
+ condition,
338
+ ].join("\n")
339
+ end
265
340
 
266
- klass.scope method_names[:finishing_after], ->(value) do
267
- value = arel_table[value] if value.is_a?(Symbol)
268
- value = ::Arel.sql(connection.quote(value)) \
269
- unless value.is_a?(::Arel::Attributes::Attribute)
341
+ ## METHOD BUILDERS
342
+ def klass_current_on
343
+ arel_formatting_value("where(#{arel_check_condition(:contains)})")
344
+ end
270
345
 
271
- where(builder.named_function(:upper, builder.arel_attribute).gt(value))
272
- end
346
+ def klass_current
347
+ [
348
+ "value = #{arel_sql_quote(current_getter)}",
349
+ "where(#{arel_check_condition(:contains)})",
350
+ ].join("\n")
351
+ end
273
352
 
274
- klass.scope method_names[:finishing_before], ->(value) do
275
- value = arel_table[value] if value.is_a?(Symbol)
276
- value = ::Arel.sql(connection.quote(value)) \
277
- unless value.is_a?(::Arel::Attributes::Attribute)
353
+ def klass_not_current
354
+ [
355
+ "value = #{arel_sql_quote(current_getter)}",
356
+ "where.not(#{arel_check_condition(:contains)})",
357
+ ].join("\n")
358
+ end
278
359
 
279
- where(builder.named_function(:upper, builder.arel_attribute).lt(value))
280
- end
360
+ def klass_containing
361
+ arel_formatting_value("where(#{arel_attribute}.contains(value))")
362
+ end
281
363
 
282
- if threshold.present?
283
- klass.scope method_names[:real_containing], ->(value) do
284
- value = arel_table[value] if value.is_a?(Symbol)
285
- value = ::Arel.sql(connection.quote(value)) unless value.respond_to?(:cast)
286
- where(builder.real_arel_attribute.contains(value))
287
- end
364
+ def klass_not_containing
365
+ arel_formatting_value("where.not(#{arel_attribute}.contains(value))")
366
+ end
288
367
 
289
- klass.scope method_names[:real_overlapping], ->(value, right = nil) do
290
- value = arel_table[value] if value.is_a?(Symbol)
368
+ def klass_overlapping
369
+ arel_formatting_left_right("where(#{arel_attribute}.overlaps(value))")
370
+ end
291
371
 
292
- if right.present?
293
- value = ::Arel.sql(connection.quote(value))
294
- right = ::Arel.sql(connection.quote(right))
295
- value = builder.arel_convert_to_type(value, right)
296
- elsif !value.respond_to?(:cast)
297
- value = ::Arel.sql(connection.quote(value))
298
- end
372
+ def klass_not_overlapping
373
+ arel_formatting_left_right("where.not(#{arel_attribute}.overlaps(value))")
374
+ end
299
375
 
300
- where(builder.real_arel_attribute.overlaps(value))
301
- end
376
+ def klass_starting_after
377
+ arel_formatting_value("where((#{arel_start_at}).gt(value))")
378
+ end
302
379
 
303
- klass.scope method_names[:real_starting_after], ->(value) do
304
- value = arel_table[value] if value.is_a?(Symbol)
305
- condition = builder.named_function(:lower, builder.arel_attribute)
306
- condition -= builder.threshold_value
307
- condition = condition.cast(:date) if builder.type.eql?(:daterange)
308
- where(condition.gt(value))
309
- end
380
+ def klass_starting_before
381
+ arel_formatting_value("where((#{arel_start_at}).lt(value))")
382
+ end
310
383
 
311
- klass.scope method_names[:real_starting_before], ->(value) do
312
- value = arel_table[value] if value.is_a?(Symbol)
313
- condition = builder.named_function(:lower, builder.arel_attribute)
314
- condition -= builder.threshold_value
315
- condition = condition.cast(:date) if builder.type.eql?(:daterange)
316
- where(condition.lt(value))
317
- end
384
+ def klass_finishing_after
385
+ arel_formatting_value("where((#{arel_finish_at}).gt(value))")
386
+ end
318
387
 
319
- klass.scope method_names[:real_finishing_after], ->(value) do
320
- value = arel_table[value] if value.is_a?(Symbol)
321
- condition = builder.named_function(:upper, builder.arel_attribute)
322
- condition += builder.threshold_value
323
- condition = condition.cast(:date) if builder.type.eql?(:daterange)
324
- where(condition.gt(value))
325
- end
388
+ def klass_finishing_before
389
+ arel_formatting_value("where((#{arel_finish_at}).lt(value))")
390
+ end
326
391
 
327
- klass.scope method_names[:real_finishing_before], ->(value) do
328
- value = arel_table[value] if value.is_a?(Symbol)
329
- condition = builder.named_function(:upper, builder.arel_attribute)
330
- condition += builder.threshold_value
331
- condition = condition.cast(:date) if builder.type.eql?(:daterange)
332
- where(condition.lt(value))
333
- end
334
- end
392
+ def klass_real_containing
393
+ arel_formatting_value("where(#{arel_real_attribute}.contains(value))")
394
+ end
335
395
 
336
- unless type.eql?(:daterange)
337
- klass.scope method_names[:containing_date], ->(value) do
338
- value = arel_table[value] if value.is_a?(Symbol)
339
- value = ::Arel.sql(connection.quote(value)) unless value.respond_to?(:cast)
340
- where(builder.arel_daterange.contains(value))
341
- end
396
+ def klass_real_overlapping
397
+ arel_formatting_left_right("where(#{arel_real_attribute}.overlaps(value))")
398
+ end
342
399
 
343
- klass.scope method_names[:not_containing_date], ->(value) do
344
- value = arel_table[value] if value.is_a?(Symbol)
345
- value = ::Arel.sql(connection.quote(value)) unless value.respond_to?(:cast)
346
- where.not(builder.arel_daterange.contains(value))
347
- end
400
+ def klass_real_starting_after
401
+ arel_formatting_value("where(#{arel_real_start_at}.gt(value))")
402
+ end
348
403
 
349
- klass.scope method_names[:overlapping_date], ->(value, right = nil) do
350
- value = arel_table[value] if value.is_a?(Symbol)
404
+ def klass_real_starting_before
405
+ arel_formatting_value("where(#{arel_real_start_at}.lt(value))")
406
+ end
351
407
 
352
- if right.present?
353
- value = ::Arel.sql(connection.quote(value))
354
- right = ::Arel.sql(connection.quote(right))
355
- value = builder.arel_convert_to_type(value, right, :daterange)
356
- elsif !value.respond_to?(:cast)
357
- value = ::Arel.sql(connection.quote(value))
358
- end
408
+ def klass_real_finishing_after
409
+ arel_formatting_value("where(#{arel_real_finish_at}.gt(value))")
410
+ end
359
411
 
360
- where(builder.arel_daterange.overlaps(value))
361
- end
412
+ def klass_real_finishing_before
413
+ arel_formatting_value("where(#{arel_real_finish_at}.lt(value))")
414
+ end
362
415
 
363
- klass.scope method_names[:not_overlapping_date], ->(value, right = nil) do
364
- value = arel_table[value] if value.is_a?(Symbol)
416
+ def klass_containing_date
417
+ arel_formatting_value("where(#{arel_daterange}.contains(value))",
418
+ cast: :date)
419
+ end
365
420
 
366
- if right.present?
367
- value = ::Arel.sql(connection.quote(value))
368
- right = ::Arel.sql(connection.quote(right))
369
- value = builder.arel_convert_to_type(value, right, :daterange)
370
- elsif !value.respond_to?(:cast)
371
- value = ::Arel.sql(connection.quote(value))
372
- end
421
+ def klass_not_containing_date
422
+ arel_formatting_value("where.not(#{arel_daterange}.contains(value))",
423
+ cast: :date)
424
+ end
373
425
 
374
- where.not(builder.arel_daterange.overlaps(value))
375
- end
376
- end
426
+ def klass_overlapping_date
427
+ arel_formatting_left_right("where(#{arel_daterange}.overlaps(value))",
428
+ :daterange, cast: :date)
429
+ end
430
+
431
+ def klass_not_overlapping_date
432
+ arel_formatting_left_right("where.not(#{arel_daterange}.overlaps(value))",
433
+ :daterange, cast: :date)
377
434
  end
378
435
 
379
- # Build model instance methods
380
- def build_instance_methods
381
- attr = attribute
382
- builder = self
436
+ def klass_real_containing_date
437
+ arel_formatting_value("where(#{arel_daterange(true)}.contains(value))",
438
+ cast: :date)
439
+ end
383
440
 
384
- attr_threshold = threshold
385
- attr_threshold = attr_threshold.to_sym if attr_threshold.is_a?(String)
386
- attr_threshold = attr_threshold.seconds if attr_threshold.is_a?(Numeric)
441
+ def klass_real_overlapping_date
442
+ arel_formatting_left_right("where(#{arel_daterange(true)}.overlaps(value))",
443
+ :daterange, cast: :date)
444
+ end
387
445
 
388
- # TODO: Rewrite these as string
389
- period_module.module_eval do
390
- define_method(builder.method_names[:current?]) do
391
- public_send(builder.method_names[:current_on?], eval(builder.current_getter))
392
- end
446
+ def instance_current?
447
+ "#{method_names[:current_on?]}(#{current_getter})"
448
+ end
393
449
 
394
- define_method(builder.method_names[:current_on?]) do |value|
395
- attr_value = builder.threshold ? builder.method_names[:real] : attr
396
- attr_value = public_send(attr_value)
450
+ def instance_current_on?
451
+ attr_value = threshold.present? ? method_names[:real] : attribute
452
+ default_value = default.inspect
453
+ [
454
+ "return #{default_value} if #{attr_value}.nil?",
455
+ "return #{default_value} if #{attr_value}.min.try(:infinite?)",
456
+ "return #{default_value} if #{attr_value}.max.try(:infinite?)",
457
+ "#{attr_value}.min < value && #{attr_value}.max > value",
458
+ ].join("\n")
459
+ end
397
460
 
398
- return builder.default if attr_value.nil? ||
399
- (attr_value.min.try(:infinite?) && attr_value.max.try(:infinite?))
461
+ def instance_start
462
+ "#{attribute}&.min"
463
+ end
400
464
 
401
- attr_value.min < value && attr_value.max > value
402
- end
465
+ def instance_finish
466
+ "#{attribute}&.max"
467
+ end
403
468
 
404
- define_method(builder.method_names[:start]) do
405
- public_send(attr)&.min
406
- end
469
+ def instance_real
470
+ left = method_names[:real_start]
471
+ right = method_names[:real_finish]
407
472
 
408
- define_method(builder.method_names[:finish]) do
409
- public_send(attr)&.max
410
- end
473
+ [
474
+ "left = #{left}",
475
+ "right = #{right}",
476
+ 'return unless left || right',
477
+ '((left || -::Float::INFINITY)..(right || ::Float::INFINITY))',
478
+ ].join("\n")
479
+ end
411
480
 
412
- if attr_threshold.present?
413
- define_method(builder.method_names[:start]) do
414
- public_send(attr)&.min
415
- end
416
-
417
- define_method(builder.method_names[:finish]) do
418
- public_send(attr)&.max
419
- end
420
-
421
- define_method(builder.method_names[:real]) do
422
- left = public_send(builder.method_names[:real_start])
423
- right = public_send(builder.method_names[:real_finish])
424
- return unless left || right
425
-
426
- left ||= -::Float::INFINITY
427
- right ||= ::Float::INFINITY
428
-
429
- (left..right)
430
- end
431
-
432
- define_method(builder.method_names[:real_start]) do
433
- threshold = attr_threshold
434
- threshold = public_send(threshold) if threshold.is_a?(Symbol)
435
- result = public_send(attr)&.min.try(:-, threshold)
436
- builder.type.eql?(:daterange) ? result&.to_date : result
437
- end
438
-
439
- define_method(builder.method_names[:real_finish]) do
440
- threshold = attr_threshold
441
- threshold = public_send(threshold) if threshold.is_a?(Symbol)
442
- result = public_send(attr)&.max.try(:+, threshold)
443
- builder.type.eql?(:daterange) ? result&.to_date : result
444
- end
445
- end
446
- end
481
+ def instance_real_start
482
+ suffix = type.eql?(:daterange) ? '.to_date' : ''
483
+ threshold_value = threshold.is_a?(Symbol) \
484
+ ? threshold.to_s \
485
+ : threshold.to_i.to_s + '.seconds'
486
+
487
+ [
488
+ "return if #{method_names[:start]}.nil?",
489
+ "value = #{method_names[:start]}",
490
+ "value -= (#{threshold_value} || 0)",
491
+ "value#{suffix}"
492
+ ].join("\n")
493
+ end
494
+
495
+ def instance_real_finish
496
+ suffix = type.eql?(:daterange) ? '.to_date' : ''
497
+ threshold_value = threshold.is_a?(Symbol) \
498
+ ? threshold.to_s \
499
+ : threshold.to_i.to_s + '.seconds'
500
+
501
+ [
502
+ "return if #{method_names[:finish]}.nil?",
503
+ "value = #{method_names[:finish]}",
504
+ "value += (#{threshold_value} || 0)",
505
+ "value#{suffix}"
506
+ ].join("\n")
447
507
  end
448
508
  end
449
509
  end