arel_extensions 2.0.24 → 2.1.2

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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +358 -71
  3. data/Gemfile +8 -8
  4. data/README.md +107 -0
  5. data/gemfiles/rails5_2.gemfile +8 -7
  6. data/gemfiles/rails6.gemfile +7 -8
  7. data/gemfiles/rails6_1.gemfile +6 -7
  8. data/gemfiles/rails7.gemfile +22 -0
  9. data/lib/arel_extensions/aliases.rb +14 -0
  10. data/lib/arel_extensions/attributes.rb +2 -0
  11. data/lib/arel_extensions/date_duration.rb +2 -2
  12. data/lib/arel_extensions/helpers.rb +48 -0
  13. data/lib/arel_extensions/math.rb +17 -27
  14. data/lib/arel_extensions/nodes/case.rb +5 -10
  15. data/lib/arel_extensions/nodes/date_diff.rb +23 -4
  16. data/lib/arel_extensions/nodes/format.rb +3 -2
  17. data/lib/arel_extensions/nodes/function.rb +1 -7
  18. data/lib/arel_extensions/version.rb +1 -1
  19. data/lib/arel_extensions/visitors/mssql.rb +123 -51
  20. data/lib/arel_extensions/visitors/mysql.rb +23 -2
  21. data/lib/arel_extensions/visitors/oracle.rb +13 -1
  22. data/lib/arel_extensions/visitors/postgresql.rb +23 -5
  23. data/lib/arel_extensions/visitors/sqlite.rb +6 -3
  24. data/lib/arel_extensions/visitors/to_sql.rb +13 -8
  25. data/lib/arel_extensions.rb +27 -7
  26. data/test/arelx_test_helper.rb +45 -1
  27. data/test/database.yml +8 -2
  28. data/test/real_db_test.rb +5 -1
  29. data/test/support/fake_record.rb +1 -1
  30. data/test/visitors/test_to_sql.rb +38 -10
  31. data/test/with_ar/all_agnostic_test.rb +83 -5
  32. data/test/with_ar/insert_agnostic_test.rb +6 -2
  33. data/test/with_ar/test_bulk_sqlite.rb +6 -2
  34. data/test/with_ar/test_math_sqlite.rb +6 -2
  35. data/test/with_ar/test_string_mysql.rb +6 -2
  36. data/test/with_ar/test_string_sqlite.rb +6 -2
  37. data/version_v1.rb +1 -1
  38. data/version_v2.rb +1 -1
  39. metadata +6 -4
  40. data/appveyor.yml +0 -44
@@ -2,27 +2,37 @@ module ArelExtensions
2
2
  module Visitors
3
3
  module MSSQL
4
4
 
5
- Arel::Visitors::MSSQL::DATE_MAPPING = {
5
+ mssql_class = Arel::Visitors.constants.select { |c|
6
+ Arel::Visitors.const_get(c).is_a?(Class) && %i[MSSQL SQLServer].include?(c)
7
+ }.first
8
+
9
+ LOADED_VISITOR = Arel::Visitors.const_get(mssql_class) || Arel::Visitors.const_get('MSSQL')
10
+
11
+ LOADED_VISITOR::DATE_MAPPING = {
6
12
  'd' => 'day', 'm' => 'month', 'y' => 'year', 'wd' => 'weekday', 'w' => 'week', 'h' => 'hour', 'mn' => 'minute', 's' => 'second'
7
13
  }.freeze
8
14
 
9
- Arel::Visitors::MSSQL::DATE_FORMAT_DIRECTIVES = {
15
+ LOADED_VISITOR::DATE_FORMAT_DIRECTIVES = {
10
16
  '%Y' => 'YYYY', '%C' => '', '%y' => 'YY', '%m' => 'MM', '%B' => '', '%b' => '', '%^b' => '', # year, month
11
17
  '%d' => 'DD', '%e' => '', '%j' => '', '%w' => 'dw', '%A' => '', # day, weekday
12
18
  '%H' => 'hh', '%k' => '', '%I' => '', '%l' => '', '%P' => '', '%p' => '', # hours
13
19
  '%M' => 'mi', '%S' => 'ss', '%L' => 'ms', '%N' => 'ns', '%z' => 'tz'
14
20
  }.freeze
15
21
 
16
- Arel::Visitors::MSSQL::DATE_FORMAT_REGEX =
22
+ LOADED_VISITOR::DATE_FORMAT_FORMAT = {
23
+ 'YY' => '0#', 'MM' => '0#', 'DD' => '0#', 'hh' => '0#', 'mi' => '0#', 'ss' => '0#'
24
+ }
25
+
26
+ LOADED_VISITOR::DATE_FORMAT_REGEX =
17
27
  Regexp.new(
18
- Arel::Visitors::MSSQL::DATE_FORMAT_DIRECTIVES
28
+ LOADED_VISITOR::DATE_FORMAT_DIRECTIVES
19
29
  .keys
20
30
  .map{|k| Regexp.escape(k)}
21
31
  .join('|')
22
32
  ).freeze
23
33
 
24
34
  # TODO; all others... http://www.sql-server-helper.com/tips/date-formats.aspx
25
- Arel::Visitors::MSSQL::DATE_CONVERT_FORMATS = {
35
+ LOADED_VISITOR::DATE_CONVERT_FORMATS = {
26
36
  'YYYY-MM-DD' => 120,
27
37
  'YY-MM-DD' => 120,
28
38
  'MM/DD/YYYY' => 101,
@@ -79,7 +89,7 @@ module ArelExtensions
79
89
  def visit_ArelExtensions_Nodes_Concat o, collector
80
90
  collector << "CONCAT("
81
91
  o.expressions.each_with_index { |arg, i|
82
- collector << Arel::Visitors::MSSQL::COMMA if i != 0
92
+ collector << LOADED_VISITOR::COMMA if i != 0
83
93
  collector = visit arg, collector
84
94
  }
85
95
  collector << ")"
@@ -102,23 +112,23 @@ module ArelExtensions
102
112
  case o.right_node_type
103
113
  when :ruby_date, :ruby_time, :date, :datetime, :time
104
114
  collector << case o.left_node_type
105
- when :ruby_time, :datetime, :time then 'DATEDIFF(second'
106
- else 'DATEDIFF(day'
107
- end
108
- collector << Arel::Visitors::MSSQL::COMMA
115
+ when :ruby_time, :datetime, :time then 'DATEDIFF(second'
116
+ else 'DATEDIFF(day'
117
+ end
118
+ collector << LOADED_VISITOR::COMMA
109
119
  collector = visit o.right, collector
110
- collector << Arel::Visitors::MSSQL::COMMA
120
+ collector << LOADED_VISITOR::COMMA
111
121
  collector = visit o.left, collector
112
122
  collector << ')'
113
123
  else
114
124
  da = ArelExtensions::Nodes::DateAdd.new([])
115
125
  collector << "DATEADD("
116
126
  collector = visit da.mssql_datepart(o.right), collector
117
- collector << Arel::Visitors::MSSQL::COMMA
127
+ collector << LOADED_VISITOR::COMMA
118
128
  collector << "-("
119
129
  collector = visit da.mssql_value(o.right), collector
120
130
  collector << ")"
121
- collector << Arel::Visitors::MSSQL::COMMA
131
+ collector << LOADED_VISITOR::COMMA
122
132
  collector = visit o.left, collector
123
133
  collector << ")"
124
134
  collector
@@ -129,9 +139,9 @@ module ArelExtensions
129
139
  def visit_ArelExtensions_Nodes_DateAdd o, collector
130
140
  collector << "DATEADD("
131
141
  collector = visit o.mssql_datepart(o.right), collector
132
- collector << Arel::Visitors::MSSQL::COMMA
142
+ collector << LOADED_VISITOR::COMMA
133
143
  collector = visit o.mssql_value(o.right), collector
134
- collector << Arel::Visitors::MSSQL::COMMA
144
+ collector << LOADED_VISITOR::COMMA
135
145
  collector = visit o.left, collector
136
146
  collector << ")"
137
147
  collector
@@ -144,8 +154,8 @@ module ArelExtensions
144
154
  left = o.left.end_with?('i') ? o.left[0..-2] : o.left
145
155
  conv = ['h', 'mn', 's'].include?(o.left)
146
156
  collector << 'DATEPART('
147
- collector << Arel::Visitors::MSSQL::DATE_MAPPING[left]
148
- collector << Arel::Visitors::MSSQL::COMMA
157
+ collector << LOADED_VISITOR::DATE_MAPPING[left]
158
+ collector << LOADED_VISITOR::COMMA
149
159
  collector << 'CONVERT(datetime,' if conv
150
160
  collector = visit o.right, collector
151
161
  collector << ')' if conv
@@ -155,20 +165,29 @@ module ArelExtensions
155
165
  end
156
166
 
157
167
  def visit_ArelExtensions_Nodes_Length o, collector
158
- collector << "#{o.bytewise ? 'DATALENGTH' : 'LEN'}("
159
- collector = visit o.expr, collector
160
- collector << ")"
161
- collector
168
+ if o.bytewise
169
+ collector << "(DATALENGTH("
170
+ collector = visit o.expr, collector
171
+ collector << ") / ISNULL(NULLIF(DATALENGTH(LEFT(COALESCE("
172
+ collector = visit o.expr, collector
173
+ collector << ", '#' ), 1 )), 0), 1))"
174
+ collector
175
+ else
176
+ collector << "LEN("
177
+ collector = visit o.expr, collector
178
+ collector << ")"
179
+ collector
180
+ end
162
181
  end
163
182
 
164
183
  def visit_ArelExtensions_Nodes_Round o, collector
165
184
  collector << "ROUND("
166
185
  o.expressions.each_with_index { |arg, i|
167
- collector << Arel::Visitors::MSSQL::COMMA if i != 0
186
+ collector << LOADED_VISITOR::COMMA if i != 0
168
187
  collector = visit arg, collector
169
188
  }
170
189
  if o.expressions.length == 1
171
- collector << Arel::Visitors::MSSQL::COMMA
190
+ collector << LOADED_VISITOR::COMMA
172
191
  collector << "0"
173
192
  end
174
193
  collector << ")"
@@ -178,7 +197,7 @@ module ArelExtensions
178
197
  def visit_ArelExtensions_Nodes_Locate o, collector
179
198
  collector << "CHARINDEX("
180
199
  collector = visit o.right, collector
181
- collector << Arel::Visitors::MSSQL::COMMA
200
+ collector << LOADED_VISITOR::COMMA
182
201
  collector = visit o.left, collector
183
202
  collector << ")"
184
203
  collector
@@ -187,28 +206,20 @@ module ArelExtensions
187
206
  def visit_ArelExtensions_Nodes_Substring o, collector
188
207
  collector << 'SUBSTRING('
189
208
  collector = visit o.expressions[0], collector
190
- collector << Arel::Visitors::MSSQL::COMMA
209
+ collector << LOADED_VISITOR::COMMA
191
210
  collector = visit o.expressions[1], collector
192
- collector << Arel::Visitors::MSSQL::COMMA
211
+ collector << LOADED_VISITOR::COMMA
193
212
  collector = o.expressions[2] ? visit(o.expressions[2], collector) : visit(o.expressions[0].length, collector)
194
213
  collector << ')'
195
214
  collector
196
215
  end
197
216
 
198
217
  def visit_ArelExtensions_Nodes_Trim o, collector
199
- if o.right
200
- collector << "REPLACE(REPLACE(LTRIM(RTRIM(REPLACE(REPLACE("
201
- collector = visit o.left, collector
202
- collector << ", ' ', '~'), "
203
- collector = visit o.right, collector
204
- collector << ", ' '))), ' ', "
205
- collector = visit o.right, collector
206
- collector << "), '~', ' ')"
207
- else
208
- collector << "LTRIM(RTRIM("
209
- collector = visit o.left, collector
210
- collector << "))"
211
- end
218
+ collector << 'TRIM( '
219
+ collector = visit o.right, collector
220
+ collector << " FROM "
221
+ collector = visit o.left, collector
222
+ collector << ")"
212
223
  collector
213
224
  end
214
225
 
@@ -255,12 +266,28 @@ module ArelExtensions
255
266
  end
256
267
 
257
268
  def visit_ArelExtensions_Nodes_Format o, collector
258
- f = ArelExtensions::Visitors::strftime_to_format(o.iso_format, Arel::Visitors::MSSQL::DATE_FORMAT_DIRECTIVES)
259
- if fmt = Arel::Visitors::MSSQL::DATE_CONVERT_FORMATS[f]
269
+ f = ArelExtensions::Visitors::strftime_to_format(o.iso_format, LOADED_VISITOR::DATE_FORMAT_DIRECTIVES)
270
+ if fmt = LOADED_VISITOR::DATE_CONVERT_FORMATS[f]
260
271
  collector << "CONVERT(VARCHAR(#{f.length})"
261
- collector << Arel::Visitors::MSSQL::COMMA
272
+ collector << LOADED_VISITOR::COMMA
273
+ if o.time_zone
274
+ collector << 'CONVERT(datetime'
275
+ collector << LOADED_VISITOR::COMMA
276
+ collector << ' '
277
+ end
262
278
  collector = visit o.left, collector
263
- collector << Arel::Visitors::MSSQL::COMMA
279
+ case o.time_zone
280
+ when Hash
281
+ src_tz, dst_tz = o.time_zone.first
282
+ collector << ') AT TIME ZONE '
283
+ collector = visit Arel::Nodes.build_quoted(src_tz), collector
284
+ collector << ' AT TIME ZONE '
285
+ collector = visit Arel::Nodes.build_quoted(dst_tz), collector
286
+ when String
287
+ collector << ') AT TIME ZONE '
288
+ collector = visit Arel::Nodes.build_quoted(o.time_zone), collector
289
+ end
290
+ collector << LOADED_VISITOR::COMMA
264
291
  collector << fmt.to_s
265
292
  collector << ')'
266
293
  collector
@@ -272,13 +299,38 @@ module ArelExtensions
272
299
  collector << sep
273
300
  sep = ' + '
274
301
  case
275
- when s.scan(Arel::Visitors::MSSQL::DATE_FORMAT_REGEX)
276
- dir = Arel::Visitors::MSSQL::DATE_FORMAT_DIRECTIVES[s.matched]
277
- collector << 'LTRIM(STR(DATEPART('
302
+ when s.scan(LOADED_VISITOR::DATE_FORMAT_REGEX)
303
+ dir = LOADED_VISITOR::DATE_FORMAT_DIRECTIVES[s.matched]
304
+ fmt = LOADED_VISITOR::DATE_FORMAT_FORMAT[dir]
305
+ collector << 'TRIM('
306
+ collector << 'FORMAT(' if fmt
307
+ collector << 'STR(' if !fmt
308
+ collector << 'DATEPART('
278
309
  collector << dir
279
- collector << Arel::Visitors::MSSQL::COMMA
310
+ collector << LOADED_VISITOR::COMMA
311
+ if o.time_zone
312
+ collector << 'CONVERT(datetime'
313
+ collector << LOADED_VISITOR::COMMA
314
+ collector << ' '
315
+ end
280
316
  collector = visit o.left, collector
281
- collector << ')))'
317
+ case o.time_zone
318
+ when Hash
319
+ src_tz, dst_tz = o.time_zone.first.first, o.time_zone.first.second
320
+ collector << ") AT TIME ZONE "
321
+ collector = visit Arel::Nodes.build_quoted(src_tz), collector
322
+ collector << " AT TIME ZONE "
323
+ collector = visit Arel::Nodes.build_quoted(dst_tz), collector
324
+ when String
325
+ collector << ") AT TIME ZONE "
326
+ collector = visit Arel::Nodes.build_quoted(o.time_zone), collector
327
+ end
328
+ collector << ')'
329
+ collector << ')' if !fmt
330
+ collector << LOADED_VISITOR::COMMA << "'#{fmt}')" if fmt
331
+ collector << ')'
332
+ when s.scan(/^%%/)
333
+ collector = visit Arel::Nodes.build_quoted('%'), collector
282
334
  when s.scan(/[^%]+|./)
283
335
  collector = visit Arel::Nodes.build_quoted(s.matched), collector
284
336
  end
@@ -291,7 +343,7 @@ module ArelExtensions
291
343
  def visit_ArelExtensions_Nodes_Replace o, collector
292
344
  collector << "REPLACE("
293
345
  o.expressions.each_with_index { |arg, i|
294
- collector << Arel::Visitors::MSSQL::COMMA if i != 0
346
+ collector << LOADED_VISITOR::COMMA if i != 0
295
347
  collector = visit arg, collector
296
348
  }
297
349
  collector << ")"
@@ -301,7 +353,7 @@ module ArelExtensions
301
353
  def visit_ArelExtensions_Nodes_FindInSet o, collector
302
354
  collector << "dbo.FIND_IN_SET("
303
355
  o.expressions.each_with_index { |arg, i|
304
- collector << Arel::Visitors::MSSQL::COMMA if i != 0
356
+ collector << LOADED_VISITOR::COMMA if i != 0
305
357
  collector = visit arg, collector
306
358
  }
307
359
  collector << ")"
@@ -387,6 +439,27 @@ module ArelExtensions
387
439
  collector
388
440
  end
389
441
 
442
+ alias_method(:old_visit_Arel_Nodes_As, :visit_Arel_Nodes_As) rescue nil
443
+ def visit_Arel_Nodes_As o, collector
444
+ if o.left.is_a?(Arel::Nodes::Binary)
445
+ collector << '('
446
+ collector = visit o.left, collector
447
+ collector << ')'
448
+ else
449
+ collector = visit o.left, collector
450
+ end
451
+ collector << " AS "
452
+
453
+ # sometimes these values are already quoted, if they are, don't double quote it
454
+ quote = o.right.is_a?(Arel::Nodes::SqlLiteral) && o.right[0] != '"' && o.right[-1] != '"'
455
+
456
+ collector << '"' if quote
457
+ collector = visit o.right, collector
458
+ collector << '"' if quote
459
+
460
+ collector
461
+ end
462
+
390
463
  # SQL Server does not know about REGEXP
391
464
  def visit_Arel_Nodes_Regexp o, collector
392
465
  collector = visit o.left, collector
@@ -554,7 +627,6 @@ module ArelExtensions
554
627
  collector << ')'
555
628
  collector
556
629
  end
557
-
558
630
  end
559
631
  end
560
632
  end
@@ -209,7 +209,21 @@ module ArelExtensions
209
209
  when :date, :datetime, :time
210
210
  fmt = ArelExtensions::Visitors::strftime_to_format(o.iso_format, DATE_FORMAT_DIRECTIVES)
211
211
  collector << "DATE_FORMAT("
212
+ collector << "CONVERT_TZ(" if o.time_zone
212
213
  collector = visit o.left, collector
214
+ case o.time_zone
215
+ when Hash
216
+ src_tz, dst_tz = o.time_zone.first
217
+ collector << COMMA
218
+ collector = visit Arel::Nodes.build_quoted(src_tz), collector
219
+ collector << COMMA
220
+ collector = visit Arel::Nodes.build_quoted(dst_tz), collector
221
+ collector << ')'
222
+ when String
223
+ collector << COMMA << "'UTC'" << COMMA
224
+ collector = visit Arel::Nodes.build_quoted(o.time_zone), collector
225
+ collector << ')'
226
+ end
213
227
  collector << COMMA
214
228
  collector = visit Arel::Nodes.build_quoted(fmt), collector
215
229
  collector << ")"
@@ -354,9 +368,16 @@ module ArelExtensions
354
368
  else
355
369
  collector = visit o.left, collector
356
370
  end
357
- collector << " AS `"
371
+ collector << " AS "
372
+
373
+ # sometimes these values are already quoted, if they are, don't double quote it
374
+ quote = o.right.is_a?(Arel::Nodes::SqlLiteral) && o.right[0] != '`' && o.right[-1] != '`'
375
+
376
+ collector << '`' if quote
358
377
  collector = visit o.right, collector
359
- collector << "`"
378
+ collector << '`' if quote
379
+
380
+ collector
360
381
  collector
361
382
  end
362
383
 
@@ -447,7 +447,19 @@ module ArelExtensions
447
447
  def visit_ArelExtensions_Nodes_Format o, collector
448
448
  fmt = ArelExtensions::Visitors::strftime_to_format(o.iso_format, DATE_FORMAT_DIRECTIVES)
449
449
  collector << "TO_CHAR("
450
+ collector << "CAST(" if o.time_zone
450
451
  collector = visit o.left, collector
452
+ case o.time_zone
453
+ when Hash
454
+ src_tz, dst_tz = o.time_zone.first
455
+ collector << ' as timestamp) at time zone '
456
+ collector = visit Arel::Nodes.build_quoted(src_tz), collector
457
+ collecto < ' at time zone '
458
+ collector = visit Arel::Nodes.build_quoted(dst_tz), collector
459
+ when String
460
+ collector << ' as timestamp) at time zone '
461
+ collector = visit Arel::Nodes.build_quoted(o.time_zone), collector
462
+ end
451
463
  collector << COMMA
452
464
  collector = visit Arel::Nodes.build_quoted(fmt), collector
453
465
  collector << ")"
@@ -517,7 +529,7 @@ module ArelExtensions
517
529
  if element.is_a?(Time)
518
530
  ArelExtensions::Nodes::Format.new [element, '%H:%M:%S']
519
531
  elsif element.is_a?(Arel::Attributes::Attribute)
520
- col = Arel::Table.engine.connection.schema_cache.columns_hash(element.relation.table_name)[element.name.to_s]
532
+ col = ArelExtensions::column_of(element.relation.table_name, element.name.to_s)
521
533
  if col && (col.type == :time)
522
534
  ArelExtensions::Nodes::Format.new [element, '%H:%M:%S']
523
535
  else
@@ -86,9 +86,15 @@ module ArelExtensions
86
86
  else
87
87
  collector = visit o.left, collector
88
88
  end
89
- collector << " AS \""
89
+ collector << " AS "
90
+
91
+ # sometimes these values are already quoted, if they are, don't double quote it
92
+ quote = o.right.is_a?(Arel::Nodes::SqlLiteral) && o.right[0] != '"' && o.right[-1] != '"'
93
+
94
+ collector << '"' if quote
90
95
  collector = visit o.right, collector
91
- collector << "\""
96
+ collector << '"' if quote
97
+
92
98
  collector
93
99
  end
94
100
 
@@ -169,7 +175,19 @@ module ArelExtensions
169
175
  def visit_ArelExtensions_Nodes_Format o, collector
170
176
  fmt = ArelExtensions::Visitors::strftime_to_format(o.iso_format, DATE_FORMAT_DIRECTIVES)
171
177
  collector << "TO_CHAR("
178
+ collector << '(' if o.time_zone
172
179
  collector = visit o.left, collector
180
+ case o.time_zone
181
+ when Hash
182
+ src_tz, dst_tz = o.time_zone.first
183
+ collector << ') AT TIME ZONE '
184
+ collector = visit Arel::Nodes.build_quoted(src_tz), collector
185
+ collector << ' AT TIME ZONE '
186
+ collector = visit Arel::Nodes.build_quoted(dst_tz), collector
187
+ when String
188
+ collector << ') AT TIME ZONE '
189
+ collector = visit Arel::Nodes.build_quoted(o.time_zone), collector
190
+ end
173
191
  collector << COMMA
174
192
  collector = visit Arel::Nodes.build_quoted(fmt), collector
175
193
  collector << ")"
@@ -283,9 +301,9 @@ module ArelExtensions
283
301
  return collector
284
302
  end
285
303
  end
286
- collector << "EXTRACT(#{DATE_MAPPING[o.left]} FROM "
304
+ collector << "EXTRACT(#{DATE_MAPPING[o.left]} FROM CAST("
287
305
  collector = visit o.right, collector
288
- collector << ")"
306
+ collector << " AS TIMESTAMP WITH TIME ZONE))"
289
307
  collector << " * (INTERVAL '1' #{interval})" if interval && o.with_interval
290
308
  collector
291
309
  end
@@ -368,7 +386,7 @@ module ArelExtensions
368
386
  when :number, :decimal, :float
369
387
  Arel::Nodes::SqlLiteral.new('numeric')
370
388
  when :datetime
371
- Arel::Nodes::SqlLiteral.new('timestamp')
389
+ Arel::Nodes::SqlLiteral.new('timestamp with time zone')
372
390
  when :date
373
391
  Arel::Nodes::SqlLiteral.new('date')
374
392
  when :binary
@@ -1,3 +1,5 @@
1
+ require 'arel_extensions/helpers'
2
+
1
3
  module ArelExtensions
2
4
  module Visitors
3
5
  class Arel::Visitors::SQLite
@@ -327,7 +329,7 @@ module ArelExtensions
327
329
  if element.is_a?(Time)
328
330
  return Arel::Nodes::NamedFunction.new('STRFTIME',[element, '%H:%M:%S'])
329
331
  elsif element.is_a?(Arel::Attributes::Attribute)
330
- col = Arel::Table.engine.connection.schema_cache.columns_hash(element.relation.table_name)[element.name.to_s]
332
+ col = ArelExtensions::column_of(element.relation.table_name, element.name.to_s)
331
333
  if col && (col.type == :time)
332
334
  return Arel::Nodes::NamedFunction.new('STRFTIME',[element, '%H:%M:%S'])
333
335
  else
@@ -379,9 +381,10 @@ module ArelExtensions
379
381
  else
380
382
  collector = visit o.left, collector
381
383
  end
382
- collector << " AS \""
384
+ sep = o.right.size > 1 && o.right[0] == '"' && o.right[-1] == '"' ? '' : '"'
385
+ collector << " AS #{sep}"
383
386
  collector = visit o.right, collector
384
- collector << "\""
387
+ collector << "#{sep}"
385
388
  collector
386
389
  end
387
390
 
@@ -8,10 +8,15 @@ module ArelExtensions
8
8
  def make_json_string expr
9
9
  Arel::Nodes.build_quoted('"') \
10
10
  + expr
11
+ .coalesce('')
11
12
  .replace('\\','\\\\').replace('"','\"').replace("\n", '\n') \
12
13
  + '"'
13
14
  end
14
15
 
16
+ def make_json_null
17
+ Arel::Nodes.build_quoted("null")
18
+ end
19
+
15
20
  # Math Functions
16
21
  def visit_ArelExtensions_Nodes_Abs o, collector
17
22
  collector << "ABS("
@@ -601,20 +606,20 @@ module ArelExtensions
601
606
  def json_value(o,v)
602
607
  case o.type_of_node(v)
603
608
  when :string
604
- Arel.when(v.is_null).then(Arel.null).else(make_json_string(v))
609
+ Arel.when(v.is_null).then(make_json_null).else(make_json_string(v))
605
610
  when :date
606
611
  s = v.format('%Y-%m-%d')
607
- Arel.when(s.is_null).then(Arel.null).else(make_json_string(s))
612
+ Arel.when(s.is_null).then(make_json_null).else(make_json_string(s))
608
613
  when :datetime
609
614
  s = v.format('%Y-%m-%dT%H:%M:%S')
610
- Arel.when(s.is_null).then(Arel.null).else(make_json_string(s))
615
+ Arel.when(s.is_null).then(make_json_null).else(make_json_string(s))
611
616
  when :time
612
617
  s = v.format('%H:%M:%S')
613
- Arel.when(s.is_null).then(Arel.null).else(make_json_string(s))
618
+ Arel.when(s.is_null).then(make_json_null).else(make_json_string(s))
614
619
  when :nil
615
- Arel.null
620
+ make_json_null
616
621
  else
617
- ArelExtensions::Nodes::Cast.new([v, :string]).coalesce(Arel.null)
622
+ ArelExtensions::Nodes::Cast.new([v, :string]).coalesce(make_json_null)
618
623
  end
619
624
  end
620
625
 
@@ -636,7 +641,7 @@ module ArelExtensions
636
641
  if i != 0
637
642
  res += ', '
638
643
  end
639
- res += make_json_string(ArelExtensions::Nodes::Cast.new([k, :string]).coalesce("")) + ': '
644
+ res += make_json_string(ArelExtensions::Nodes::Cast.new([k, :string])) + ': '
640
645
  res += json_value(o,v)
641
646
  end
642
647
  res += '}'
@@ -658,7 +663,7 @@ module ArelExtensions
658
663
  if i != 0
659
664
  res = res + ', '
660
665
  end
661
- kv = make_json_string(ArelExtensions::Nodes::Cast.new([k, :string]).coalesce("")) + ': '
666
+ kv = make_json_string(ArelExtensions::Nodes::Cast.new([k, :string])) + ': '
662
667
  kv += json_value(o,v)
663
668
  res = res + kv.group_concat(', ', order: Array(orders)).coalesce('')
664
669
  end
@@ -52,6 +52,7 @@ if Gem::Version.new(Arel::VERSION) >= Gem::Version.new("7.1.0")
52
52
  end
53
53
 
54
54
  require 'arel_extensions/version'
55
+ require 'arel_extensions/aliases'
55
56
  require 'arel_extensions/attributes'
56
57
  require 'arel_extensions/visitors'
57
58
  require 'arel_extensions/nodes'
@@ -76,7 +77,17 @@ require 'arel_extensions/nodes/soundex'
76
77
  require 'arel_extensions/nodes/cast'
77
78
  require 'arel_extensions/nodes/json'
78
79
 
79
-
80
+ # It seems like the code in lib/arel_extensions/visitors.rb that is supposed
81
+ # to inject ArelExtension is not enough. Different versions of the sqlserver
82
+ # adapter behave differently. It doesn't always proc, so we added this for
83
+ # coverage.
84
+ if defined?(Arel::Visitors::SQLServer)
85
+ Arel::Visitors.const_set('MSSQL', Arel::Visitors::SQLServer)
86
+ require 'arel_extensions/visitors/mssql'
87
+ class Arel::Visitors::SQLServer
88
+ include ArelExtensions::Visitors::MSSQL
89
+ end
90
+ end
80
91
 
81
92
  module Arel
82
93
  def self.rand
@@ -136,6 +147,7 @@ class Arel::Attributes::Attribute
136
147
  end
137
148
 
138
149
  class Arel::Nodes::Function
150
+ include ArelExtensions::Aliases
139
151
  include ArelExtensions::Math
140
152
  include ArelExtensions::Comparators
141
153
  include ArelExtensions::DateDuration
@@ -153,11 +165,6 @@ class Arel::Nodes::Function
153
165
  end
154
166
  res
155
167
  end
156
-
157
- def xas other
158
- Arel::Nodes::As.new(self, Arel.sql(other))
159
- end
160
-
161
168
  end
162
169
 
163
170
  class Arel::Nodes::Grouping
@@ -211,6 +218,15 @@ class Arel::SelectManager
211
218
  def as table_name
212
219
  Arel::Nodes::TableAlias.new(self, table_name)
213
220
  end
221
+
222
+ # Install an alias, if present.
223
+ def xas table_name
224
+ if table_name.present?
225
+ as table_name
226
+ else
227
+ self
228
+ end
229
+ end
214
230
  end
215
231
 
216
232
  class Arel::Nodes::As
@@ -220,7 +236,11 @@ end
220
236
  class Arel::Table
221
237
  alias_method(:old_alias, :alias) rescue nil
222
238
  def alias(name = "#{self.name}_2")
223
- name.blank? ? self : Arel::Nodes::TableAlias.new(self,name)
239
+ if name.present?
240
+ Arel::Nodes::TableAlias.new(self, name)
241
+ else
242
+ self
243
+ end
224
244
  end
225
245
  end
226
246
 
@@ -6,8 +6,52 @@ require 'active_record'
6
6
 
7
7
  require 'support/fake_record'
8
8
 
9
+ def colored(color, msg)
10
+ ENV["TERM"] =~ /^xterm|-256color$/ ? "\x1b[#{color}m#{msg}\x1b[89m\x1b[0m" : "#{msg}"
11
+ end
12
+
13
+ YELLOW = "33"
14
+
15
+ def warn(msg)
16
+ $stderr.puts(colored(YELLOW, msg))
17
+ end
18
+
19
+ # Load gems specific to databases
20
+ # NOTE:
21
+ # It's strongly advised to test each database on its own. Loading multiple
22
+ # backend gems leads to undefined behavior according to tests; the backend
23
+ # might not recognize the correct DB visitor and will fallback to `ToSQL`
24
+ # and screw all tests.
25
+ #
26
+ # The issue also seems to be related to arel version: at some point, arel
27
+ # dropped its wide support for DBs and kept Postgres, MySQL and SQLite.
28
+ # Here, we're just trying to load the correct ones.
29
+ db_and_gem = if RUBY_ENGINE == 'jruby'
30
+ {
31
+ 'oracle' => 'activerecord-oracle_enhanced-adapter',
32
+ 'mssql' => 'activerecord-jdbcsqlserver-adapter'
33
+ }
34
+ else
35
+ {
36
+ 'oracle' => 'activerecord-oracle_enhanced-adapter',
37
+ 'mssql' => 'activerecord-sqlserver-adapter'
38
+ }
39
+ end
40
+
41
+ def load_lib(gem)
42
+ if gem && (RUBY_ENGINE == 'jruby' || Arel::VERSION.to_i > 9)
43
+ begin
44
+ Gem::Specification.find_by_name(gem)
45
+ require gem
46
+ rescue Gem::MissingSpecError
47
+ warn "Warning: failed to load gem #{gem}. Are you sure it's installed?"
48
+ end
49
+ end
50
+ end
51
+
52
+ load_lib(db_and_gem[ENV['DB']])
53
+
9
54
  require 'arel_extensions'
10
- Arel::Table.engine = FakeRecord::Base.new
11
55
 
12
56
  $arel_silence_type_casting_deprecation = true
13
57