marty 13.0.2 → 14.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +48 -70
- data/CHANGELOG.md +26 -0
- data/Gemfile +2 -1
- data/app/components/marty/data_grid_view.rb +5 -0
- data/app/helpers/marty/application_helper.rb +1 -1
- data/app/models/marty/data_grid.rb +256 -49
- data/app/services/marty/data_grid/constraint.rb +2 -2
- data/db/migrate/603_add_strict_null_mode_to_data_grids.rb +5 -0
- data/lib/marty/content_handler.rb +3 -1
- data/lib/marty/railtie.rb +1 -0
- data/lib/marty/util.rb +27 -0
- data/lib/marty/version.rb +1 -1
- data/spec/models/data_grid_spec.rb +335 -21
- metadata +3 -2
data/Gemfile
CHANGED
@@ -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
|
@@ -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
|
-
|
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
|
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
|
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.
|
418
|
-
return unless
|
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(
|
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.
|
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
|
-
|
436
|
-
|
437
|
-
|
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(
|
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
|
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(
|
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(
|
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(
|
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
|
-
|
624
|
-
dg
|
625
|
-
dg.
|
626
|
-
dg.
|
627
|
-
dg.
|
628
|
-
dg.
|
629
|
-
dg.
|
630
|
-
dg.
|
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
|
645
|
-
self.data
|
646
|
-
self.data_type
|
647
|
-
self.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
|
650
|
-
self.constraint
|
651
|
-
self.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
|
|