From 424341eabc8b5e9e3d6c19ee6c52a45fb20adda4 Mon Sep 17 00:00:00 2001 From: Paul Trowbridge Date: Sat, 3 Mar 2018 10:51:31 -0500 Subject: [PATCH 1/4] initial cut at function to return unmapped --- deploy/ubm_schema.sql | 252 +++++++++++++++++++++++++++++++++ functions/report_unmapped.sql | 0 sample_discovercard/mapping.md | 28 +++- 3 files changed, 279 insertions(+), 1 deletion(-) create mode 100644 functions/report_unmapped.sql diff --git a/deploy/ubm_schema.sql b/deploy/ubm_schema.sql index 8d2b810..4561a33 100644 --- a/deploy/ubm_schema.sql +++ b/deploy/ubm_schema.sql @@ -132,6 +132,258 @@ END; $$; +-- +-- Name: report_unmapped(text); Type: FUNCTION; Schema: tps; Owner: - +-- + +CREATE FUNCTION tps.report_unmapped(_srce text) RETURNS TABLE(source text, map text, ret_val jsonb, count bigint) + LANGUAGE plpgsql + AS $$ +BEGIN + +/* +first get distinct target json values +then apply regex +*/ + +RETURN QUERY +WITH + +--------------------apply regex operations to transactions--------------------------------------------------------------------------------- + +rx AS ( +SELECT + t.srce, + t.id, + t.rec, + m.target, + m.seq, + regex->>'function' regex_function, + e.v ->> 'field' result_key_name, + e.v ->> 'key' target_json_path, + e.v ->> 'flag' regex_options_flag, + e.v->>'map' map_intention, + e.v->>'retain' retain_result, + e.v->>'regex' regex_expression, + e.rn target_item_number, + COALESCE(mt.rn,rp.rn,1) result_number, + mt.mt rx_match, + rp.rp rx_replace, + --------------------------json key name assigned to return value----------------------------------------------------------------------- + CASE e.v->>'map' + WHEN 'y' THEN + e.v->>'field' + ELSE + null + END map_key, + --------------------------json value resulting from regular expression----------------------------------------------------------------- + CASE e.v->>'map' + WHEN 'y' THEN + CASE regex->>'function' + WHEN 'extract' THEN + CASE WHEN array_upper(mt.mt,1)=1 + THEN to_json(mt.mt[1]) + ELSE array_to_json(mt.mt) + END::jsonb + WHEN 'replace' THEN + to_jsonb(rp.rp) + ELSE + '{}'::jsonb + END + ELSE + NULL + END map_val, + --------------------------flag for if retruned regex result is stored as a new part of the final json output--------------------------- + CASE e.v->>'retain' + WHEN 'y' THEN + e.v->>'field' + ELSE + NULL + END retain_key, + --------------------------push regex result into json object--------------------------------------------------------------------------- + CASE e.v->>'retain' + WHEN 'y' THEN + CASE regex->>'function' + WHEN 'extract' THEN + CASE WHEN array_upper(mt.mt,1)=1 + THEN to_json(trim(mt.mt[1])) + ELSE array_to_json(mt.mt) + END::jsonb + WHEN 'replace' THEN + to_jsonb(rtrim(rp.rp)) + ELSE + '{}'::jsonb + END + ELSE + NULL + END retain_val +FROM + --------------------------start with all regex maps------------------------------------------------------------------------------------ + tps.map_rm m + --------------------------isolate matching basis to limit map to only look at certain json--------------------------------------------- + JOIN LATERAL jsonb_array_elements(m.regex->'where') w(v) ON TRUE + --------------------------break out array of regluar expressions in the map------------------------------------------------------------ + JOIN LATERAL jsonb_array_elements(m.regex->'defn') WITH ORDINALITY e(v, rn) ON true + --------------------------join to main transaction table but only certain key/values are included-------------------------------------- + INNER JOIN tps.trans t ON + t.srce = m.srce AND + t.rec @> w.v + --------------------------each regex references a path to the target value, extract the target from the reference and do regex--------- + LEFT JOIN LATERAL regexp_matches(t.rec #>> ((e.v ->> 'key')::text[]), e.v ->> 'regex'::text,COALESCE(e.v ->> 'flag','')) WITH ORDINALITY mt(mt, rn) ON + m.regex->>'function' = 'extract' + --------------------------same as above but for a replacement type function------------------------------------------------------------ + LEFT JOIN LATERAL regexp_replace(t.rec #>> ((e.v ->> 'key')::text[]), e.v ->> 'regex'::text, e.v ->> 'replace'::text,e.v ->> 'flag') WITH ORDINALITY rp(rp, rn) ON + m.regex->>'function' = 'replace' +WHERE + --t.allj IS NULL + t.srce = _srce AND + e.v @> '{"map":"y"}'::jsonb + --rec @> '{"Transaction":"ACH Credits","Transaction":"ACH Debits"}' + --rec @> '{"Description":"CHECK 93013270 086129935"}'::jsonb +/* +ORDER BY + t.id DESC, + m.target, + e.rn, + COALESCE(mt.rn,rp.rn,1) +*/ +) + +--SELECT * FROM rx LIMIT 100 + + +, agg_to_target_items AS ( +SELECT + srce + ,id + ,target + ,seq + ,map_intention + ,regex_function + ,target_item_number + ,result_key_name + ,target_json_path + ,CASE WHEN map_key IS NULL + THEN + NULL + ELSE + jsonb_build_object( + map_key, + CASE WHEN max(result_number) = 1 + THEN + jsonb_agg(map_val ORDER BY result_number) -> 0 + ELSE + jsonb_agg(map_val ORDER BY result_number) + END + ) + END map_val + ,CASE WHEN retain_key IS NULL + THEN + NULL + ELSE + jsonb_build_object( + retain_key, + CASE WHEN max(result_number) = 1 + THEN + jsonb_agg(retain_val ORDER BY result_number) -> 0 + ELSE + jsonb_agg(retain_val ORDER BY result_number) + END + ) + END retain_val +FROM + rx +GROUP BY + srce + ,id + ,target + ,seq + ,map_intention + ,regex_function + ,target_item_number + ,result_key_name + ,target_json_path + ,map_key + ,retain_key +) + +--SELECT * FROM agg_to_target_items LIMIT 100 + + +, agg_to_target AS ( +SELECT + srce + ,id + ,target + ,seq + ,map_intention + ,tps.jsonb_concat_obj(COALESCE(map_val,'{}'::JSONB)) map_val + ,jsonb_strip_nulls(tps.jsonb_concat_obj(COALESCE(retain_val,'{}'::JSONB))) retain_val +FROM + agg_to_target_items +GROUP BY + srce + ,id + ,target + ,seq + ,map_intention +) + + +, agg_to_ret AS ( +SELECT + srce + ,target + ,seq + ,map_intention + ,map_val + ,retain_val + ,count(*) "count" +FROM + agg_to_target +GROUP BY + srce + ,target + ,seq + ,map_intention + ,map_val + ,retain_val +) + +, link_map AS ( +SELECT + a.srce + ,a.target + ,a.seq + ,a.map_intention + ,a.map_val + ,a."count" + ,a.retain_val + ,v.map mapped_val +FROM + agg_to_ret a + LEFT OUTER JOIN tps.map_rv v ON + v.srce = a.srce AND + v.target = a.target AND + v.retval = a.map_val +) +SELECT + l.srce + ,l.target + ,l.map_val + ,l."count" +FROM + link_map l +WHERE + l.mapped_val IS NULL +ORDER BY + l.srce + ,l.target + ,l."count" desc; +END; +$$; + + -- -- Name: srce_import(text, text); Type: FUNCTION; Schema: tps; Owner: - -- diff --git a/functions/report_unmapped.sql b/functions/report_unmapped.sql new file mode 100644 index 0000000..e69de29 diff --git a/sample_discovercard/mapping.md b/sample_discovercard/mapping.md index 4d7fb8b..c48a22f 100644 --- a/sample_discovercard/mapping.md +++ b/sample_discovercard/mapping.md @@ -141,4 +141,30 @@ function call to re-run all the maps for a source SELECT x.message FROM - tps.srce_map_overwrite('DCARD') x(message); \ No newline at end of file + tps.srce_map_overwrite('DCARD') x(message); + + + +mass insert +------------------------------------------------------ + +INSERT INTO + tps.map_rv +SELECT + * +FROM + ( + VALUES + ('DCARD','First 20','{"f20": "DISCOUNT DRUG MART 3"}','{"party":"Discount Drug Mart","reason":"groceries"}'), + ('DCARD','First 20','{"f20": "TARGET STOW OH"}','{"party":"Target","reason":"groceries"}'), + ('DCARD','First 20','{"f20": "WALMART GROCERY 800-"}','{"party":"Walmart","reason":"groceries"}'), + ('DCARD','First 20','{"f20": "CIRCLE K 05416 STOW "}','{"party":"Circle K","reason":"gasoline"}'), + ('DCARD','First 20','{"f20": "TARGET.COM * 800-591"}','{"party":"Target","reason":"home supplies"}'), + ('DCARD','First 20','{"f20": "ACME NO. 17 STOW OH"}','{"party":"Acme","reason":"groceries"}'), + ('DCARD','First 20','{"f20": "AT&T *PAYMENT 800-28"}','{"party":"AT&T","reason":"internet"}'), + ('DCARD','First 20','{"f20": "AUTOZONE #0722 STOW "}','{"party":"Autozone","reason":"auto maint"}'), + ('DCARD','First 20','{"f20": "BESTBUYCOM8055267948"}','{"party":"BestBuy","reason":"home supplies"}'), + ('DCARD','First 20','{"f20": "BUFFALO WILD WINGS K"}','{"party":"Buffalo Wild Wings","reason":"restaurante"}'), + ('DCARD','First 20','{"f20": "CASHBACK BONUS REDEM"}','{"party":"Discover Card","reason":"financing"}'), + ('DCARD','First 20','{"f20": "CLE CLINIC PT PMTS 2"}','{"party":"Cleveland Clinic","reason":"medical"}') + ) x \ No newline at end of file From 19f8c3da3f70e922b8d5dc90ce1e913269832e17 Mon Sep 17 00:00:00 2001 From: Paul Trowbridge Date: Sat, 3 Mar 2018 10:51:45 -0500 Subject: [PATCH 2/4] add function def --- functions/report_unmapped.sql | 254 ++++++++++++++++++++++++++++++++++ 1 file changed, 254 insertions(+) diff --git a/functions/report_unmapped.sql b/functions/report_unmapped.sql index e69de29..5c76b98 100644 --- a/functions/report_unmapped.sql +++ b/functions/report_unmapped.sql @@ -0,0 +1,254 @@ +DROP FUNCTION tps.report_unmapped; +CREATE FUNCTION tps.report_unmapped(_srce text) RETURNS TABLE +( + source text, + map text, + ret_val jsonb, + "count" bigint +) +LANGUAGE plpgsql +AS +$f$ +BEGIN + +/* +first get distinct target json values +then apply regex +*/ + +RETURN QUERY +WITH + +--------------------apply regex operations to transactions--------------------------------------------------------------------------------- + +rx AS ( +SELECT + t.srce, + t.id, + t.rec, + m.target, + m.seq, + regex->>'function' regex_function, + e.v ->> 'field' result_key_name, + e.v ->> 'key' target_json_path, + e.v ->> 'flag' regex_options_flag, + e.v->>'map' map_intention, + e.v->>'retain' retain_result, + e.v->>'regex' regex_expression, + e.rn target_item_number, + COALESCE(mt.rn,rp.rn,1) result_number, + mt.mt rx_match, + rp.rp rx_replace, + --------------------------json key name assigned to return value----------------------------------------------------------------------- + CASE e.v->>'map' + WHEN 'y' THEN + e.v->>'field' + ELSE + null + END map_key, + --------------------------json value resulting from regular expression----------------------------------------------------------------- + CASE e.v->>'map' + WHEN 'y' THEN + CASE regex->>'function' + WHEN 'extract' THEN + CASE WHEN array_upper(mt.mt,1)=1 + THEN to_json(mt.mt[1]) + ELSE array_to_json(mt.mt) + END::jsonb + WHEN 'replace' THEN + to_jsonb(rp.rp) + ELSE + '{}'::jsonb + END + ELSE + NULL + END map_val, + --------------------------flag for if retruned regex result is stored as a new part of the final json output--------------------------- + CASE e.v->>'retain' + WHEN 'y' THEN + e.v->>'field' + ELSE + NULL + END retain_key, + --------------------------push regex result into json object--------------------------------------------------------------------------- + CASE e.v->>'retain' + WHEN 'y' THEN + CASE regex->>'function' + WHEN 'extract' THEN + CASE WHEN array_upper(mt.mt,1)=1 + THEN to_json(trim(mt.mt[1])) + ELSE array_to_json(mt.mt) + END::jsonb + WHEN 'replace' THEN + to_jsonb(rtrim(rp.rp)) + ELSE + '{}'::jsonb + END + ELSE + NULL + END retain_val +FROM + --------------------------start with all regex maps------------------------------------------------------------------------------------ + tps.map_rm m + --------------------------isolate matching basis to limit map to only look at certain json--------------------------------------------- + JOIN LATERAL jsonb_array_elements(m.regex->'where') w(v) ON TRUE + --------------------------break out array of regluar expressions in the map------------------------------------------------------------ + JOIN LATERAL jsonb_array_elements(m.regex->'defn') WITH ORDINALITY e(v, rn) ON true + --------------------------join to main transaction table but only certain key/values are included-------------------------------------- + INNER JOIN tps.trans t ON + t.srce = m.srce AND + t.rec @> w.v + --------------------------each regex references a path to the target value, extract the target from the reference and do regex--------- + LEFT JOIN LATERAL regexp_matches(t.rec #>> ((e.v ->> 'key')::text[]), e.v ->> 'regex'::text,COALESCE(e.v ->> 'flag','')) WITH ORDINALITY mt(mt, rn) ON + m.regex->>'function' = 'extract' + --------------------------same as above but for a replacement type function------------------------------------------------------------ + LEFT JOIN LATERAL regexp_replace(t.rec #>> ((e.v ->> 'key')::text[]), e.v ->> 'regex'::text, e.v ->> 'replace'::text,e.v ->> 'flag') WITH ORDINALITY rp(rp, rn) ON + m.regex->>'function' = 'replace' +WHERE + --t.allj IS NULL + t.srce = _srce AND + e.v @> '{"map":"y"}'::jsonb + --rec @> '{"Transaction":"ACH Credits","Transaction":"ACH Debits"}' + --rec @> '{"Description":"CHECK 93013270 086129935"}'::jsonb +/* +ORDER BY + t.id DESC, + m.target, + e.rn, + COALESCE(mt.rn,rp.rn,1) +*/ +) + +--SELECT * FROM rx LIMIT 100 + + +, agg_to_target_items AS ( +SELECT + srce + ,id + ,target + ,seq + ,map_intention + ,regex_function + ,target_item_number + ,result_key_name + ,target_json_path + ,CASE WHEN map_key IS NULL + THEN + NULL + ELSE + jsonb_build_object( + map_key, + CASE WHEN max(result_number) = 1 + THEN + jsonb_agg(map_val ORDER BY result_number) -> 0 + ELSE + jsonb_agg(map_val ORDER BY result_number) + END + ) + END map_val + ,CASE WHEN retain_key IS NULL + THEN + NULL + ELSE + jsonb_build_object( + retain_key, + CASE WHEN max(result_number) = 1 + THEN + jsonb_agg(retain_val ORDER BY result_number) -> 0 + ELSE + jsonb_agg(retain_val ORDER BY result_number) + END + ) + END retain_val +FROM + rx +GROUP BY + srce + ,id + ,target + ,seq + ,map_intention + ,regex_function + ,target_item_number + ,result_key_name + ,target_json_path + ,map_key + ,retain_key +) + +--SELECT * FROM agg_to_target_items LIMIT 100 + + +, agg_to_target AS ( +SELECT + srce + ,id + ,target + ,seq + ,map_intention + ,tps.jsonb_concat_obj(COALESCE(map_val,'{}'::JSONB)) map_val + ,jsonb_strip_nulls(tps.jsonb_concat_obj(COALESCE(retain_val,'{}'::JSONB))) retain_val +FROM + agg_to_target_items +GROUP BY + srce + ,id + ,target + ,seq + ,map_intention +) + + +, agg_to_ret AS ( +SELECT + srce + ,target + ,seq + ,map_intention + ,map_val + ,retain_val + ,count(*) "count" +FROM + agg_to_target +GROUP BY + srce + ,target + ,seq + ,map_intention + ,map_val + ,retain_val +) + +, link_map AS ( +SELECT + a.srce + ,a.target + ,a.seq + ,a.map_intention + ,a.map_val + ,a."count" + ,a.retain_val + ,v.map mapped_val +FROM + agg_to_ret a + LEFT OUTER JOIN tps.map_rv v ON + v.srce = a.srce AND + v.target = a.target AND + v.retval = a.map_val +) +SELECT + l.srce + ,l.target + ,l.map_val + ,l."count" +FROM + link_map l +WHERE + l.mapped_val IS NULL +ORDER BY + l.srce + ,l.target + ,l."count" desc; +END; +$f$ \ No newline at end of file From 403ff88247750871fb0b8a922fd3ee76554f0b77 Mon Sep 17 00:00:00 2001 From: Paul Trowbridge Date: Sat, 3 Mar 2018 11:11:20 -0500 Subject: [PATCH 3/4] test expanding a single json object to full table --- functions/manual_expand_map_json.sql | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 functions/manual_expand_map_json.sql diff --git a/functions/manual_expand_map_json.sql b/functions/manual_expand_map_json.sql new file mode 100644 index 0000000..8d0710c --- /dev/null +++ b/functions/manual_expand_map_json.sql @@ -0,0 +1,16 @@ +/*--------------------------------------------------------------------------- +turns a single json object into a table suitable for insert to tps.map_rv +this could facilitate a call to a function for inserting many rows from ui +----------------------------------------------------------------------------*/ +WITH j AS ( +select +$$ +[{"source":"DCARD","map":"First 20","ret_val":{"f20": "DISCOUNT DRUG MART 3"},"mapped":{"party":"Discount Drug Mart","reason":"groceries"}},{"source":"DCARD","map":"First 20","ret_val":{"f20": "TARGET STOW OH"},"mapped":{"party":"Target","reason":"groceries"}},{"source":"DCARD","map":"First 20","ret_val":{"f20": "WALMART GROCERY 800-"},"mapped":{"party":"Walmart","reason":"groceries"}},{"source":"DCARD","map":"First 20","ret_val":{"f20": "CIRCLE K 05416 STOW "},"mapped":{"party":"Circle K","reason":"gasoline"}},{"source":"DCARD","map":"First 20","ret_val":{"f20": "TARGET.COM * 800-591"},"mapped":{"party":"Target","reason":"home supplies"}},{"source":"DCARD","map":"First 20","ret_val":{"f20": "ACME NO. 17 STOW OH"},"mapped":{"party":"Acme","reason":"groceries"}},{"source":"DCARD","map":"First 20","ret_val":{"f20": "AT&T *PAYMENT 800-28"},"mapped":{"party":"AT&T","reason":"internet"}},{"source":"DCARD","map":"First 20","ret_val":{"f20": "AUTOZONE #0722 STOW "},"mapped":{"party":"Autozone","reason":"auto maint"}},{"source":"DCARD","map":"First 20","ret_val":{"f20": "BESTBUYCOM8055267948"},"mapped":{"party":"BestBuy","reason":"home supplies"}},{"source":"DCARD","map":"First 20","ret_val":{"f20": "BUFFALO WILD WINGS K"},"mapped":{"party":"Buffalo Wild Wings","reason":"restaurante"}},{"source":"DCARD","map":"First 20","ret_val":{"f20": "CASHBACK BONUS REDEM"},"mapped":{"party":"Discover Card","reason":"financing"}},{"source":"DCARD","map":"First 20","ret_val":{"f20": "CLE CLINIC PT PMTS 2"},"mapped":{"party":"Cleveland Clinic","reason":"medical"}}] +$$::jsonb x +) +SELECT + jtr.* +FROM + j + LEFT JOIN LATERAL jsonb_array_elements(j.x) ae(v) ON TRUE + LEFT JOIN LATERAL jsonb_to_record(ae.v) AS jtr(source text, map text, ret_val jsonb, mapped jsonb) ON TRUE \ No newline at end of file From 506cc5ebf5dbdbc10808e42d03495f9e59e29b36 Mon Sep 17 00:00:00 2001 From: Paul Trowbridge Date: Sat, 3 Mar 2018 11:47:31 -0500 Subject: [PATCH 4/4] create function to insert many updates from one json --- deploy/ubm_schema.sql | 65 +++++++++++++++++++++++++++ functions/srce_map_val_set_multi.sql | 60 +++++++++++++++++++++++++ sample_discovercard/build_maps.xlsm | Bin 0 -> 16638 bytes 3 files changed, 125 insertions(+) create mode 100644 functions/srce_map_val_set_multi.sql create mode 100644 sample_discovercard/build_maps.xlsm diff --git a/deploy/ubm_schema.sql b/deploy/ubm_schema.sql index 4561a33..b7c63d0 100644 --- a/deploy/ubm_schema.sql +++ b/deploy/ubm_schema.sql @@ -1000,6 +1000,71 @@ END $_$; +-- +-- Name: srce_map_val_set_multi(jsonb); Type: FUNCTION; Schema: tps; Owner: - +-- + +CREATE FUNCTION tps.srce_map_val_set_multi(_maps jsonb) RETURNS jsonb + LANGUAGE plpgsql + AS $_$ + +DECLARE + _message jsonb; + _MESSAGE_TEXT text; + _PG_EXCEPTION_DETAIL text; + _PG_EXCEPTION_HINT text; + +BEGIN + + + WITH + -----------expand the json into a table------------------------------------------------------------------------------ + t AS ( + SELECT + jtr.* + FROM + jsonb_array_elements(_maps) ae(v) + JOIN LATERAL jsonb_to_record(ae.v) AS jtr(source text, map text, ret_val jsonb, mapped jsonb) ON TRUE + ) + -----------do merge--------------------------------------------------------------------------------------------------- + INSERT INTO + tps.map_rv + SELECT + t."source" + ,t."map" + ,t.ret_val + ,t.mapped + FROM + t + ON CONFLICT ON CONSTRAINT map_rv_pk DO UPDATE SET + map = excluded.map; + + -------return message-------------------------------------------------------------------------------------------------- + _message:= jsonb_build_object('status','complete'); + RETURN _message; + +EXCEPTION WHEN OTHERS THEN + + GET STACKED DIAGNOSTICS + _MESSAGE_TEXT = MESSAGE_TEXT, + _PG_EXCEPTION_DETAIL = PG_EXCEPTION_DETAIL, + _PG_EXCEPTION_HINT = PG_EXCEPTION_HINT; + _message:= + ($$ + { + "status":"fail", + "message":"error setting map value" + } + $$::jsonb) + ||jsonb_build_object('message_text',_MESSAGE_TEXT) + ||jsonb_build_object('pg_exception_detail',_PG_EXCEPTION_DETAIL); + + RETURN _message; + +END; +$_$; + + -- -- Name: srce_set(text, jsonb); Type: FUNCTION; Schema: tps; Owner: - -- diff --git a/functions/srce_map_val_set_multi.sql b/functions/srce_map_val_set_multi.sql new file mode 100644 index 0000000..edaba13 --- /dev/null +++ b/functions/srce_map_val_set_multi.sql @@ -0,0 +1,60 @@ +DROP FUNCTION tps.srce_map_val_set_multi; +CREATE OR REPLACE FUNCTION tps.srce_map_val_set_multi(_maps jsonb) RETURNS JSONB +LANGUAGE plpgsql +AS $f$ + +DECLARE + _message jsonb; + _MESSAGE_TEXT text; + _PG_EXCEPTION_DETAIL text; + _PG_EXCEPTION_HINT text; + +BEGIN + + + WITH + -----------expand the json into a table------------------------------------------------------------------------------ + t AS ( + SELECT + jtr.* + FROM + jsonb_array_elements(_maps) ae(v) + JOIN LATERAL jsonb_to_record(ae.v) AS jtr(source text, map text, ret_val jsonb, mapped jsonb) ON TRUE + ) + -----------do merge--------------------------------------------------------------------------------------------------- + INSERT INTO + tps.map_rv + SELECT + t."source" + ,t."map" + ,t.ret_val + ,t.mapped + FROM + t + ON CONFLICT ON CONSTRAINT map_rv_pk DO UPDATE SET + map = excluded.map; + + -------return message-------------------------------------------------------------------------------------------------- + _message:= jsonb_build_object('status','complete'); + RETURN _message; + +EXCEPTION WHEN OTHERS THEN + + GET STACKED DIAGNOSTICS + _MESSAGE_TEXT = MESSAGE_TEXT, + _PG_EXCEPTION_DETAIL = PG_EXCEPTION_DETAIL, + _PG_EXCEPTION_HINT = PG_EXCEPTION_HINT; + _message:= + ($$ + { + "status":"fail", + "message":"error setting map value" + } + $$::jsonb) + ||jsonb_build_object('message_text',_MESSAGE_TEXT) + ||jsonb_build_object('pg_exception_detail',_PG_EXCEPTION_DETAIL); + + RETURN _message; + +END; +$f$ \ No newline at end of file diff --git a/sample_discovercard/build_maps.xlsm b/sample_discovercard/build_maps.xlsm new file mode 100644 index 0000000000000000000000000000000000000000..2b5222846fc361297c39877c8b6a1a30dbaa3d91 GIT binary patch literal 16638 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