marty 1.0.44 → 1.0.46
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/Gemfile +3 -0
- data/Gemfile.lock +4 -2
- data/app/controllers/marty/diagnostic_controller.rb +324 -72
- data/app/controllers/marty/rpc_controller.rb +10 -4
- data/app/models/marty/data_grid.rb +47 -95
- data/app/views/marty/diagnostic/op.html.erb +58 -0
- data/config/routes.rb +1 -2
- data/db/js/errinfo_v1.js +16 -0
- data/db/js/lookup_grid_distinct_v1.js +64 -0
- data/db/js/query_grid_dir_v1.js +61 -0
- data/db/migrate/400_create_dg_plv8_v1_fns.rb +12 -0
- data/lib/marty/migrations.rb +20 -0
- data/lib/marty/schema_helper.rb +41 -0
- data/lib/marty/version.rb +1 -1
- data/spec/controllers/diagnostic_controller_spec.rb +157 -67
- data/spec/controllers/rpc_controller_spec.rb +45 -10
- data/spec/dummy/public/{404.html → 400.html} +0 -0
- data/spec/lib/logger_spec.rb +1 -0
- data/spec/models/data_grid_spec.rb +0 -1
- metadata +8 -4
- data/app/views/marty/diagnostic/diagnostic.html.erb +0 -15
@@ -34,6 +34,11 @@ class Marty::RpcController < ActionController::Base
|
|
34
34
|
end
|
35
35
|
end
|
36
36
|
|
37
|
+
def massage_message(msg)
|
38
|
+
m = %r|'#/([^']+)' of type ([^ ]+) matched the disallowed schema|.match(msg)
|
39
|
+
return msg unless m
|
40
|
+
"disallowed parameter '#{m[1]}' of type #{m[2]} was received"
|
41
|
+
end
|
37
42
|
def _get_errors(errs)
|
38
43
|
if errs.is_a?(Array)
|
39
44
|
errs.map { |err| _get_errors(err) }
|
@@ -41,10 +46,11 @@ class Marty::RpcController < ActionController::Base
|
|
41
46
|
if !errs.include?(:failed_attribute)
|
42
47
|
errs.map { |k, v| _get_errors(v) }
|
43
48
|
else
|
44
|
-
fa, message, errors = errs.values_at(:failed_attribute,
|
45
|
-
|
46
|
-
|
47
|
-
|
49
|
+
fa, fragment, message, errors = errs.values_at(:failed_attribute,
|
50
|
+
:fragment,
|
51
|
+
:message, :errors)
|
52
|
+
((['AllOf','AnyOf','Not'].include?(fa) && fragment =='#/') ?
|
53
|
+
[] : [massage_message(message)]) + _get_errors(errors || {})
|
48
54
|
end
|
49
55
|
end
|
50
56
|
end
|
@@ -1,5 +1,4 @@
|
|
1
1
|
class Marty::DataGrid < Marty::Base
|
2
|
-
|
3
2
|
# If data_type is nil, assume float
|
4
3
|
DEFAULT_DATA_TYPE = "float"
|
5
4
|
|
@@ -146,102 +145,52 @@ class Marty::DataGrid < Marty::Base
|
|
146
145
|
data_type.constantize rescue nil
|
147
146
|
end
|
148
147
|
|
149
|
-
def
|
150
|
-
|
151
|
-
|
152
|
-
sqla = infos.map do |inf|
|
153
|
-
type, attr = inf["type"], inf["attr"]
|
154
|
-
|
155
|
-
next unless h.has_key?(attr)
|
156
|
-
|
157
|
-
v = h[attr]
|
158
|
-
|
159
|
-
ix_class = INDEX_MAP[type] || INDEX_MAP["string"]
|
160
|
-
|
161
|
-
q = "key IS NULL"
|
162
|
-
|
163
|
-
unless v.nil?
|
164
|
-
q = case type
|
165
|
-
when "boolean"
|
166
|
-
"key = ?"
|
167
|
-
when "numrange", "int4range"
|
168
|
-
"key @> ?"
|
169
|
-
else # "string", "integer", AR klass
|
170
|
-
"key @> ARRAY[?]"
|
171
|
-
end + " OR #{q}"
|
172
|
-
|
173
|
-
# FIXME: very hacky -- need to cast numrange/intrange values or
|
174
|
-
# we get errors from PG.
|
175
|
-
v = case type
|
176
|
-
when "string"
|
177
|
-
v.to_s
|
178
|
-
when "numrange"
|
179
|
-
v.to_f
|
180
|
-
when "int4range", "integer"
|
181
|
-
v.to_i
|
182
|
-
when "boolean"
|
183
|
-
v
|
184
|
-
else # AR class
|
185
|
-
# FIXME: really hacky to hard-code "name". Used to
|
186
|
-
# perform to_s which could lead ot strange failures when
|
187
|
-
# model had no to_s defined.
|
188
|
-
begin
|
189
|
-
String === v ? v : v.name
|
190
|
-
rescue NoMethodError
|
191
|
-
raise "could not get name for #{v}"
|
192
|
-
end
|
193
|
-
end
|
194
|
-
end
|
195
|
-
|
196
|
-
# FIXME: could potentially order results by key NULLS LAST.
|
197
|
-
# This would prefer more specific rather than wild card
|
198
|
-
# solutions. However, would need to figure out how to preserve
|
199
|
-
# ordering on subsequent INTERSECT operations.
|
200
|
-
ix_class.
|
201
|
-
select(:index).
|
202
|
-
distinct.
|
203
|
-
where(data_grid_id: group_id,
|
204
|
-
created_dt: created_dt,
|
205
|
-
attr: inf["attr"],
|
206
|
-
).
|
207
|
-
where(q, v).to_sql
|
208
|
-
end.compact
|
209
|
-
|
210
|
-
sql = sqla.join(" INTERSECT ")
|
211
|
-
|
212
|
-
self.class.connection.execute(sql).to_a.map { |hh| hh["index"].to_i }
|
148
|
+
def self.clear_dtcache
|
149
|
+
@@dtcache = {}
|
213
150
|
end
|
214
151
|
|
215
|
-
|
216
|
-
isets = ["h", "v"].each_with_object({}) do |dir, ih|
|
217
|
-
infos = dir_infos(dir)
|
218
|
-
|
219
|
-
ih[dir] = query_grid_dir(h, infos)
|
152
|
+
PLV_DT_FMT = "%Y-%m-%d %H:%M:%S.%N6"
|
220
153
|
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
154
|
+
def plv_lookup_grid_distinct(h_passed, ret_grid_data=false, distinct=true)
|
155
|
+
row_info = {"id" => id,
|
156
|
+
"group_id" => group_id,
|
157
|
+
"created_dt" =>
|
158
|
+
(@@dtcache ||= {})[created_dt] ||= created_dt.strftime(PLV_DT_FMT)
|
159
|
+
}
|
160
|
+
h = metadata.each_with_object({}) do |m, h|
|
161
|
+
attr = m["attr"]
|
162
|
+
inc = h_passed.fetch(attr, :__nf__)
|
163
|
+
next if inc == :__nf__
|
164
|
+
h[attr] = (defined? inc.name) ? inc.name : inc
|
229
165
|
end
|
230
166
|
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
167
|
+
fn = "lookup_grid_distinct"
|
168
|
+
hjson = "'#{h.to_json}'::JSONB"
|
169
|
+
rijson = "'#{row_info.to_json}'::JSONB"
|
170
|
+
params = "#{hjson}, #{rijson}, #{ret_grid_data}, #{distinct}"
|
171
|
+
sql = "SELECT #{fn}(#{params})"
|
172
|
+
raw = ActiveRecord::Base.connection.execute(sql)[0][fn]
|
173
|
+
res = JSON.parse(raw)
|
174
|
+
|
175
|
+
if res["error"]
|
176
|
+
msg = res["error"]
|
177
|
+
parms, sqls, ress, dg = res["error_extra"].values_at(
|
178
|
+
"params", "sql",
|
179
|
+
"results", "dg")
|
180
|
+
raise "DG #{name}: Error in PLV8 call: #{msg}\n"\
|
181
|
+
"params: #{parms}\n"\
|
182
|
+
"sqls: #{sqls}\n"\
|
183
|
+
"results: #{ress}\n"\
|
184
|
+
"dg: #{dg}\n"\
|
185
|
+
"ri: #{row_info}" if res["error"]
|
186
|
+
end
|
238
187
|
|
239
|
-
|
240
|
-
|
241
|
-
"
|
242
|
-
"
|
243
|
-
|
244
|
-
|
188
|
+
if ret_grid_data
|
189
|
+
md, mmd = modify_grid(h_passed)
|
190
|
+
res["data"] = md
|
191
|
+
res["metadata"] = mmd
|
192
|
+
end
|
193
|
+
res
|
245
194
|
end
|
246
195
|
|
247
196
|
# FIXME: added for Apollo -- not sure where this belongs given that
|
@@ -252,8 +201,7 @@ class Marty::DataGrid < Marty::Base
|
|
252
201
|
raise "bad DataGrid #{dg}" unless Marty::DataGrid === dg
|
253
202
|
raise "non-hash arg #{h}" unless Hash === h
|
254
203
|
|
255
|
-
res = dg.
|
256
|
-
|
204
|
+
res = dg.plv_lookup_grid_distinct(h, false, distinct)
|
257
205
|
res["result"]
|
258
206
|
end
|
259
207
|
|
@@ -283,7 +231,7 @@ class Marty::DataGrid < Marty::Base
|
|
283
231
|
# "data" => <grid's data array>
|
284
232
|
# "metadata" => <grid's metadata (array of hashes)>
|
285
233
|
|
286
|
-
vhash =
|
234
|
+
vhash = plv_lookup_grid_distinct(h, return_grid_data)
|
287
235
|
|
288
236
|
return vhash if vhash["result"].nil? || !data_type
|
289
237
|
|
@@ -291,7 +239,11 @@ class Marty::DataGrid < Marty::Base
|
|
291
239
|
|
292
240
|
return vhash if String === c_data_type
|
293
241
|
|
294
|
-
|
242
|
+
res = vhash["result"]
|
243
|
+
|
244
|
+
v = Marty::PgEnum === res ?
|
245
|
+
c_data_type.find_by_name(res) :
|
246
|
+
Marty::DataConversion.find_row(c_data_type, {"name" => res}, pt)
|
295
247
|
|
296
248
|
return vhash.merge({"result" => v}) unless (Marty::DataGrid === v && follow)
|
297
249
|
|
@@ -0,0 +1,58 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title><%=Rails.application.class.parent_name%> Diagnostic</title>
|
5
|
+
<style type="text/css">
|
6
|
+
body { margin: auto;
|
7
|
+
margin-bottom: 50px;
|
8
|
+
border: 0;
|
9
|
+
background-color: #fff;
|
10
|
+
color: #333333;
|
11
|
+
font-family: Arial, Helvetica, Verdana, sans-serif;
|
12
|
+
font-weight: normal;
|
13
|
+
font-size: 9pt; }
|
14
|
+
table { border: none;
|
15
|
+
border-collapse: collapse;
|
16
|
+
display: inline-block;
|
17
|
+
margin: 0px 5px 0px 5px;
|
18
|
+
text-align: left;
|
19
|
+
}
|
20
|
+
th { padding: 9px;
|
21
|
+
border: none;
|
22
|
+
background-color: #d6d6d6 }
|
23
|
+
td { padding: 9px;
|
24
|
+
border: none;
|
25
|
+
}
|
26
|
+
th.error { background-color: #ff5555;
|
27
|
+
color: #ffffff;
|
28
|
+
}
|
29
|
+
tr.passed { background-color: #d0e9c6 }
|
30
|
+
tr.failed { background-color: #ff5555;
|
31
|
+
color: #ffffff
|
32
|
+
}
|
33
|
+
td.desc { font-size: 10pt; }
|
34
|
+
h1 { display: block;
|
35
|
+
margin: 0px auto 40px auto;
|
36
|
+
padding: 8px;
|
37
|
+
background-color: #1aaa55;
|
38
|
+
font-size: 18pt;
|
39
|
+
color: #ffffff;
|
40
|
+
line-height: 1.5em; }
|
41
|
+
h2 {text-align: center;}
|
42
|
+
h3.error {color: red;}
|
43
|
+
h3 {text-align: center;}
|
44
|
+
td.overflow { max-width: 350px;
|
45
|
+
overflow: auto; }
|
46
|
+
.wrapper {
|
47
|
+
overflow-x: auto;
|
48
|
+
white-space: nowrap;
|
49
|
+
display: block;}
|
50
|
+
</style>
|
51
|
+
</head>
|
52
|
+
<body>
|
53
|
+
<div style="text-align:center">
|
54
|
+
<h1><%=Rails.application.class.parent_name%> Diagnostic ⛨</h1>
|
55
|
+
<%== @result %>
|
56
|
+
</div>
|
57
|
+
</body>
|
58
|
+
</html>
|
data/config/routes.rb
CHANGED
@@ -2,6 +2,5 @@ Marty::Engine.routes.draw do
|
|
2
2
|
match via: [:get, :post], "rpc/:action(.:format)" => "rpc", as: :rpc
|
3
3
|
get "job/:action" => "job", as: :job
|
4
4
|
match via: [:get, :post], "report(.:format)" => "report#index", as: :report
|
5
|
-
get 'diag
|
6
|
-
get 'diagnostic/(:action)', controller: 'diagnostic'
|
5
|
+
get 'diag', to: 'diagnostic#op'
|
7
6
|
end
|
data/db/js/errinfo_v1.js
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
// PARAM: err JSONB
|
2
|
+
// RETURN: JSONB
|
3
|
+
var locre = /at.*[ (]([a-z][a-z0-9_]*[:][0-9]+)[:][0-9]+/i;
|
4
|
+
var stack = err.stack;
|
5
|
+
var res = '';
|
6
|
+
if (stack) {
|
7
|
+
var lines = stack.split('\\n');
|
8
|
+
for (i=0, len=lines.length; i<len; ++i) {
|
9
|
+
m = locre.exec(lines[i]);
|
10
|
+
if (m) {
|
11
|
+
res += m[1];
|
12
|
+
}
|
13
|
+
}
|
14
|
+
return { "error": `${res} ${err.message}` };
|
15
|
+
}
|
16
|
+
else return { "error": err };
|
@@ -0,0 +1,64 @@
|
|
1
|
+
// PARAM: h JSONB
|
2
|
+
// PARAM: row_info JSONB
|
3
|
+
// PARAM: return_grid_data boolean default false
|
4
|
+
// PARAM: dis boolean default false
|
5
|
+
// RETURN: JSONB
|
6
|
+
var sqls = []
|
7
|
+
var ress = []
|
8
|
+
try {
|
9
|
+
var query_dir = plv8.find_function('query_grid_dir');
|
10
|
+
var errinfo = plv8.find_function('errinfo');
|
11
|
+
var ih = {};
|
12
|
+
var sql = 'SELECT metadata, lenient, name, group_id, data FROM marty_data_grids WHERE id = $1'
|
13
|
+
var dg = plv8.execute(sql, [row_info['id']])[0];
|
14
|
+
var res;
|
15
|
+
['h','v'].forEach(function(dir) {
|
16
|
+
var infos = dg["metadata"].filter(function(md) { return md["dir"] == dir; });
|
17
|
+
if (infos.length == 0)
|
18
|
+
{
|
19
|
+
ih[dir] = [0]
|
20
|
+
return
|
21
|
+
}
|
22
|
+
a = query_dir(h, infos, row_info);
|
23
|
+
sqls.push(a);
|
24
|
+
ih[dir] = []
|
25
|
+
if (a) {
|
26
|
+
res = plv8.execute(a[0], a[1]);
|
27
|
+
ress.push(res);
|
28
|
+
for (var j = 0; j < res.length; j++)
|
29
|
+
{
|
30
|
+
ih[dir].push(res[j]["index"])
|
31
|
+
}
|
32
|
+
}
|
33
|
+
if (dis && ih[dir].length > 1) {
|
34
|
+
throw Error("matches > 1");
|
35
|
+
}
|
36
|
+
});
|
37
|
+
if ((ih["v"].length == 0 || ih["h"].length == 0) &&
|
38
|
+
!dg['lenient'] && !return_grid_data) {
|
39
|
+
throw Error("Data Grid lookup failed");
|
40
|
+
}
|
41
|
+
|
42
|
+
vi = ih["v"].length > 0 ? Math.min.apply(9999, ih["v"]) : null
|
43
|
+
hi = ih["h"].length > 0 ? Math.min.apply(9999, ih["h"]) : null
|
44
|
+
|
45
|
+
var result = null;
|
46
|
+
if (vi!==null && hi!==null) {
|
47
|
+
result = dg["data"][vi][hi];
|
48
|
+
}
|
49
|
+
return { "result" : result,
|
50
|
+
"name" : dg["name"],
|
51
|
+
"data" : return_grid_data ? dg["data"] : null,
|
52
|
+
"metadata" : return_grid_data ? dg["metadata"] : null
|
53
|
+
};
|
54
|
+
} catch (err) {
|
55
|
+
ei = errinfo(err);
|
56
|
+
|
57
|
+
ei["error_extra"] = {}
|
58
|
+
ei["error_extra"]["sql"] = sqls;
|
59
|
+
ei["error_extra"]["results"] = ress;
|
60
|
+
ei["error_extra"]["params"] = h;
|
61
|
+
ei["error_extra"]["dg"] = dg;
|
62
|
+
|
63
|
+
return ei;
|
64
|
+
}
|
@@ -0,0 +1,61 @@
|
|
1
|
+
// PARAM: h JSONB
|
2
|
+
// PARAM: infos JSONB[]
|
3
|
+
// PARAM: row_info JSONB
|
4
|
+
// RETURN: JSONB
|
5
|
+
var getfilter = function(type, idx) {
|
6
|
+
switch(type) {
|
7
|
+
case "boolean":
|
8
|
+
return "key = $" + idx + " OR ";
|
9
|
+
case "numrange":
|
10
|
+
return "key @> $" + idx + "::numeric OR ";
|
11
|
+
case "int4range":
|
12
|
+
return "key @> $" + idx + "::integer OR ";
|
13
|
+
case "integer":
|
14
|
+
return "key @> ARRAY[$" + idx + "::integer] OR ";
|
15
|
+
default:
|
16
|
+
return "key @> ARRAY[$" + idx + "::text] OR ";
|
17
|
+
}
|
18
|
+
}
|
19
|
+
|
20
|
+
var temp = [];
|
21
|
+
var args = [];
|
22
|
+
|
23
|
+
var sql;
|
24
|
+
for (var sqlidx=1, i = 0; i < infos.length; i++) {
|
25
|
+
var type = infos[i]["type"];
|
26
|
+
var attr = infos[i]["attr"];
|
27
|
+
var v = h[attr];
|
28
|
+
if (!h.hasOwnProperty(attr)) {
|
29
|
+
//throw Error(`missing attr ${attr}`)
|
30
|
+
continue;
|
31
|
+
}
|
32
|
+
switch (type) {
|
33
|
+
case 'boolean':
|
34
|
+
case "numrange":
|
35
|
+
case "int4range":
|
36
|
+
case "integer":
|
37
|
+
tab = `marty_grid_index_${type}s`;
|
38
|
+
break;
|
39
|
+
default:
|
40
|
+
tab = 'marty_grid_index_strings';
|
41
|
+
};
|
42
|
+
|
43
|
+
sql = `SELECT DISTINCT index from ${tab} ` +
|
44
|
+
"WHERE data_grid_id = $" + sqlidx++ +
|
45
|
+
" AND created_dt = $" + sqlidx++ +
|
46
|
+
" AND attr = $" + sqlidx++ + ' ';
|
47
|
+
|
48
|
+
args.push(row_info["group_id"]);
|
49
|
+
args.push(row_info["created_dt"]);
|
50
|
+
args.push(attr);
|
51
|
+
|
52
|
+
if (v!==null) {
|
53
|
+
filt = getfilter(type, sqlidx++);
|
54
|
+
args.push(v);
|
55
|
+
} else filt = ''
|
56
|
+
sql += ' AND (' + filt + "key is NULL) ";
|
57
|
+
|
58
|
+
temp.push(sql);
|
59
|
+
}
|
60
|
+
if (temp ==[]) return null;
|
61
|
+
return [temp.join(" INTERSECT "), args];
|
@@ -0,0 +1,12 @@
|
|
1
|
+
class CreateDgPlv8V1Fns < ActiveRecord::Migration
|
2
|
+
def change
|
3
|
+
connection.execute <<-SQL
|
4
|
+
-- required to utilize plv8 extension
|
5
|
+
CREATE EXTENSION IF NOT EXISTS plv8;
|
6
|
+
SQL
|
7
|
+
marty_path = Gem.loaded_specs["marty"].full_gem_path
|
8
|
+
Dir.glob("#{marty_path}/db/js/*_v1.js") do |f|
|
9
|
+
connection.execute(Marty::Migrations.get_plv8_migration(f))
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
data/lib/marty/migrations.rb
CHANGED
@@ -303,4 +303,24 @@ OUT
|
|
303
303
|
"cols = #{cols}, act_cols = #{act_cols}")
|
304
304
|
end.map(&:to_sym)
|
305
305
|
end
|
306
|
+
|
307
|
+
def self.get_plv8_migration(file)
|
308
|
+
fnname = %r(/([^/]+)_v[0-9]+\.js\z).match(file)[1]
|
309
|
+
lines=File.readlines(file)
|
310
|
+
parts = lines.map do |line|
|
311
|
+
next [:param, $1] if %r(\A// PARAM[:] (.*)$).match(line)
|
312
|
+
next [:ret, $1] if %r(\A// RETURN[:] (.*)$).match(line)
|
313
|
+
[:body, line]
|
314
|
+
end.group_by(&:first)
|
315
|
+
args = parts[:param].map{ |(_,l)| l}.join(",\n")
|
316
|
+
ret = parts[:ret][0][1]
|
317
|
+
body = parts[:body].map{ |(_,l)| l}.join
|
318
|
+
<<EOT
|
319
|
+
CREATE OR REPLACE FUNCTION #{fnname} (
|
320
|
+
#{args}
|
321
|
+
) RETURNS #{ret} AS $$
|
322
|
+
#{body}
|
323
|
+
$$ LANGUAGE plv8;
|
324
|
+
EOT
|
325
|
+
end
|
306
326
|
end
|