marty 13.0.2 → 14.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile CHANGED
@@ -20,7 +20,8 @@ group :default, :cmit do
20
20
  end
21
21
 
22
22
  group :development, :test do
23
- gem 'benchmark-ips'
23
+ # FIXME: 2.8.0 is broken, we should wait until 2.8.1 is out
24
+ gem 'benchmark-ips', '< 2.8.0'
24
25
  gem 'capybara'
25
26
  gem 'connection_pool'
26
27
  gem 'database_cleaner'
@@ -96,6 +96,7 @@ module Marty; class DataGridView < McflyGridPanel
96
96
  :vcols,
97
97
  :hcols,
98
98
  :lenient,
99
+ :strict_null_mode,
99
100
  :data_type,
100
101
  :constraint,
101
102
  :perm_view,
@@ -305,6 +306,10 @@ module Marty; class DataGridView < McflyGridPanel
305
306
  c.width = 75
306
307
  end
307
308
 
309
+ attribute :strict_null_mode do |c|
310
+ c.width = 100
311
+ end
312
+
308
313
  attribute :data_type do |c|
309
314
  c.label = 'Data Type'
310
315
  c.width = 200
@@ -11,7 +11,7 @@ module Marty
11
11
  end
12
12
 
13
13
  def javascript_exists?(file)
14
- asset_exists?(file, :js, DEFAULT_ASSETS_PATH + '/javascript')
14
+ asset_exists?(file, :js, DEFAULT_ASSETS_PATH + '/javascripts')
15
15
  end
16
16
 
17
17
  def stylesheet_exists?(file)
@@ -13,6 +13,7 @@ class Marty::DataGrid < Marty::Base
13
13
  ARRSEP = '|'.freeze
14
14
  NOT_STRING_START = 'NOT ('.freeze
15
15
  NOT_STRING_END = ')'.freeze
16
+ NULL_STRING = 'NULL'.freeze
16
17
 
17
18
  class DataGridValidator < ActiveModel::Validator
18
19
  def validate(dg)
@@ -106,7 +107,7 @@ class Marty::DataGrid < Marty::Base
106
107
  # FIXME: if the caller requests data as part of fields, there could
107
108
  # be memory concerns with caching since some data_grids have massive data
108
109
  delorean_fn :lookup_h, cache: true, sig: [2, 3] do |pt, name, fields = nil|
109
- fields ||= %w(id group_id created_dt metadata data_type name)
110
+ fields ||= %w(id group_id created_dt metadata data_type name strict_null_mode)
110
111
  dga = mcfly_pt(pt).where(name: name).pluck(*fields).first
111
112
  dga && Hash[fields.zip(dga)]
112
113
  end
@@ -184,6 +185,160 @@ class Marty::DataGrid < Marty::Base
184
185
 
185
186
  PLV_DT_FMT = '%Y-%m-%d %H:%M:%S.%N6'
186
187
 
188
+ def self.ruby_lookup_indices(h_passed, dgh)
189
+ dgh['metadata'].each_with_object({ 'v' => [], 'h' => [] }) do |m, h|
190
+ attr = m['attr']
191
+
192
+ inc = h_passed[attr]
193
+
194
+ val = (defined? inc.name) ? inc.name : inc
195
+
196
+ dir = m['dir']
197
+
198
+ m_type = m['type']
199
+ nots = m.fetch('nots', [])
200
+ wildcards = m.fetch('wildcards', [])
201
+
202
+ unless dgh['strict_null_mode']
203
+ next unless h_passed.key?(attr)
204
+ # FIXME: Make sure it won't break lookups
205
+ # Before missing attribute would match anything,
206
+ # while explicitly passed nil would only match wildcard keys
207
+ # We want to be consistent and treat nil attribute as missing one,
208
+ # unless it's a stict_null_mode, where nil would be explicitly mapped
209
+ # to NULL keys
210
+ next if val.nil?
211
+ end
212
+
213
+ converted_val = if val.nil?
214
+ nil
215
+ elsif m_type == 'string'
216
+ val.to_s
217
+ elsif m_type == 'integer'
218
+ val.to_i
219
+ elsif m_type == 'numrange'
220
+ val.to_f
221
+ elsif m_type == 'int4range'
222
+ val.to_i
223
+ elsif m_type == 'boolean'
224
+ ActiveModel::Type::Boolean.new.cast(val)
225
+ else
226
+ val
227
+ end
228
+
229
+ arr = m['keys'].each_with_index.map do |key_val, index|
230
+ wildcard = wildcards.fetch(index, true) # By default empty value is a wildcard
231
+
232
+ next index if key_val.nil? && wildcard
233
+
234
+ not_condition = nots[index]
235
+
236
+ check_res = if ['int4range', 'numrange'].include?(m_type)
237
+ raise 'Data Grid lookup failed' if val.nil?
238
+
239
+ checks = Marty::Util.pg_range_to_ruby(key_val)
240
+ checks.all? do |check|
241
+ converted_val.send(check[0], check[1])
242
+ end
243
+ elsif key_val.nil? # Non-wildcard lookup
244
+ val.nil?
245
+ elsif m_type == 'boolean'
246
+ key_val == converted_val
247
+ else
248
+ key_val.include?(converted_val)
249
+ end
250
+
251
+ if check_res && !not_condition
252
+ next index
253
+ elsif !check_res && not_condition
254
+ next index
255
+ end
256
+
257
+ nil
258
+ end.compact
259
+
260
+ h[dir] << arr
261
+ end
262
+ end
263
+
264
+ def self.ruby_lookup_grid_distinct(h_passed, dgh, ret_grid_data = false,
265
+ distinct = true)
266
+
267
+ grid = Marty::DataGrid.find(dgh['id'])
268
+ indices = ruby_lookup_indices(h_passed, dgh)
269
+
270
+ # We use the 0 as default, if there are no indices in that dir
271
+ # Otherwise we find an intersection between all indices
272
+ v_indices = if indices['v'].empty?
273
+ [0]
274
+ else
275
+ indices['v'].reduce(:&)
276
+ end
277
+
278
+ h_indices = if indices['h'].empty?
279
+ [0]
280
+ else
281
+ indices['h'].reduce(:&)
282
+ end
283
+
284
+ if distinct
285
+ raise 'matches > 1' if v_indices.size > 1
286
+ raise 'matches > 1' if h_indices.size > 1
287
+ end
288
+
289
+ v_index_min = v_indices.min
290
+ h_index_min = h_indices.min
291
+
292
+ if v_index_min.nil? || h_index_min.nil?
293
+ nil_res = {
294
+ 'data' => nil,
295
+ 'name' => grid.name,
296
+ 'result' => nil,
297
+ 'metadata' => nil
298
+ }
299
+
300
+ return nil_res if grid.lenient && !ret_grid_data
301
+
302
+ raise 'Data Grid lookup failed'
303
+ end
304
+
305
+ res2 = grid.data.dig(v_index_min, h_index_min)
306
+
307
+ if ret_grid_data
308
+ {
309
+ 'data' => grid.data,
310
+ 'name' => grid.name,
311
+ 'result' => res2,
312
+ 'metadata' => grid.metadata
313
+ }
314
+ else
315
+ {
316
+ 'data' => nil,
317
+ 'name' => grid.name,
318
+ 'result' => res2,
319
+ 'metadata' => nil
320
+ }
321
+ end
322
+ rescue StandardError => e
323
+ ri = {
324
+ 'id' => grid.id,
325
+ 'group_id' => grid.group_id,
326
+ 'created_dt' => grid.created_dt
327
+ }
328
+
329
+ dg = grid.attributes.reject do |k, _|
330
+ next true if !ret_grid_data && k == 'data'
331
+
332
+ k == 'permissions'
333
+ end
334
+
335
+ raise "DG #{name}: Error in Ruby call: #{e.message} \n"\
336
+ "params: #{h_passed}\n"\
337
+ "results: #{[h_indices, v_indices]}\n"\
338
+ "dg: #{grid.attributes}\n"\
339
+ "ri: #{ri}"
340
+ end
341
+
187
342
  def self.plpg_lookup_grid_distinct(h_passed, dgh, ret_grid_data = false,
188
343
  distinct = true)
189
344
  cd = dgh['created_dt']
@@ -280,7 +435,13 @@ class Marty::DataGrid < Marty::Base
280
435
  # "name" => <grid name>
281
436
  # "data" => <grid's data array>
282
437
  # "metadata" => <grid's metadata (array of hashes)>
283
- vhash = plpg_lookup_grid_distinct(h, dgh, return_grid_data, distinct)
438
+
439
+ vhash = if Rails.application.config.marty.data_grid_plpg_lookups &&
440
+ Rails.env.test? # Keep plpg lookups for performance tests
441
+ plpg_lookup_grid_distinct(h, dgh, return_grid_data, distinct)
442
+ else
443
+ ruby_lookup_grid_distinct(h, dgh, return_grid_data, distinct)
444
+ end
284
445
 
285
446
  return vhash if vhash['result'].nil? || !dgh['data_type']
286
447
 
@@ -323,16 +484,25 @@ class Marty::DataGrid < Marty::Base
323
484
 
324
485
  type = inf['type']
325
486
  nots = inf.fetch('nots', [])
487
+ wildcards = inf.fetch('wildcards', [])
326
488
  klass = type.constantize unless INDEX_MAP[type]
327
489
 
328
- keys = inf['keys'].map do |v|
490
+ keys = inf['keys'].each_with_index.map do |v, index|
491
+ wildcard = wildcards.fetch(index, true)
492
+
493
+ next 'NULL' if v.nil? && !wildcard
494
+
329
495
  case type
330
496
  when 'numrange', 'int4range'
331
497
  Marty::Util.pg_range_to_human(v)
332
498
  when 'boolean'
333
499
  v.to_s
334
500
  when 'string', 'integer'
335
- v.map(&:to_s).join(ARRSEP) if v
501
+ v.map do |val|
502
+ next 'NULL' if val.nil? && !wildcard
503
+
504
+ val.to_s
505
+ end.join(ARRSEP) if v
336
506
  else
337
507
  # assume it's an AR class
338
508
  v.each do |k|
@@ -362,16 +532,13 @@ class Marty::DataGrid < Marty::Base
362
532
  def export_array
363
533
  # add data type metadata row if not default
364
534
  lenstr = 'lenient' if lenient
535
+ strict_null_mode_str = 'strict_null_mode' if strict_null_mode
365
536
 
366
537
  typestr = data_type unless [nil, DEFAULT_DATA_TYPE].member?(data_type)
367
- len_type = [lenstr, typestr].compact.join(' ')
368
-
369
- meta_rows = if (lenient || typestr) && constraint
370
- [[len_type, constraint]]
371
- elsif lenient || typestr
372
- [[len_type]]
373
- elsif constraint
374
- [['', constraint]]
538
+ len_type = [lenstr, strict_null_mode_str, typestr].compact.join(' ')
539
+
540
+ meta_rows = if len_type.present? || constraint.present?
541
+ [[len_type, constraint].compact]
375
542
  else
376
543
  []
377
544
  end
@@ -414,30 +581,54 @@ class Marty::DataGrid < Marty::Base
414
581
  dg.export
415
582
  end
416
583
 
417
- def self.parse_fvalue(pt, v, type, klass)
418
- return unless v
584
+ def self.null_value?(value, strict_null_mode)
585
+ return false unless value == NULL_STRING
586
+ return true if strict_null_mode
587
+
588
+ raise 'NULL is not supported in grids without strict_null_mode'
589
+ end
590
+
591
+ def self.parse_fvalue(pt, passed_val, type, klass, strict_null_mode = false)
592
+ return unless passed_val
419
593
 
420
- v = remove_not(v)
594
+ v = remove_not(passed_val)
595
+
596
+ return nil if null_value?(v, strict_null_mode)
421
597
 
422
598
  case type
423
599
  when 'numrange', 'int4range'
424
600
  Marty::Util.human_to_pg_range(v)
425
601
  when 'integer'
426
602
  v.split(ARRSEP).map do |val|
603
+ next nil if null_value?(val, strict_null_mode)
604
+
427
605
  Integer(val) rescue raise "invalid integer: #{val}"
428
- end.uniq.sort
606
+ end.uniq.sort_by(&:to_i)
429
607
  when 'float'
430
608
  v.split(ARRSEP).map do |val|
609
+ next nil if null_value?(val, strict_null_mode)
610
+
431
611
  Float(val) rescue raise "invalid float: #{val}"
432
612
  end.uniq.sort
433
613
  when 'string'
434
- res = v.split(ARRSEP).uniq.sort
435
- raise 'leading/trailing spaces in elements not allowed' if
436
- res.any? { |x| x != x.strip }
437
- raise '0-length string not allowed' if res.any?(&:empty?)
614
+ res = v.split(ARRSEP).uniq.sort.map do |val|
615
+ next nil if null_value?(val, strict_null_mode)
616
+
617
+ val
618
+ end
619
+
620
+ raise 'leading/trailing spaces in elements not allowed' if res.any? do |x|
621
+ x != x&.strip
622
+ end
623
+
624
+ raise '0-length string not allowed' if res.any? do |x|
625
+ x&.empty?
626
+ end
438
627
 
439
628
  res
440
629
  when 'boolean'
630
+ return nil if null_value?(v, strict_null_mode)
631
+
441
632
  case v.downcase
442
633
  when 'true', 't'
443
634
  true
@@ -469,15 +660,15 @@ class Marty::DataGrid < Marty::Base
469
660
  raise "unknown header type/klass: #{type}"
470
661
  end
471
662
 
472
- def self.parse_keys(pt, keys, type)
663
+ def self.parse_keys(pt, keys, type, strict_null_mode)
473
664
  klass = maybe_get_klass(type)
474
665
 
475
666
  keys.map do |v|
476
- parse_fvalue(pt, v, type, klass)
667
+ parse_fvalue(pt, v, type, klass, strict_null_mode)
477
668
  end
478
669
  end
479
670
 
480
- def self.parse_nots(_pt, keys)
671
+ def self.parse_nots(keys)
481
672
  keys.map do |v|
482
673
  next false unless v
483
674
 
@@ -485,6 +676,10 @@ class Marty::DataGrid < Marty::Base
485
676
  end
486
677
  end
487
678
 
679
+ def self.parse_wildcards(keys)
680
+ keys.map(&:nil?)
681
+ end
682
+
488
683
  # parse grid external representation into metadata/data
489
684
  def self.parse(pt, grid_text, options)
490
685
  options[:headers] ||= false
@@ -503,20 +698,23 @@ class Marty::DataGrid < Marty::Base
503
698
 
504
699
  raise "last row can't be blank" if rows[-1].all?(&:nil?)
505
700
 
506
- data_type, lenient = nil, false
701
+ data_type, lenient, strict_null_mode = nil, false, false
507
702
 
508
703
  # check if there's a data_type definition
509
704
  dt, constraint, *x = rows[0]
510
705
  if dt && x.all?(&:nil?)
511
706
  dts = dt.split
512
- raise "bad data type '#{dt}'" if dts.count > 2
513
707
 
514
- lenient = dts.delete 'lenient'
708
+ lenient = dts.delete('lenient').present?
709
+ strict_null_mode = dts.delete('strict_null_mode').present?
515
710
  data_type = dts.first
711
+ raise "bad data type '#{dt}'" if dts.size > 1
516
712
  end
713
+
517
714
  constraint = nil if x.first.in?(['v', 'h'])
518
715
 
519
- start_md = constraint || data_type || lenient ? 1 : 0
716
+ start_md = constraint || data_type || lenient || strict_null_mode ? 1 : 0
717
+
520
718
  rows_for_metadata = rows[start_md...blank_index]
521
719
  metadata = rows_for_metadata.map do |attr, type, dir, rs_keep, key|
522
720
  raise 'metadata elements must include attr/type/dir' unless
@@ -525,8 +723,9 @@ class Marty::DataGrid < Marty::Base
525
723
  raise "unknown metadata type #{type}" unless
526
724
  Marty::DataGrid.type_to_index(type)
527
725
 
528
- keys = key && parse_keys(pt, [key], type)
529
- nots = key && parse_nots(pt, [key])
726
+ keys = key && parse_keys(pt, [key], type, strict_null_mode)
727
+ nots = key && parse_nots([key])
728
+ wildcards = key && parse_wildcards([key])
530
729
 
531
730
  res = {
532
731
  'attr' => attr,
@@ -534,6 +733,7 @@ class Marty::DataGrid < Marty::Base
534
733
  'dir' => dir,
535
734
  'keys' => keys,
536
735
  'nots' => nots,
736
+ 'wildcards' => wildcards,
537
737
  }
538
738
  res['rs_keep'] = rs_keep if rs_keep
539
739
  res
@@ -552,8 +752,9 @@ class Marty::DataGrid < Marty::Base
552
752
  raise "horiz. key row #{data_index + i} must include nil starting cells" if
553
753
  row[0, v_infos.count].any?
554
754
 
555
- inf['keys'] = parse_keys(pt, row[v_infos.count, row.count], inf['type'])
556
- inf['nots'] = parse_nots(pt, row[v_infos.count, row.count])
755
+ inf['keys'] = parse_keys(pt, row[v_infos.count, row.count], inf['type'], strict_null_mode)
756
+ inf['nots'] = parse_nots(row[v_infos.count, row.count])
757
+ inf['wildcards'] = parse_wildcards(row[v_infos.count, row.count])
557
758
  end
558
759
 
559
760
  raise 'horiz. info keys length mismatch!' unless
@@ -565,8 +766,9 @@ class Marty::DataGrid < Marty::Base
565
766
  v_key_cols = data_rows.map { |r| r[0, v_infos.count] }.transpose
566
767
 
567
768
  v_infos.each_with_index do |inf, i|
568
- inf['keys'] = parse_keys(pt, v_key_cols[i], inf['type'])
569
- inf['nots'] = parse_nots(pt, v_key_cols[i])
769
+ inf['keys'] = parse_keys(pt, v_key_cols[i], inf['type'], strict_null_mode)
770
+ inf['nots'] = parse_nots(v_key_cols[i])
771
+ inf['wildcards'] = parse_wildcards(v_key_cols[i])
570
772
  end
571
773
 
572
774
  raise 'vert. info keys length mismatch!' unless
@@ -607,6 +809,7 @@ class Marty::DataGrid < Marty::Base
607
809
  data: data,
608
810
  data_type: data_type,
609
811
  lenient: lenient,
812
+ strict_null_mode: strict_null_mode,
610
813
  constraint: constraint,
611
814
  }
612
815
  end
@@ -619,15 +822,17 @@ class Marty::DataGrid < Marty::Base
619
822
  data_type = parsed_result[:data_type]
620
823
  lenient = parsed_result[:lenient]
621
824
  constraint = parsed_result[:constraint]
622
-
623
- dg = new
624
- dg.name = name
625
- dg.data = data
626
- dg.data_type = data_type
627
- dg.lenient = !!lenient
628
- dg.metadata = metadata
629
- dg.created_dt = created_dt if created_dt
630
- dg.constraint = constraint
825
+ strict_null_mode = parsed_result[:strict_null_mode]
826
+
827
+ dg = new
828
+ dg.name = name
829
+ dg.data = data
830
+ dg.data_type = data_type
831
+ dg.lenient = lenient
832
+ dg.strict_null_mode = strict_null_mode
833
+ dg.metadata = metadata
834
+ dg.created_dt = created_dt if created_dt
835
+ dg.constraint = constraint
631
836
  dg.save!
632
837
  dg
633
838
  end
@@ -640,15 +845,17 @@ class Marty::DataGrid < Marty::Base
640
845
  data_type = parsed_result[:data_type]
641
846
  lenient = parsed_result[:lenient]
642
847
  constraint = parsed_result[:constraint]
848
+ strict_null_mode = parsed_result[:strict_null_mode]
643
849
 
644
- self.name = name
645
- self.data = data
646
- self.data_type = data_type
647
- self.lenient = !!lenient
850
+ self.name = name
851
+ self.data = data
852
+ self.data_type = data_type
853
+ self.lenient = !!lenient
854
+ self.strict_null_mode = strict_null_mode
648
855
  # Otherwise changed will depend on order in hashes
649
- self.metadata = new_metadata unless metadata == new_metadata
650
- self.constraint = constraint
651
- self.created_dt = created_dt if created_dt
856
+ self.metadata = new_metadata unless metadata == new_metadata
857
+ self.constraint = constraint
858
+ self.created_dt = created_dt if created_dt
652
859
  save!
653
860
  end
654
861