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.
- 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
|
|