marty 13.0.2 → 14.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -19,7 +19,7 @@ module Marty
19
19
 
20
20
  pt = 'infinity'
21
21
  vals = raw_vals.map do |v|
22
- DataGrid.parse_fvalue(pt, v, data_type, dt)
22
+ DataGrid.parse_fvalue(pt, v, data_type, dt, false)
23
23
  end
24
24
  [[:in?, vals.flatten]]
25
25
  end
@@ -56,7 +56,7 @@ module Marty
56
56
  err = nil
57
57
  begin
58
58
  cvt_val = cvt && !data_v.class.in?(rt) ?
59
- [DataGrid.parse_fvalue(pt, data_v, dt, klass)].
59
+ [DataGrid.parse_fvalue(pt, data_v, dt, klass, false)].
60
60
  flatten.first : data_v
61
61
  rescue StandardError => e
62
62
  err = e.message
@@ -0,0 +1,5 @@
1
+ class AddStrictNullModeToDataGrids < ActiveRecord::Migration[5.1]
2
+ def change
3
+ add_column :marty_data_grids, :strict_null_mode, :boolean, null: false, default: false
4
+ end
5
+ end
@@ -6,6 +6,7 @@ module Marty::ContentHandler
6
6
  'html' => ['text/html', 'download'],
7
7
  'txt' => ['text/plain', 'inline'],
8
8
  'json' => ['application/json', 'download'],
9
+ 'pdf' => ['application/pdf', 'download'],
9
10
 
10
11
  # hacky: default format is JSON
11
12
  nil => ['application/json', 'download'],
@@ -29,7 +30,7 @@ module Marty::ContentHandler
29
30
  res = to_zip(data)
30
31
  when nil, 'json'
31
32
  res, format = data.to_json, 'json'
32
- when 'html'
33
+ when 'html', 'pdf'
33
34
  res = data.to_s
34
35
  else
35
36
  res, format = { error: "Unknown format: #{format}" }.to_json, 'json'
@@ -92,6 +93,7 @@ module Marty::ContentHandler
92
93
  res = Zip::OutputStream.write_buffer do |stream|
93
94
  to_zip_stream(stream, [], data)
94
95
  end
96
+
95
97
  res.string
96
98
  end
97
99
  end
@@ -6,5 +6,6 @@ module Marty
6
6
  config.marty.promise_job_enqueue_hooks = []
7
7
  config.marty.redis_url = nil
8
8
  config.marty.enable_action_cable = true
9
+ config.marty.data_grid_plpg_lookups = false
9
10
  end
10
11
  end
@@ -1,4 +1,6 @@
1
1
  module Marty::Util
2
+ extend Delorean::Functions
3
+
2
4
  def self.set_posting_id(sid)
3
5
  snap = Marty::Posting.find_by(id: sid)
4
6
  sid = nil if snap && (snap.created_dt == Float::INFINITY)
@@ -56,6 +58,31 @@ module Marty::Util
56
58
  res
57
59
  end
58
60
 
61
+ # Returns an array of methods and values that can be applied to a number
62
+ # in order to check if it's in the given range.
63
+ # Example: '(1,14]') => [[">", 1.0], ["<=", 14.0]]
64
+ delorean_fn :pg_range_to_ruby, cache: true do |r|
65
+ next r if r == 'empty' || r.nil?
66
+
67
+ m = pg_range_match(r)
68
+
69
+ raise "bad PG range #{r}" unless m
70
+
71
+ res = []
72
+
73
+ if m[:start] != ''
74
+ op = m[:open] == '(' ? '>' : '>='
75
+ res += [[op, m[:start].to_f]]
76
+ end
77
+
78
+ if m[:end] != ''
79
+ op = m[:close] == ')' ? '<' : '<='
80
+ res += [[op, m[:end].to_f]]
81
+ end
82
+
83
+ res
84
+ end
85
+
59
86
  def self.human_to_pg_range(r)
60
87
  return r if r == 'empty'
61
88
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Marty
4
- VERSION = '13.0.2'
4
+ VERSION = '14.0.0'
5
5
  end
@@ -1,4 +1,5 @@
1
1
  require 'spec_helper'
2
+ require 'benchmark/ips'
2
3
 
3
4
  module Marty::DataGridSpec # rubocop:disable Metrics/ModuleLength
4
5
  describe DataGrid do
@@ -187,11 +188,51 @@ Investor Services\t-0.625
187
188
  NOT (Admin Premium Services|Admin Services|Admin Services Plus)\t-1.0
188
189
  Admin Services Plus\t-1.625
189
190
  Investor Services Acadamy\t-0.5
191
+ EOS
192
+
193
+ G1_with_nulls = <<EOS
194
+ strict_null_mode
195
+ state\tstring\tv\t\t
196
+ ltv\tnumrange\tv\t\t
197
+ fico\tnumrange\th\t\t
198
+
199
+ \t\t>=600<700\t>=700<750\t>=750
200
+ CA\t<=80\t1.1\t2.2\t3.3
201
+ TX|HI\t>80<=105\t4.4\t5.5\t6.6
202
+ NM\t<=80\t1.2\t2.3\t3.4
203
+ MA\t>80<=105\t4.5\t5.6\t
204
+ NULL\t<=80\t11\t22\t33
205
+ EOS
206
+
207
+ G1_with_bool_nulls = <<EOS
208
+ strict_null_mode
209
+ bool_state\tboolean\tv\t\t
210
+ ltv\tnumrange\tv\t\t
211
+ fico\tnumrange\th\t\t
212
+
213
+ \t\t>=600<700\t>=700<750\t>=750
214
+ f\t>80<=105\t4.5\t5.6\t
215
+ NULL\t<=80\t11\t22\t33
216
+ EOS
217
+
218
+ G1_with_integer_nulls = <<EOS
219
+ strict_null_mode
220
+ int_state\tinteger\tv\t\t
221
+ ltv\tnumrange\tv\t\t
222
+ fico\tnumrange\th\t\t
223
+
224
+ \t\t>=600<700\t>=700<750\t>=750
225
+ 1\t<=80\t1.1\t2.2\t3.3
226
+ 2\t>80<=105\t4.4\t5.5\t6.6
227
+ 3\t<=80\t1.2\t2.3\t3.4
228
+ 4|5\t>80<=105\t4.5\t5.6\t
229
+ NULL\t<=80\t11\t22\t33
190
230
  EOS
191
231
 
192
232
  before(:each) do
193
233
  # Mcfly.whodunnit = Marty::User.find_by_login('marty')
194
234
  marty_whodunnit
235
+ Rails.application.config.marty.data_grid_plpg_lookups = false
195
236
  end
196
237
 
197
238
  def lookup_grid_helper(pt, gridname, params, follow = false, distinct = true)
@@ -213,6 +254,110 @@ EOS
213
254
  dg_from_import('Gh', Gh + "\t\t\n")
214
255
  end.to raise_error(RuntimeError)
215
256
  end
257
+
258
+ it 'show not allow import NULL fields unless strict_null_mode is on' do
259
+ expect do
260
+ dg_from_import(
261
+ 'G1_with_nulls',
262
+ G1_with_nulls.gsub("strict_null_mode\n", '')
263
+ )
264
+ end.to raise_error(
265
+ /NULL is not supported in grids without strict_null_mode/
266
+ )
267
+ end
268
+
269
+ it 'should import wildcards' do
270
+ dg = dg_from_import('G1', G1)
271
+ state_attr = dg.metadata.find { |key| key['attr'] == 'state' }
272
+ expect(state_attr['keys'].last).to be nil
273
+ expect(state_attr['wildcards'].last).to be true
274
+ expect(state_attr['wildcards']).to eq [false, false, false, false, true]
275
+ end
276
+
277
+ it 'allows to import NULL values in string fields' do
278
+ dg = dg_from_import('G1_with_nulls', G1_with_nulls)
279
+ state_attr = dg.metadata.find { |key| key['attr'] == 'state' }
280
+ expect(state_attr['keys'].last).to be nil
281
+ expect(state_attr['wildcards'].last).to be false
282
+
283
+ # FIXME: do we actually need mixing nulls with values?
284
+ dg = dg_from_import(
285
+ 'G1_with_nulls2',
286
+ G1_with_nulls.sub('NULL', 'NY|NULL')
287
+ )
288
+ state_attr = dg.metadata.find { |key| key['attr'] == 'state' }
289
+ expect(state_attr['keys'].last).to eq [nil, 'NY']
290
+ expect(state_attr['wildcards'].last).to be false
291
+
292
+ dg = dg_from_import(
293
+ 'G1_with_nulls3',
294
+ G1_with_nulls.sub('NULL', 'NOT (NULL)')
295
+ )
296
+
297
+ state_attr = dg.metadata.find { |key| key['attr'] == 'state' }
298
+ expect(state_attr['keys'].last).to be nil
299
+ expect(state_attr['wildcards'].last).to be false
300
+ expect(state_attr['nots'].last).to be true
301
+
302
+ dg = dg_from_import(
303
+ 'G1_with_nulls4',
304
+ G1_with_nulls.sub('NULL', 'NOT (NY|NULL)')
305
+ )
306
+
307
+ state_attr = dg.metadata.find { |key| key['attr'] == 'state' }
308
+ expect(state_attr['keys'].last).to eq [nil, 'NY']
309
+ expect(state_attr['wildcards'].last).to be false
310
+ expect(state_attr['nots'].last).to be true
311
+ end
312
+
313
+ it 'allows to import NULL values in integer field' do
314
+ dg = dg_from_import('G1_with_integer_nulls', G1_with_integer_nulls)
315
+ state_attr = dg.metadata.find { |key| key['attr'] == 'int_state' }
316
+ expect(state_attr['keys'].last).to be nil
317
+ expect(state_attr['wildcards'].last).to be false
318
+
319
+ dg = dg_from_import(
320
+ 'G1_with_integer_nulls2',
321
+ G1_with_integer_nulls.sub('NULL', '6|NULL')
322
+ )
323
+
324
+ state_attr = dg.metadata.find { |key| key['attr'] == 'int_state' }
325
+ expect(state_attr['keys'].last).to eq [nil, 6]
326
+ expect(state_attr['nots'].last).to be false
327
+ expect(state_attr['wildcards'].last).to be false
328
+
329
+ dg = dg_from_import(
330
+ 'G1_with_integer_nulls3',
331
+ G1_with_integer_nulls.sub('NULL', 'NOT (NULL)')
332
+ )
333
+
334
+ state_attr = dg.metadata.find { |key| key['attr'] == 'int_state' }
335
+ expect(state_attr['keys'].last).to be nil
336
+ expect(state_attr['nots'].last).to be true
337
+ expect(state_attr['wildcards'].last).to be false
338
+
339
+ dg = dg_from_import(
340
+ 'G1_with_integer_nulls4',
341
+ G1_with_integer_nulls.sub('NULL', 'NOT (6|NULL)')
342
+ )
343
+
344
+ state_attr = dg.metadata.find { |key| key['attr'] == 'int_state' }
345
+ expect(state_attr['keys'].last).to eq [nil, 6]
346
+ expect(state_attr['nots'].last).to be true
347
+ expect(state_attr['wildcards'].last).to be false
348
+ end
349
+
350
+ it 'allows to import NULL values in boolean field' do
351
+ dg = dg_from_import('G1_with_bool_nulls', G1_with_bool_nulls)
352
+ state_attr = dg.metadata.find { |key| key['attr'] == 'bool_state' }
353
+ expect(state_attr['keys'].last).to be nil
354
+ expect(state_attr['wildcards'].last).to be false
355
+
356
+ dg = dg_from_import('G1_with_bool_nulls2', G1_with_bool_nulls.sub('NULL', 'NOT (NULL)'))
357
+ state_attr = dg.metadata.find { |key| key['attr'] == 'bool_state' }
358
+ expect(state_attr['keys'].last).to be nil
359
+ expect(state_attr['nots'].last).to be true
360
+ end
216
361
  end
217
362
 
218
363
  describe 'validations' do
@@ -306,7 +451,7 @@ EOS
306
451
 
307
452
  before(:each) do
308
453
  %w[G1 G2 G3 G4 G5 G6 G7 G8 Ga Gb
309
- Gc Gd Ge Gf Gg Gh Gj Gl].each do |g|
454
+ Gc Gd Ge Gf Gg Gh Gj Gl G1_with_nulls].each do |g|
310
455
  dg_from_import(g, "Marty::DataGridSpec::#{g}".constantize)
311
456
  end
312
457
  end
@@ -351,6 +496,24 @@ EOS
351
496
  end
352
497
  end
353
498
 
499
+ it 'should cast types' do
500
+ res = Marty::DataGrid.lookup_grid_h(pt, 'Gf', { 'i' => 13, 'n' => 15 }, true)
501
+ expect(res).to eq('N')
502
+
503
+ res = Marty::DataGrid.lookup_grid_h(pt, 'Gf', { 'i' => '13', 'n' => '15' }, true)
504
+ expect(res).to eq('N')
505
+
506
+ res = Marty::DataGrid.lookup_grid_h(pt, 'Gf', { 'b' => 'true', 'i4' => '6' }, false)
507
+ expect(res).to eq('Y')
508
+
509
+ res = Marty::DataGrid.lookup_grid_h(pt, 'Gg', { 'i1' => 2, 'i2' => 1 }, false)
510
+ expect(res).to eq(1)
511
+
512
+ dg_from_import('G9', G9)
513
+ res = Marty::DataGrid.lookup_grid_h(pt, 'G9', { 'state' => 4, 'ltv' => 81 }, false)
514
+ expect(res).to eq(456)
515
+ end
516
+
354
517
  it 'should handle ambiguous lookups' do
355
518
  h1 = {
356
519
  'property_state' => 'NY',
@@ -454,7 +617,7 @@ EOS
454
617
  'ltv' => 100,
455
618
  'cltv' => 110.1,
456
619
  )
457
- end.to raise_error(RuntimeError)
620
+ end.to raise_error(RuntimeError, /matches > 1/)
458
621
  end
459
622
 
460
623
  it 'should return nil when matching data grid cell is nil' do
@@ -474,9 +637,81 @@ EOS
474
637
  'state' => 'GU',
475
638
  'ltv' => 80,
476
639
  )
640
+
477
641
  expect(res).to eq [22, 'G1']
478
642
  end
479
643
 
644
+ it 'should treat nil as missing attr' do
645
+ expect do
646
+ res = lookup_grid_helper('infinity',
647
+ 'G1',
648
+ 'fico' => 720,
649
+ 'state' => 'NM',
650
+ 'ltv' => 80,
651
+ )
652
+ end.to raise_error(RuntimeError, /matches > 1/)
653
+
654
+ expect do
655
+ res = lookup_grid_helper('infinity',
656
+ 'G1',
657
+ 'fico' => 720,
658
+ 'ltv' => 80,
659
+ )
660
+ end.to raise_error(RuntimeError, /matches > 1/)
661
+
662
+ expect do
663
+ res = lookup_grid_helper('infinity',
664
+ 'G1',
665
+ 'fico' => 720,
666
+ 'state' => nil,
667
+ 'ltv' => 80,
668
+ )
669
+ end.to raise_error(RuntimeError, /matches > 1/)
670
+ end
671
+
672
+ it 'should handle string NULLS' do
673
+ res = lookup_grid_helper('infinity',
674
+ 'G1_with_nulls',
675
+ 'fico' => 720,
676
+ 'state' => nil,
677
+ 'ltv' => 80,
678
+ )
679
+
680
+ expect(res).to eq [22, 'G1_with_nulls']
681
+
682
+ expect do
683
+ lookup_grid_helper('infinity',
684
+ 'G1_with_nulls',
685
+ 'fico' => 720,
686
+ 'state' => 'BLABLA',
687
+ 'ltv' => 80,
688
+ )
689
+ end.to raise_error(/Data Grid lookup failed/)
690
+
691
+ dg = dg_from_import(
692
+ 'G1_with_nulls2',
693
+ G1_with_nulls.sub('NULL', 'NY|NULL')
694
+ )
695
+
696
+ res = lookup_grid_helper('infinity',
697
+ dg.name,
698
+ 'fico' => 720,
699
+ 'state' => nil,
700
+ 'ltv' => 80,
701
+ )
702
+
703
+ expect(res).to eq [22, dg.name]
704
+
705
+ res = lookup_grid_helper('infinity',
706
+ dg.name,
707
+ 'fico' => 720,
708
+ 'state' => 'NY',
709
+ 'ltv' => 80,
710
+ )
711
+
712
+ expect(res).to eq [22, dg.name]
713
+ end
714
+
480
715
  it 'should handle matches which also have a wildcard match' do
481
716
  dg_from_import('G9', G9)
482
717
 
@@ -485,7 +720,7 @@ EOS
485
720
  'G9',
486
721
  'state' => 'CA', 'ltv' => 81,
487
722
  )
488
- end.to raise_error(RuntimeError)
723
+ end.to raise_error(RuntimeError, /matches > 1/)
489
724
 
490
725
  res = lookup_grid_helper('infinity',
491
726
  'G9',
@@ -494,30 +729,42 @@ EOS
494
729
  expect(res).to eq [456, 'G9']
495
730
  end
496
731
 
497
- it 'should raise on nil attr values' do
732
+ # it 'should raise on nil attr values' do
733
+ # next
734
+ # dg_from_import('G9', G9)
735
+ #
736
+ # expect do
737
+ # lookup_grid_helper('infinity',
738
+ # 'G9',
739
+ # 'ltv' => 81,
740
+ # )
741
+ # end.to raise_error(/matches > 1/)
742
+ #
743
+ # err = /Data Grid lookup failed/
744
+ # expect do
745
+ # lookup_grid_helper('infinity',
746
+ # 'G9',
747
+ # { 'state' => 'CA', 'ltv' => nil },
748
+ # false, false)
749
+ # end.to raise_error(err)
750
+ #
751
+ # res = lookup_grid_helper('infinity',
752
+ # 'G9',
753
+ # { 'state' => nil, 'ltv' => 81 },
754
+ # false, false)
755
+ #
756
+ # expect(res).to eq [456, 'G9']
757
+ # end
758
+
759
+ it 'should raise if nothing was found' do
498
760
  dg_from_import('G9', G9)
499
761
 
500
762
  expect do
501
763
  lookup_grid_helper('infinity',
502
764
  'G9',
503
- 'ltv' => 81,
765
+ 'ltv' => 80,
504
766
  )
505
- end.to raise_error(/matches > 1/)
506
-
507
- err = /Data Grid lookup failed/
508
- expect do
509
- lookup_grid_helper('infinity',
510
- 'G9',
511
- { 'state' => 'CA', 'ltv' => nil },
512
- false, false)
513
- end.to raise_error(err)
514
-
515
- res = lookup_grid_helper('infinity',
516
- 'G9',
517
- { 'state' => nil, 'ltv' => 81 },
518
- false, false)
519
-
520
- expect(res).to eq [456, 'G9']
767
+ end.to raise_error(/Data Grid lookup failed/)
521
768
  end
522
769
 
523
770
  it 'should handle boolean keys' do
@@ -635,21 +882,25 @@ EOS
635
882
  'attr' => 'units',
636
883
  'keys' => [[1, 2], [1, 2], [3, 4], [3, 4]],
637
884
  'nots' => [false, false, false, false],
885
+ 'wildcards' => [false, false, false, false],
638
886
  'type' => 'integer' },
639
887
  { 'dir' => 'v',
640
888
  'attr' => 'ltv',
641
889
  'keys' => ['[,80]', '(80,105]', '[,80]', '(80,105]'],
642
890
  'nots' => [false, false, false, false],
891
+ 'wildcards' => [false, false, false, false],
643
892
  'type' => 'numrange' },
644
893
  { 'dir' => 'h',
645
894
  'attr' => 'cltv',
646
895
  'keys' => ['[100,110)', '[110,120)', '[120,]'],
647
896
  'nots' => [false, false, false],
897
+ 'wildcards' => [false, false, false],
648
898
  'type' => 'numrange' },
649
899
  { 'dir' => 'h',
650
900
  'attr' => 'fico',
651
901
  'keys' => ['[600,700)', '[700,750)', '[750,]'],
652
902
  'nots' => [false, false, false],
903
+ 'wildcards' => [false, false, false],
653
904
  'type' => 'numrange' }]
654
905
 
655
906
  dgh = Marty::DataGrid.lookup_h(pt, 'G2')
@@ -666,17 +917,20 @@ EOS
666
917
  'attr' => 'state',
667
918
  'keys' => [['CA'], ['HI', 'TX'], ['NM'], ['MA'], nil],
668
919
  'nots' => [false, false, false, false, false],
920
+ 'wildcards' => [false, false, false, false, true],
669
921
  'type' => 'string' },
670
922
  { 'dir' => 'v',
671
923
  'attr' => 'ltv',
672
924
  'keys' => ['[,80]', '(80,105]', '[,80]', '(80,105]',
673
925
  '[,80]'],
674
926
  'nots' => [false, false, false, false, false],
927
+ 'wildcards' => [false, false, false, false, false],
675
928
  'type' => 'numrange' },
676
929
  { 'dir' => 'h',
677
930
  'attr' => 'fico',
678
931
  'keys' => ['[600,700)', '[700,750)', '[750,]'],
679
932
  'nots' => [false, false, false],
933
+ 'wildcards' => [false, false, false],
680
934
  'type' => 'numrange' }]
681
935
  dgh = Marty::DataGrid.lookup_h(pt, 'G8')
682
936
  res = Marty::DataGrid.lookup_grid_distinct_entry_h(pt,
@@ -694,6 +948,7 @@ EOS
694
948
  'attr' => 'ltv',
695
949
  'keys' => ['[,115]', '(115,135]', '(135,140]'],
696
950
  'nots' => [false, false, false],
951
+ 'wildcards' => [false, false, false],
697
952
  'type' => 'numrange' }]
698
953
  dgh = Marty::DataGrid.lookup_h(pt, 'G8')
699
954
  res = Marty::DataGrid.lookup_grid_distinct_entry_h(pt,
@@ -945,6 +1200,65 @@ EOS
945
1200
  false\t\t>10\t\t#{values3[2]}
946
1201
  EOS
947
1202
  end
1203
+
1204
+ describe 'performance' do
1205
+ before(:each) do
1206
+ %w[G1 Gf Gl].each do |g|
1207
+ dg_from_import(g, "Marty::DataGridSpec::#{g}".constantize)
1208
+ end
1209
+ end
1210
+
1211
+ after do
1212
+ Rails.application.config.marty.data_grid_plpg_lookups = false
1213
+ end
1214
+
1215
+ let(:pt) { 'infinity' }
1216
+
1217
+ grid_data = {
1218
+ 'Gf' => { 'b' => true },
1219
+ 'G1' => {
1220
+ 'fico' => 600,
1221
+ 'state' => 'RI',
1222
+ 'ltv' => 10,
1223
+ },
1224
+ 'Gl' => {
1225
+ 'fha_203k_option2' => 'Not Existing Services'
1226
+ }
1227
+ }
1228
+
1229
+ grid_data.each_with_index do |(grid, params), index|
1230
+ it "ruby lookup is faster than plpgsql #{index}" do
1231
+ bm = Benchmark.ips do |x|
1232
+ x.report('postgres') do
1233
+ Rails.application.config.marty.data_grid_plpg_lookups = true
1234
+ res = Marty::DataGrid.lookup_grid_h(pt, grid, params, false)
1235
+ end
1236
+
1237
+ x.report('ruby') do
1238
+ Rails.application.config.marty.data_grid_plpg_lookups = false
1239
+ res = Marty::DataGrid.lookup_grid_h(pt, grid, params, false)
1240
+ end
1241
+
1242
+ x.compare!
1243
+ end
1244
+
1245
+ h = bm.entries.each_with_object({}) do |e, hh|
1246
+ hh[e.label] = e.stats.central_tendency
1247
+ end
1248
+
1249
+ factor = h['ruby'] / h['postgres']
1250
+
1251
+ if ENV['CI'] == 'true'
1252
+ # Performance drops down in CI, probably due to running postgres
1253
+ # in a separate container utilizing it's own CPU core.
1254
+ expect(factor).to be > 0.8
1255
+ else
1256
+ expect(factor).to be > 1.02
1257
+ end
1258
+ end
1259
+ end
1260
+ end
1261
+
948
1262
  describe 'constraint' do
949
1263
  it 'constraint' do
950
1264
  Mcfly.whodunnit = system_user