marty 1.0.54 → 1.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +3 -1
- data/Gemfile.lock +2 -2
- data/app/models/marty/base.rb +20 -0
- data/app/models/marty/data_grid.rb +2 -5
- data/app/models/marty/script.rb +2 -5
- data/lib/marty.rb +1 -1
- data/lib/marty/mcfly_model.rb +188 -0
- data/lib/marty/monkey.rb +58 -1
- data/lib/marty/version.rb +1 -1
- data/marty.gemspec +1 -1
- data/spec/dummy/app/models/gemini/bud_category.rb +1 -1
- data/spec/dummy/app/models/gemini/fannie_bup.rb +1 -1
- data/spec/dummy/config/environment.rb +1 -1
- data/spec/lib/delorean_query_spec.rb +159 -0
- data/spec/spec_helper.rb +1 -1
- metadata +6 -5
- data/lib/marty/mcfly_query.rb +0 -189
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5f65bd901e74662263479f7acb3c2f616e41be2a
|
4
|
+
data.tar.gz: 6ef39fe48e763f6758e93d0f75aae5266bb3ab64
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bf459ac2132cd88ce789485e564c1885b2433a8bf2fed54239249d660038db911dfec94345e8166ee279c8b1955aa7a93812fed51e16013084679e8235c2fd5d
|
7
|
+
data.tar.gz: 5c89ea1896bfed735b88a6cf3e0d8e507d7ef329d35c53cf1e933d1183826861fa411378f3dbf36dc6de6882a5bfc399b3e12803754b602aa0440b4e3c4c37c5
|
data/Gemfile
CHANGED
@@ -26,7 +26,9 @@ group :development, :test do
|
|
26
26
|
gem 'netzke-core'
|
27
27
|
gem 'netzke-basepack'
|
28
28
|
gem 'netzke-testing'
|
29
|
+
gem 'rspec-instafail', require: false
|
29
30
|
|
30
31
|
gem 'marty_rspec'
|
31
|
-
|
32
|
+
|
33
|
+
# gem 'delorean_lang', path: File.expand_path('../../delorean', __FILE__)
|
32
34
|
end
|
data/Gemfile.lock
CHANGED
data/app/models/marty/base.rb
CHANGED
@@ -1,4 +1,24 @@
|
|
1
1
|
class Marty::Base < ActiveRecord::Base
|
2
2
|
self.table_name_prefix = "marty_"
|
3
3
|
self.abstract_class = true
|
4
|
+
|
5
|
+
def self.mcfly_pt(pt)
|
6
|
+
tb = self.table_name
|
7
|
+
self.where("#{tb}.obsoleted_dt >= ? AND #{tb}.created_dt < ?", pt, pt)
|
8
|
+
end
|
9
|
+
MCFLY_PT_SIG = [1, 1]
|
10
|
+
|
11
|
+
# FIXME: hacky signatures for AR queries
|
12
|
+
COUNT_SIG = [0, 0]
|
13
|
+
DISTINCT_SIG = [0, 100]
|
14
|
+
FIRST_SIG = [0, 1]
|
15
|
+
GROUP_SIG = [1, 100]
|
16
|
+
JOINS_SIG = [1, 100]
|
17
|
+
LAST_SIG = [0, 1]
|
18
|
+
LIMIT_SIG = [1, 1]
|
19
|
+
NOT_SIG = [1, 100]
|
20
|
+
ORDER_SIG = [1, 100]
|
21
|
+
PLUCK_SIG = [1, 100]
|
22
|
+
SELECT_SIG = [1, 100]
|
23
|
+
WHERE_SIG = [0, 100]
|
4
24
|
end
|
@@ -86,11 +86,8 @@ class Marty::DataGrid < Marty::Base
|
|
86
86
|
validates_with DataGridValidator
|
87
87
|
validates_with Marty::NameValidator, field: :name
|
88
88
|
|
89
|
-
gen_mcfly_lookup :lookup,
|
90
|
-
|
91
|
-
}
|
92
|
-
|
93
|
-
gen_mcfly_lookup :get_all, {}, mode: :all
|
89
|
+
gen_mcfly_lookup :lookup, [:name], cache: true
|
90
|
+
gen_mcfly_lookup :get_all, [], mode: nil
|
94
91
|
|
95
92
|
cached_mcfly_lookup :lookup_id, sig: 2 do
|
96
93
|
|pt, group_id|
|
data/app/models/marty/script.rb
CHANGED
@@ -10,11 +10,8 @@ class Marty::Script < Marty::Base
|
|
10
10
|
|
11
11
|
belongs_to :user, class_name: "Marty::User"
|
12
12
|
|
13
|
-
gen_mcfly_lookup :lookup,
|
14
|
-
|
15
|
-
}
|
16
|
-
|
17
|
-
gen_mcfly_lookup :get_all, {}, mode: :all
|
13
|
+
gen_mcfly_lookup :lookup, [:name], cache: true
|
14
|
+
gen_mcfly_lookup :get_all, {}, mode: nil
|
18
15
|
|
19
16
|
# find script by name/tag
|
20
17
|
def self.find_script(sname, tag=nil)
|
data/lib/marty.rb
CHANGED
@@ -0,0 +1,188 @@
|
|
1
|
+
require 'mcfly'
|
2
|
+
|
3
|
+
module Mcfly::Model
|
4
|
+
def self.included(base)
|
5
|
+
base.send :extend, ClassMethods
|
6
|
+
end
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
def clear_lookup_cache!
|
10
|
+
@LOOKUP_CACHE.clear if @LOOKUP_CACHE
|
11
|
+
end
|
12
|
+
|
13
|
+
# FIXME IDEA: we just make :cache an argument to delorean_fn.
|
14
|
+
# That way, we don't need the cached_ flavors. It'll make all
|
15
|
+
# this code a lot simpler. We should also just add the :private
|
16
|
+
# mechanism here.
|
17
|
+
|
18
|
+
# Implements a VERY HACKY class-based (per process) caching
|
19
|
+
# mechanism for database lookup results. Issues include: cached
|
20
|
+
# values are ActiveRecord objects. Query results can be very
|
21
|
+
# large lists which we count as one item in the cache. Caching
|
22
|
+
# mechanism will result in large processes.
|
23
|
+
def cached_delorean_fn(name, options = {}, &block)
|
24
|
+
@LOOKUP_CACHE ||= {}
|
25
|
+
|
26
|
+
delorean_fn(name, options) do |ts, *args|
|
27
|
+
cache_key = [name, ts] + args.map{ |a|
|
28
|
+
a.is_a?(ActiveRecord::Base) ? a.id : a
|
29
|
+
} unless Mcfly.is_infinity(ts)
|
30
|
+
|
31
|
+
next @LOOKUP_CACHE[cache_key] if
|
32
|
+
cache_key && @LOOKUP_CACHE.has_key?(cache_key)
|
33
|
+
|
34
|
+
res = block.call(ts, *args)
|
35
|
+
|
36
|
+
if cache_key
|
37
|
+
# Cache has >1000 items, clear out the oldest 200. FIXME:
|
38
|
+
# hard-coded, should be configurable. Cache
|
39
|
+
# size/invalidation should be per lookup and not class.
|
40
|
+
# We're invalidating cache items simply based on age and
|
41
|
+
# not usage. This is faster but not as fair.
|
42
|
+
if @LOOKUP_CACHE.count > 1000
|
43
|
+
@LOOKUP_CACHE.keys[0..200].each{|k| @LOOKUP_CACHE.delete(k)}
|
44
|
+
end
|
45
|
+
@LOOKUP_CACHE[cache_key] = res
|
46
|
+
|
47
|
+
# Since we're caching this object and don't want anyone
|
48
|
+
# changing it. FIXME: ideally should freeze this object
|
49
|
+
# recursively.
|
50
|
+
res.freeze unless res.is_a?(ActiveRecord::Relation)
|
51
|
+
end
|
52
|
+
res
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# FIXME: duplicate code from Mcfly's mcfly_lookup.
|
57
|
+
def cached_mcfly_lookup(name, options = {}, &block)
|
58
|
+
cached_delorean_fn(name, options) do |ts, *args|
|
59
|
+
raise "nil timestamp" if ts.nil?
|
60
|
+
|
61
|
+
ts = Mcfly.normalize_infinity(ts)
|
62
|
+
|
63
|
+
self.mcfly_pt(ts).scoping do
|
64
|
+
block.call(ts, *args)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# FIXME: add private mode. This should make the function
|
70
|
+
# unavailable to delorean.
|
71
|
+
def gen_mcfly_lookup(name, attrs, options={})
|
72
|
+
raise "bad options #{options.keys}" unless
|
73
|
+
(options.keys - [:mode, :cache, :private]).empty?
|
74
|
+
|
75
|
+
mode = options.fetch(:mode, :first)
|
76
|
+
|
77
|
+
# if mode is nil, don't cache -- i.e. don't cache AR queries
|
78
|
+
cache = mode && options[:cache]
|
79
|
+
|
80
|
+
# the older mode=:all is not supported (it's bogus)
|
81
|
+
raise "bad mode #{mode}" unless [nil, :first].member?(mode)
|
82
|
+
|
83
|
+
assoc = Set.new(self.reflect_on_all_associations.map(&:name))
|
84
|
+
|
85
|
+
qstr = attrs.map {|k, v|
|
86
|
+
k = "#{k}_id" if assoc.member?(k)
|
87
|
+
|
88
|
+
v ? "(#{k} = ? OR #{k} IS NULL)" : "(#{k} = ?)"
|
89
|
+
}.join(" AND ")
|
90
|
+
|
91
|
+
if Hash === attrs
|
92
|
+
order = attrs.select {|k, v| v}.keys.reverse.map { |k|
|
93
|
+
k = "#{k}_id" if assoc.member?(k)
|
94
|
+
|
95
|
+
"#{k} NULLS LAST"
|
96
|
+
}.join(", ")
|
97
|
+
attrs = attrs.keys
|
98
|
+
else
|
99
|
+
raise "bad attrs" unless Array === attrs
|
100
|
+
end
|
101
|
+
|
102
|
+
fn = cache ? :cached_mcfly_lookup : :mcfly_lookup
|
103
|
+
|
104
|
+
# hacky: if private, set sig to bad value -- i.e. can't be
|
105
|
+
# called from delorean. Ideally, we should have a 'private'
|
106
|
+
# option for delorean_fn.
|
107
|
+
sig = options[:private] ? -1 : attrs.length+1
|
108
|
+
|
109
|
+
send(fn, name, sig: sig) do
|
110
|
+
|t, *attr_list|
|
111
|
+
|
112
|
+
attr_list_ids = attr_list.each_with_index.map {|x, i|
|
113
|
+
assoc.member?(attrs[i]) ?
|
114
|
+
(attr_list[i] && attr_list[i].id) : attr_list[i]
|
115
|
+
}
|
116
|
+
|
117
|
+
q = self.where(qstr, *attr_list_ids)
|
118
|
+
q = q.order(order) if order
|
119
|
+
mode ? q.send(mode) : q
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
######################################################################
|
124
|
+
|
125
|
+
# Generates categorization lookups, e.g. given class GFee:
|
126
|
+
|
127
|
+
# gen_mcfly_lookup_cat :lookup_q,
|
128
|
+
# [:security_instrument,
|
129
|
+
# 'Gemini::SecurityInstrumentCategorization',
|
130
|
+
# :g_fee_category],
|
131
|
+
# {
|
132
|
+
# entity: true,
|
133
|
+
# security_instrument: true,
|
134
|
+
# coupon: true,
|
135
|
+
# },
|
136
|
+
# nil
|
137
|
+
|
138
|
+
# rel_attr = :security_instrument
|
139
|
+
# cat_assoc_klass = Gemini::SecurityInstrumentCategorization
|
140
|
+
# cat_attr = :g_fee_category
|
141
|
+
# name = :lookup_q
|
142
|
+
# pc_name = :pc_lookup_q
|
143
|
+
# pc_attrs = {entity: true, security_instrument: true, coupon: true}
|
144
|
+
|
145
|
+
def gen_mcfly_lookup_cat(name, catrel, attrs, options={})
|
146
|
+
rel_attr, cat_assoc_name, cat_attr = catrel
|
147
|
+
|
148
|
+
raise "#{rel_attr} should be mapped in attrs" if attrs[rel_attr].nil?
|
149
|
+
|
150
|
+
cat_assoc_klass = cat_assoc_name.constantize
|
151
|
+
|
152
|
+
# replace rel_attr with cat_attr in attrs
|
153
|
+
pc_attrs = attrs.each_with_object({}) {|(k, v), h|
|
154
|
+
h[k == rel_attr ? "#{cat_attr}_id" : k] = v
|
155
|
+
}
|
156
|
+
|
157
|
+
pc_name = "pc_#{name}".to_sym
|
158
|
+
|
159
|
+
gen_mcfly_lookup(pc_name, pc_attrs, options + {private: true})
|
160
|
+
|
161
|
+
lpi = attrs.keys.index rel_attr
|
162
|
+
|
163
|
+
raise "should not include #{cat_attr}" if attrs.member?(cat_attr)
|
164
|
+
raise "need #{rel_attr} argument" unless lpi
|
165
|
+
|
166
|
+
# cache if mode is not nil
|
167
|
+
fn = options.fetch(:mode, :first) ? :cached_delorean_fn : :delorean_fn
|
168
|
+
|
169
|
+
send(fn, name, sig: attrs.length+1) do
|
170
|
+
|ts, *args|
|
171
|
+
|
172
|
+
# Example: rel is a Gemini::SecurityInstrument instance.
|
173
|
+
rel = args[lpi]
|
174
|
+
raise "#{rel_attr} can't be nil" unless rel
|
175
|
+
|
176
|
+
args[lpi] = cat_assoc_klass.
|
177
|
+
mcfly_pt(ts).
|
178
|
+
# FIXME: XXXX why is this join needed???
|
179
|
+
# joins(cat_attr).
|
180
|
+
where(rel_attr => rel).
|
181
|
+
pluck("#{cat_attr}_id").
|
182
|
+
first
|
183
|
+
|
184
|
+
self.send(pc_name, ts, *args)
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
data/lib/marty/monkey.rb
CHANGED
@@ -202,7 +202,7 @@ end
|
|
202
202
|
class StringEnum < String
|
203
203
|
include Delorean::Model
|
204
204
|
def name
|
205
|
-
self
|
205
|
+
self.to_s
|
206
206
|
end
|
207
207
|
def id
|
208
208
|
self
|
@@ -224,6 +224,7 @@ class StringEnum < String
|
|
224
224
|
new(v)
|
225
225
|
end
|
226
226
|
end
|
227
|
+
|
227
228
|
YAML::add_domain_type("pennymac.com,2017-06-02", "stringEnum") do
|
228
229
|
|type, val|
|
229
230
|
StringEnum.new(val)
|
@@ -252,3 +253,59 @@ module ActiveRecord
|
|
252
253
|
end
|
253
254
|
end
|
254
255
|
end
|
256
|
+
|
257
|
+
######################################################################
|
258
|
+
|
259
|
+
class ActiveRecord::Relation
|
260
|
+
def mcfly_pt(pt, cls=nil)
|
261
|
+
cls ||= self.klass
|
262
|
+
tb = cls.table_name
|
263
|
+
self.where("#{tb}.obsoleted_dt >= ? AND #{tb}.created_dt < ?", pt, pt)
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
######################################################################
|
268
|
+
|
269
|
+
class ActiveRecord::Base
|
270
|
+
class << self
|
271
|
+
alias_method :old_joins, :joins
|
272
|
+
|
273
|
+
def joins(*args)
|
274
|
+
# when joins args are strings, checks to see if they're
|
275
|
+
# associations attrs. If so, convert them to symbols for joins
|
276
|
+
# to work properly.
|
277
|
+
new_args = args.map {|a|
|
278
|
+
self.reflections.has_key?(a) ? a.to_sym : a
|
279
|
+
}
|
280
|
+
old_joins(*new_args)
|
281
|
+
end
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
args_hack = [[ActiveRecord::Relation, ActiveRecord::QueryMethods::WhereChain]] +
|
286
|
+
[[Object, nil]]*10
|
287
|
+
|
288
|
+
Delorean::RUBY_WHITELIST.merge!(
|
289
|
+
count: [ActiveRecord::Relation],
|
290
|
+
distinct: args_hack,
|
291
|
+
group: args_hack,
|
292
|
+
joins: args_hack,
|
293
|
+
limit: [ActiveRecord::Relation, Integer],
|
294
|
+
not: args_hack,
|
295
|
+
order: args_hack,
|
296
|
+
pluck: args_hack,
|
297
|
+
select: args_hack,
|
298
|
+
where: args_hack,
|
299
|
+
mcfly_pt: [ActiveRecord::Relation,
|
300
|
+
[Date, Time, ActiveSupport::TimeWithZone, String],
|
301
|
+
[nil, Class]],
|
302
|
+
)
|
303
|
+
|
304
|
+
######################################################################
|
305
|
+
|
306
|
+
module Mcfly::Controller
|
307
|
+
# define mcfly user to be Flowscape's current_user.
|
308
|
+
def user_for_mcfly
|
309
|
+
find_current_user rescue nil
|
310
|
+
end
|
311
|
+
end
|
data/lib/marty/version.rb
CHANGED
data/marty.gemspec
CHANGED
@@ -0,0 +1,159 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
module Marty
|
4
|
+
|
5
|
+
bud_cats =<<EOF
|
6
|
+
name
|
7
|
+
Conv Fixed 30
|
8
|
+
Conv Fixed 20
|
9
|
+
EOF
|
10
|
+
|
11
|
+
fannie_bup =<<EOF
|
12
|
+
bud_category note_rate buy_up buy_down settlement_mm settlement_yy
|
13
|
+
Conv Fixed 30 2.250 4.42000 7.24000 12 2012
|
14
|
+
Conv Fixed 30 2.375 4.42000 7.24000 12 2012
|
15
|
+
Conv Fixed 30 2.500 4.41300 7.22800 12 2012
|
16
|
+
Conv Fixed 30 2.625 4.37500 7.16200 12 2012
|
17
|
+
Conv Fixed 30 2.750 4.32900 7.09300 12 2012
|
18
|
+
Conv Fixed 20 2.875 4.24800 6.95900 12 2012
|
19
|
+
Conv Fixed 20 2.875 4.24800 6.95900 11 2012
|
20
|
+
EOF
|
21
|
+
|
22
|
+
script =<<EOF
|
23
|
+
A:
|
24
|
+
c = Gemini::FannieBup.
|
25
|
+
joins("bud_category").
|
26
|
+
where("name LIKE '%30'").
|
27
|
+
count
|
28
|
+
|
29
|
+
s = Gemini::FannieBup.
|
30
|
+
joins("bud_category").
|
31
|
+
select("name").
|
32
|
+
distinct("name").
|
33
|
+
pluck("name")
|
34
|
+
|
35
|
+
o = Gemini::FannieBup.
|
36
|
+
order("note_rate DESC", "buy_down ASC").
|
37
|
+
select("note_rate").
|
38
|
+
first.note_rate
|
39
|
+
|
40
|
+
g = Gemini::FannieBup.
|
41
|
+
select("settlement_yy*settlement_mm AS x, count(*) AS c").
|
42
|
+
group("settlement_mm", "settlement_yy").
|
43
|
+
order("settlement_mm").to_a
|
44
|
+
|
45
|
+
gg = [r.attributes for r in g]
|
46
|
+
|
47
|
+
n = Gemini::FannieBup.where.not("settlement_mm < 12").count
|
48
|
+
|
49
|
+
q = Gemini::FannieBup.where("settlement_mm = 12")
|
50
|
+
q1 = q.order("note_rate ASC").pluck("note_rate")
|
51
|
+
q2 = q.order("note_rate DESC").pluck("note_rate")
|
52
|
+
|
53
|
+
settlement_mm =?
|
54
|
+
note_rate =?
|
55
|
+
pq = Gemini::FannieBup.
|
56
|
+
where({"settlement_mm": settlement_mm}).
|
57
|
+
where("note_rate > ?", note_rate).
|
58
|
+
pluck("note_rate")
|
59
|
+
|
60
|
+
m = Gemini::FannieBup.
|
61
|
+
joins("bud_category").
|
62
|
+
mcfly_pt('infinity').
|
63
|
+
select("name").
|
64
|
+
pluck("name")
|
65
|
+
|
66
|
+
mm = Gemini::FannieBup.
|
67
|
+
mcfly_pt('01-01-2003').count
|
68
|
+
EOF
|
69
|
+
|
70
|
+
describe 'DeloreanQuery' do
|
71
|
+
before(:each) do
|
72
|
+
marty_whodunnit
|
73
|
+
Marty::DataImporter.do_import_summary(Gemini::BudCategory, bud_cats)
|
74
|
+
Marty::DataImporter.do_import_summary(Gemini::FannieBup, fannie_bup)
|
75
|
+
|
76
|
+
Marty::Script.load_script_bodies(
|
77
|
+
{
|
78
|
+
"A" => script,
|
79
|
+
}, Date.today)
|
80
|
+
|
81
|
+
@engine = Marty::ScriptSet.new.get_engine("A")
|
82
|
+
end
|
83
|
+
|
84
|
+
it "perfroms join+count" do
|
85
|
+
res = @engine.evaluate("A", "c", {})
|
86
|
+
|
87
|
+
expect(res).to eq Gemini::FannieBup.
|
88
|
+
joins("bud_category").
|
89
|
+
where("name LIKE '%30'").
|
90
|
+
count
|
91
|
+
|
92
|
+
end
|
93
|
+
|
94
|
+
it "perfroms select+distinct" do
|
95
|
+
res = @engine.evaluate("A", "s", {})
|
96
|
+
|
97
|
+
expect(res).to eq Gemini::FannieBup.
|
98
|
+
joins("bud_category").
|
99
|
+
select("name").
|
100
|
+
distinct("name").
|
101
|
+
pluck("name")
|
102
|
+
end
|
103
|
+
|
104
|
+
it "perfroms mcfly_pt" do
|
105
|
+
res = @engine.evaluate("A", ["m", "mm"], {})
|
106
|
+
|
107
|
+
expect(res).to eq [
|
108
|
+
Gemini::FannieBup.
|
109
|
+
joins("bud_category").
|
110
|
+
mcfly_pt('infinity').
|
111
|
+
select("name").
|
112
|
+
pluck("name"),
|
113
|
+
Gemini::FannieBup.
|
114
|
+
mcfly_pt('01-01-2003').count,
|
115
|
+
]
|
116
|
+
end
|
117
|
+
|
118
|
+
it "perfroms order+first" do
|
119
|
+
res = @engine.evaluate("A", "o", {})
|
120
|
+
|
121
|
+
expect(res).to eq Gemini::FannieBup.
|
122
|
+
order("note_rate DESC", "buy_down ASC").
|
123
|
+
select("note_rate").
|
124
|
+
first.note_rate
|
125
|
+
end
|
126
|
+
|
127
|
+
it "perfroms group+count" do
|
128
|
+
res = @engine.evaluate("A", "gg", {})
|
129
|
+
|
130
|
+
expect(res).
|
131
|
+
to eq Gemini::FannieBup.
|
132
|
+
select("settlement_yy*settlement_mm AS x, count(*) AS c").
|
133
|
+
group("settlement_mm", "settlement_yy").
|
134
|
+
order("settlement_mm").
|
135
|
+
map(&:attributes)
|
136
|
+
end
|
137
|
+
|
138
|
+
it "perfroms where+not" do
|
139
|
+
res = @engine.evaluate("A", "n", {})
|
140
|
+
|
141
|
+
expect(res).to eq Gemini::FannieBup.where.not("settlement_mm < 12").count
|
142
|
+
end
|
143
|
+
|
144
|
+
it "perfroms query+query" do
|
145
|
+
res = @engine.evaluate("A", ["q1", "q2"], {})
|
146
|
+
|
147
|
+
expect(res).to eq [
|
148
|
+
[2.25, 2.375, 2.5, 2.625, 2.75, 2.875],
|
149
|
+
[2.875, 2.75, 2.625, 2.5, 2.375, 2.25],
|
150
|
+
]
|
151
|
+
end
|
152
|
+
|
153
|
+
it "handle query params" do
|
154
|
+
res = @engine.evaluate("A", "pq",
|
155
|
+
{"settlement_mm" => 12, "note_rate" => 2.5})
|
156
|
+
expect(res).to eq [2.625, 2.75, 2.875]
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -5,7 +5,7 @@ require 'rspec/rails'
|
|
5
5
|
require 'database_cleaner'
|
6
6
|
require 'marty_rspec'
|
7
7
|
|
8
|
-
Dummy::Application.initialize!
|
8
|
+
Dummy::Application.initialize! unless Dummy::Application.initialized?
|
9
9
|
|
10
10
|
ActiveRecord::Migrator.migrate File.expand_path("../../db/migrate/", __FILE__)
|
11
11
|
ActiveRecord::Migrator.migrate File.expand_path("../dummy/db/migrate/", __FILE__)
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: marty
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Arman Bostani
|
@@ -14,7 +14,7 @@ authors:
|
|
14
14
|
autorequire:
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
|
-
date: 2017-
|
17
|
+
date: 2017-12-04 00:00:00.000000000 Z
|
18
18
|
dependencies:
|
19
19
|
- !ruby/object:Gem::Dependency
|
20
20
|
name: pg
|
@@ -92,14 +92,14 @@ dependencies:
|
|
92
92
|
requirements:
|
93
93
|
- - "~>"
|
94
94
|
- !ruby/object:Gem::Version
|
95
|
-
version:
|
95
|
+
version: 0.3.24
|
96
96
|
type: :runtime
|
97
97
|
prerelease: false
|
98
98
|
version_requirements: !ruby/object:Gem::Requirement
|
99
99
|
requirements:
|
100
100
|
- - "~>"
|
101
101
|
- !ruby/object:Gem::Version
|
102
|
-
version:
|
102
|
+
version: 0.3.24
|
103
103
|
- !ruby/object:Gem::Dependency
|
104
104
|
name: mcfly
|
105
105
|
requirement: !ruby/object:Gem::Requirement
|
@@ -496,7 +496,7 @@ files:
|
|
496
496
|
- lib/marty/json_schema.rb
|
497
497
|
- lib/marty/lazy_column_loader.rb
|
498
498
|
- lib/marty/logger.rb
|
499
|
-
- lib/marty/
|
499
|
+
- lib/marty/mcfly_model.rb
|
500
500
|
- lib/marty/migrations.rb
|
501
501
|
- lib/marty/monkey.rb
|
502
502
|
- lib/marty/permissions.rb
|
@@ -1616,6 +1616,7 @@ files:
|
|
1616
1616
|
- spec/job_helper.rb
|
1617
1617
|
- spec/lib/data_exporter_spec.rb
|
1618
1618
|
- spec/lib/data_importer_spec.rb
|
1619
|
+
- spec/lib/delorean_query_spec.rb
|
1619
1620
|
- spec/lib/json_schema_spec.rb
|
1620
1621
|
- spec/lib/logger_spec.rb
|
1621
1622
|
- spec/lib/migrations/vw_marty_postings.sql.expected
|
data/lib/marty/mcfly_query.rb
DELETED
@@ -1,189 +0,0 @@
|
|
1
|
-
require 'mcfly'
|
2
|
-
|
3
|
-
module Mcfly
|
4
|
-
module Model
|
5
|
-
|
6
|
-
def self.included(base)
|
7
|
-
base.send :extend, ClassMethods
|
8
|
-
end
|
9
|
-
|
10
|
-
module ClassMethods
|
11
|
-
def clear_lookup_cache!
|
12
|
-
@LOOKUP_CACHE.clear if @LOOKUP_CACHE
|
13
|
-
end
|
14
|
-
|
15
|
-
# Implements a VERY HACKY class-based caching mechanism for
|
16
|
-
# database lookup results. Issues include: cached values are
|
17
|
-
# ActiveRecord objects. Not sure if these should be shared
|
18
|
-
# across connections. Query results can potentially be very
|
19
|
-
# large lists which we simply count as one item in the cache.
|
20
|
-
# Caching mechanism will result in large processes. Caches are
|
21
|
-
# not sharable across different Ruby processes.
|
22
|
-
def cached_delorean_fn(name, options = {}, &block)
|
23
|
-
@LOOKUP_CACHE ||= {}
|
24
|
-
|
25
|
-
delorean_fn(name, options) do |ts, *args|
|
26
|
-
cache_key = [name, ts] + args.map{ |a|
|
27
|
-
a.is_a?(ActiveRecord::Base) ? a.id : a
|
28
|
-
} unless Mcfly.is_infinity(ts)
|
29
|
-
|
30
|
-
next @LOOKUP_CACHE[cache_key] if
|
31
|
-
cache_key && @LOOKUP_CACHE.has_key?(cache_key)
|
32
|
-
|
33
|
-
res = block.call(ts, *args)
|
34
|
-
|
35
|
-
if cache_key
|
36
|
-
# Cache has >1000 items, clear out the oldest 200. FIXME:
|
37
|
-
# hard-coded, should be configurable. Cache
|
38
|
-
# size/invalidation should be per lookup and not class.
|
39
|
-
# We're invalidating cache items simply based on age and
|
40
|
-
# not usage. This is faster but not as fair.
|
41
|
-
if @LOOKUP_CACHE.count > 1000
|
42
|
-
@LOOKUP_CACHE.keys[0..200].each{|k| @LOOKUP_CACHE.delete(k)}
|
43
|
-
end
|
44
|
-
@LOOKUP_CACHE[cache_key] = res
|
45
|
-
|
46
|
-
# Since we're caching this object and don't want anyone
|
47
|
-
# changing it. FIXME: ideally should freeze this object
|
48
|
-
# recursively.
|
49
|
-
res.freeze unless res.is_a?(ActiveRecord::Relation)
|
50
|
-
end
|
51
|
-
res
|
52
|
-
end
|
53
|
-
end
|
54
|
-
|
55
|
-
# FIXME: duplicate code from Mcfly's mcfly_lookup.
|
56
|
-
def cached_mcfly_lookup(name, options = {}, &block)
|
57
|
-
cached_delorean_fn(name, options) do |ts, *args|
|
58
|
-
raise "time cannot be nil" if ts.nil?
|
59
|
-
|
60
|
-
ts = Mcfly.normalize_infinity(ts)
|
61
|
-
|
62
|
-
where("obsoleted_dt >= ? AND created_dt < ?", ts, ts).scoping do
|
63
|
-
block.call(ts, *args)
|
64
|
-
end
|
65
|
-
end
|
66
|
-
end
|
67
|
-
|
68
|
-
# FIXME: for validation purposes, this mechanism should make
|
69
|
-
# sure that the allable attrs are not required.
|
70
|
-
def gen_mcfly_lookup(name, attrs, options={})
|
71
|
-
raise "bad options" unless options.is_a?(Hash)
|
72
|
-
|
73
|
-
# FIXME: mode should be sent later, not as a part of
|
74
|
-
# gen_mcfly_lookup. i.e. we just generate the search and the
|
75
|
-
# mode is applied at runtime by delorean code. That would
|
76
|
-
# allow lookups to be used in either mode dynamically.
|
77
|
-
mode = options.fetch(:mode, :first)
|
78
|
-
|
79
|
-
assoc = Set.new(self.reflect_on_all_associations.map(&:name))
|
80
|
-
attr_names = attrs.keys
|
81
|
-
|
82
|
-
allables = attrs.select {|k, v| v}
|
83
|
-
|
84
|
-
order = allables.keys.reverse.map { |k|
|
85
|
-
k = "#{k}_id" if assoc.member?(k)
|
86
|
-
"#{k} NULLS LAST"
|
87
|
-
}.join(", ")
|
88
|
-
|
89
|
-
qstr = attrs.map {|k, v|
|
90
|
-
k = "#{k}_id" if assoc.member?(k)
|
91
|
-
v ? "(#{k} = ? OR #{k} IS NULL)" : "(#{k} = ?)"
|
92
|
-
}.join(" AND ")
|
93
|
-
|
94
|
-
cached_mcfly_lookup(name, sig: attrs.length+1) do
|
95
|
-
|t, *attr_list|
|
96
|
-
|
97
|
-
attr_list_ids = attr_list.each_with_index.map {|x, i|
|
98
|
-
assoc.member?(attr_names[i]) ?
|
99
|
-
(attr_list[i] && attr_list[i].id) : attr_list[i]
|
100
|
-
}
|
101
|
-
|
102
|
-
q = self.where(qstr, *attr_list_ids)
|
103
|
-
q = q.order(order) unless order.empty?
|
104
|
-
mode = :to_a if mode == :all
|
105
|
-
mode ? q.send(mode) : q
|
106
|
-
end
|
107
|
-
end
|
108
|
-
|
109
|
-
######################################################################
|
110
|
-
|
111
|
-
# Generates categorization lookups. For instance,
|
112
|
-
# suppose we have the following in class GFee:
|
113
|
-
#
|
114
|
-
# gen_mcfly_lookup_cat :lookup_q,
|
115
|
-
# [:security_instrument,
|
116
|
-
# 'Gemini::SecurityInstrumentCategorization',
|
117
|
-
# :g_fee_category],
|
118
|
-
# {
|
119
|
-
# entity: true,
|
120
|
-
# security_instrument: true,
|
121
|
-
# coupon: true,
|
122
|
-
# },
|
123
|
-
# nil
|
124
|
-
|
125
|
-
# In the above case,
|
126
|
-
# rel_attr = :security_instrument
|
127
|
-
# cat_assoc_klass = Gemini::SecurityInstrumentCategorization
|
128
|
-
# cat_attr = :g_fee_category
|
129
|
-
# name = :lookup_q
|
130
|
-
# pc_name = :pc_lookup_q
|
131
|
-
# pc_attrs = {entity: true, security_instrument: true,
|
132
|
-
# g_fee_category: true, coupon: true}
|
133
|
-
|
134
|
-
def gen_mcfly_lookup_cat(name, catrel, attrs, options={})
|
135
|
-
rel_attr, cat_assoc_name, cat_attr = catrel
|
136
|
-
|
137
|
-
raise "#{rel_attr} should be mapped in attrs" if
|
138
|
-
attrs[rel_attr].nil?
|
139
|
-
|
140
|
-
cat_assoc_klass = cat_assoc_name.constantize
|
141
|
-
|
142
|
-
raise "need lookup method on #{cat_assoc_klass}" unless
|
143
|
-
cat_assoc_klass.respond_to? :lookup
|
144
|
-
|
145
|
-
# replace rel_attr with cat_attr in attrs
|
146
|
-
pc_attrs = attrs.each_with_object({}) {|(k, v), h|
|
147
|
-
h[k == rel_attr ? cat_attr : k] = v
|
148
|
-
}
|
149
|
-
|
150
|
-
pc_name = "pc_#{name}".to_sym
|
151
|
-
gen_mcfly_lookup(pc_name, pc_attrs, options)
|
152
|
-
|
153
|
-
lpi = attrs.keys.index rel_attr
|
154
|
-
|
155
|
-
raise "should not include #{cat_attr}" if
|
156
|
-
attrs.member?(cat_attr)
|
157
|
-
|
158
|
-
raise "need #{rel_attr} argument" unless lpi
|
159
|
-
|
160
|
-
delorean_fn(name, sig: attrs.length+1) do |ts, *args|
|
161
|
-
# Example: rel is a Gemini::SecurityInstrument instance.
|
162
|
-
rel = args[lpi]
|
163
|
-
raise "#{rel_attr} can't be nil" unless rel
|
164
|
-
|
165
|
-
# Assumes there's a mcfly :lookup function on
|
166
|
-
# cat_assoc_klass.
|
167
|
-
categorizing_obj = cat_assoc_klass.lookup(ts, rel)
|
168
|
-
raise "no categorization #{cat_assoc_klass} for #{rel}" unless
|
169
|
-
categorizing_obj
|
170
|
-
|
171
|
-
pc = categorizing_obj.send(cat_attr)
|
172
|
-
raise ("#{categorizing_obj} must have assoc." +
|
173
|
-
" #{cat_attr}/#{rel.inspect}") unless pc
|
174
|
-
|
175
|
-
args[lpi] = pc
|
176
|
-
self.send(pc_name, ts, *args)
|
177
|
-
end
|
178
|
-
end
|
179
|
-
|
180
|
-
end
|
181
|
-
end
|
182
|
-
end
|
183
|
-
|
184
|
-
module Mcfly::Controller
|
185
|
-
# define mcfly user to be Flowscape's current_user.
|
186
|
-
def user_for_mcfly
|
187
|
-
find_current_user rescue nil
|
188
|
-
end
|
189
|
-
end
|