marty 13.0.2 → 14.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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