torque-postgresql 1.0.1 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
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