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