Skip to content

Commit 7c78a28

Browse files
committed
readme
1 parent 28ee30f commit 7c78a28

2 files changed

Lines changed: 343 additions & 13 deletions

File tree

packages/plpgsql-deparser/README.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,26 @@
11
# plpgsql-deparser
22

3+
<p align="center" width="100%">
4+
<img height="250" src="https://raw.githubusercontent.com/constructive-io/constructive/refs/heads/main/assets/outline-logo.svg" />
5+
</p>
6+
7+
<p align="center" width="100%">
8+
<a href="https://github.com/constructive-io/pgsql-parser/actions/workflows/run-tests.yaml">
9+
<img height="20" src="https://github.com/constructive-io/pgsql-parser/actions/workflows/run-tests.yaml/badge.svg" />
10+
</a>
11+
<a href="https://www.npmjs.com/package/plpgsql-deparser"><img height="20" src="https://img.shields.io/npm/dt/plpgsql-deparser"></a>
12+
<a href="https://www.npmjs.com/package/plpgsql-deparser"><img height="20" src="https://img.shields.io/npm/dw/plpgsql-deparser"/></a>
13+
<a href="https://github.com/constructive-io/pgsql-parser/blob/main/LICENSE-MIT"><img height="20" src="https://img.shields.io/badge/license-MIT-blue.svg"/></a>
14+
<a href="https://www.npmjs.com/package/plpgsql-deparser"><img height="20" src="https://img.shields.io/github/package-json/v/constructive-io/pgsql-parser?filename=packages%2Fplpgsql-deparser%2Fpackage.json"/></a>
15+
</p>
16+
317
PL/pgSQL AST Deparser - Converts PL/pgSQL function ASTs back to SQL strings.
418

19+
> **⚠️ Experimental:** This package is currently experimental. If you're looking for SQL deparsing (not PL/pgSQL), see [`pgsql-deparser`](https://www.npmjs.com/package/pgsql-deparser).
20+
521
## Overview
622

7-
This package provides a deparser for PL/pgSQL (PostgreSQL's procedural language) AST structures. It works with the AST output from `parsePlPgSQL` function in `@libpg-query/parser` (or `libpg-query-full`).
23+
This package provides a deparser for PL/pgSQL (PostgreSQL's procedural language) AST structures. It works with the AST output from `parsePlPgSQL` function in `@libpg-query/parser`.
824

925
The PL/pgSQL AST is different from the regular SQL AST - it represents the internal structure of PL/pgSQL function bodies, including:
1026

packages/plpgsql-deparser/__tests__/pretty/__snapshots__/plpgsql-pretty.test.ts.snap

Lines changed: 326 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,171 @@
11
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
22

33
exports[`lowercase: big-function.sql 1`] = `
4-
"begin
5-
if val > 100 then
6-
return 'large';
7-
elsif val > 10 then
8-
return 'medium';
4+
"declare
5+
v_orders_scanned int := 0;
6+
v_orders_upserted int := 0;
7+
v_gross numeric := 0;
8+
v_discount numeric := 0;
9+
v_tax numeric := 0;
10+
v_net numeric := 0;
11+
v_avg numeric := 0;
12+
v_top_sku text := NULL;
13+
v_top_sku_qty bigint := 0;
14+
v_now timestamptz := clock_timestamp();
15+
v_jitter numeric := (random() - 0.5) * 0.02;
16+
v_discount_rate numeric := GREATEST(LEAST(p_discount_rate, 0.50), 0);
17+
v_tax_rate numeric := GREATEST(LEAST(p_tax_rate, 0.30), 0);
18+
v_min_total numeric := COALESCE(p_min_total, 0);
19+
v_sql text;
20+
v_rowcount int := 0;
21+
v_lock_key bigint := ('x' || substr(md5(p_org_id::text), 1, 16))::bit(64)::bigint;
22+
sqlstate constant text;
23+
sqlerrm constant text;
24+
begin
25+
begin
26+
if p_org_id IS NULL OR p_user_id IS NULL then
27+
raise exception 'p_org_id and p_user_id are required';
28+
end if;
29+
if p_from_ts > p_to_ts then
30+
raise exception 'p_from_ts (%) must be <= p_to_ts (%)', p_from_ts, p_to_ts;
31+
end if;
32+
if p_max_rows < 1 OR p_max_rows > 10000 then
33+
raise exception 'p_max_rows out of range: %', p_max_rows;
34+
end if;
35+
if p_round_to < 0 OR p_round_to > 6 then
36+
raise exception 'p_round_to out of range: %', p_round_to;
37+
end if;
38+
if p_lock then
39+
perform SELECT pg_advisory_xact_lock(v_lock_key);
40+
end if;
41+
if p_debug then
42+
raise notice 'big_kitchen_sink start=% org=% user=% from=% to=% min_total=%', v_now, p_org_id, p_user_id, p_from_ts, p_to_ts, v_min_total;
43+
end if;
44+
WITH base AS (
45+
SELECT
46+
o.id,
47+
o.total_amount::numeric AS total_amount,
48+
o.currency,
49+
o.created_at
50+
FROM app_public.app_order o
51+
WHERE o.org_id = p_org_id
52+
AND o.user_id = p_user_id
53+
AND o.created_at >= p_from_ts
54+
AND o.created_at < p_to_ts
55+
AND o.total_amount::numeric >= v_min_total
56+
AND o.currency = p_currency
57+
ORDER BY o.created_at DESC
58+
LIMIT p_max_rows
59+
),
60+
totals AS (
61+
SELECT
62+
count(*)::int AS orders_scanned,
63+
COALESCE(sum(total_amount), 0) AS gross_total,
64+
COALESCE(avg(total_amount), 0) AS avg_total
65+
FROM base
66+
)
67+
SELECT
68+
t.orders_scanned,
69+
t.gross_total,
70+
t.avg_total
71+
FROM totals t;
72+
if p_apply_discount then
73+
v_discount := round(v_gross * GREATEST(LEAST(v_discount_rate + v_jitter, 0.50), 0), p_round_to);
974
else
10-
return 'small';
75+
v_discount := 0;
76+
end if;
77+
v_tax := round(GREATEST(v_gross - v_discount, 0) * v_tax_rate, p_round_to);
78+
v_net := round((v_gross - v_discount + v_tax) * power(10::numeric, 0), p_round_to);
79+
SELECT
80+
oi.sku,
81+
sum(oi.quantity)::bigint AS qty
82+
FROM app_public.order_item oi
83+
JOIN app_public.app_order o ON o.id = oi.order_id
84+
WHERE o.org_id = p_org_id
85+
AND o.user_id = p_user_id
86+
AND o.created_at >= p_from_ts
87+
AND o.created_at < p_to_ts
88+
AND o.currency = p_currency
89+
GROUP BY oi.sku
90+
ORDER BY qty DESC, oi.sku ASC
91+
LIMIT 1;
92+
INSERT INTO app_public.order_rollup (
93+
org_id,
94+
user_id,
95+
period_from,
96+
period_to,
97+
currency,
98+
orders_scanned,
99+
gross_total,
100+
discount_total,
101+
tax_total,
102+
net_total,
103+
avg_order_total,
104+
top_sku,
105+
top_sku_qty,
106+
note,
107+
updated_at
108+
)
109+
VALUES (
110+
p_org_id,
111+
p_user_id,
112+
p_from_ts,
113+
p_to_ts,
114+
p_currency,
115+
v_orders_scanned,
116+
v_gross,
117+
v_discount,
118+
v_tax,
119+
v_net,
120+
v_avg,
121+
v_top_sku,
122+
v_top_sku_qty,
123+
p_note,
124+
now()
125+
)
126+
ON CONFLICT (org_id, user_id, period_from, period_to, currency)
127+
DO UPDATE SET
128+
orders_scanned = EXCLUDED.orders_scanned,
129+
gross_total = EXCLUDED.gross_total,
130+
discount_total = EXCLUDED.discount_total,
131+
tax_total = EXCLUDED.tax_total,
132+
net_total = EXCLUDED.net_total,
133+
avg_order_total = EXCLUDED.avg_order_total,
134+
top_sku = EXCLUDED.top_sku,
135+
top_sku_qty = EXCLUDED.top_sku_qty,
136+
note = COALESCE(EXCLUDED.note, app_public.order_rollup.note),
137+
updated_at = now();
138+
get diagnostics v_rowcount = ;
139+
v_orders_upserted := v_rowcount;
140+
v_sql := format(
141+
'SELECT count(*)::int FROM %I.%I WHERE org_id = $1 AND created_at >= $2 AND created_at < $3',
142+
'app_public',
143+
'app_order'
144+
);
145+
execute v_sql into (unnamed row) using p_org_id, p_from_ts, p_to_ts;
146+
if p_debug then
147+
raise notice 'dynamic count(app_order)=%', v_rowcount;
11148
end if;
149+
org_id := p_org_id;
150+
user_id := p_user_id;
151+
period_from := p_from_ts;
152+
period_to := p_to_ts;
153+
orders_scanned := v_orders_scanned;
154+
orders_upserted := v_orders_upserted;
155+
gross_total := v_gross;
156+
discount_total := v_discount;
157+
tax_total := v_tax;
158+
net_total := v_net;
159+
avg_order_total := round(v_avg, p_round_to);
160+
top_sku := v_top_sku;
161+
top_sku_qty := v_top_sku_qty;
162+
message := format(
163+
'rollup ok: gross=%s discount=%s tax=%s net=%s (discount_rate=%s tax_rate=%s)',
164+
v_gross, v_discount, v_tax, v_net, v_discount_rate, v_tax_rate
165+
);
166+
return next;
167+
return;
168+
end;
12169
return;
13170
end"
14171
`;
@@ -45,14 +202,171 @@ end"
45202
`;
46203
47204
exports[`uppercase: big-function.sql 1`] = `
48-
"BEGIN
49-
IF val > 100 THEN
50-
RETURN 'large';
51-
ELSIF val > 10 THEN
52-
RETURN 'medium';
205+
"DECLARE
206+
v_orders_scanned int := 0;
207+
v_orders_upserted int := 0;
208+
v_gross numeric := 0;
209+
v_discount numeric := 0;
210+
v_tax numeric := 0;
211+
v_net numeric := 0;
212+
v_avg numeric := 0;
213+
v_top_sku text := NULL;
214+
v_top_sku_qty bigint := 0;
215+
v_now timestamptz := clock_timestamp();
216+
v_jitter numeric := (random() - 0.5) * 0.02;
217+
v_discount_rate numeric := GREATEST(LEAST(p_discount_rate, 0.50), 0);
218+
v_tax_rate numeric := GREATEST(LEAST(p_tax_rate, 0.30), 0);
219+
v_min_total numeric := COALESCE(p_min_total, 0);
220+
v_sql text;
221+
v_rowcount int := 0;
222+
v_lock_key bigint := ('x' || substr(md5(p_org_id::text), 1, 16))::bit(64)::bigint;
223+
sqlstate CONSTANT text;
224+
sqlerrm CONSTANT text;
225+
BEGIN
226+
BEGIN
227+
IF p_org_id IS NULL OR p_user_id IS NULL THEN
228+
RAISE EXCEPTION 'p_org_id and p_user_id are required';
229+
END IF;
230+
IF p_from_ts > p_to_ts THEN
231+
RAISE EXCEPTION 'p_from_ts (%) must be <= p_to_ts (%)', p_from_ts, p_to_ts;
232+
END IF;
233+
IF p_max_rows < 1 OR p_max_rows > 10000 THEN
234+
RAISE EXCEPTION 'p_max_rows out of range: %', p_max_rows;
235+
END IF;
236+
IF p_round_to < 0 OR p_round_to > 6 THEN
237+
RAISE EXCEPTION 'p_round_to out of range: %', p_round_to;
238+
END IF;
239+
IF p_lock THEN
240+
PERFORM SELECT pg_advisory_xact_lock(v_lock_key);
241+
END IF;
242+
IF p_debug THEN
243+
RAISE NOTICE 'big_kitchen_sink start=% org=% user=% from=% to=% min_total=%', v_now, p_org_id, p_user_id, p_from_ts, p_to_ts, v_min_total;
244+
END IF;
245+
WITH base AS (
246+
SELECT
247+
o.id,
248+
o.total_amount::numeric AS total_amount,
249+
o.currency,
250+
o.created_at
251+
FROM app_public.app_order o
252+
WHERE o.org_id = p_org_id
253+
AND o.user_id = p_user_id
254+
AND o.created_at >= p_from_ts
255+
AND o.created_at < p_to_ts
256+
AND o.total_amount::numeric >= v_min_total
257+
AND o.currency = p_currency
258+
ORDER BY o.created_at DESC
259+
LIMIT p_max_rows
260+
),
261+
totals AS (
262+
SELECT
263+
count(*)::int AS orders_scanned,
264+
COALESCE(sum(total_amount), 0) AS gross_total,
265+
COALESCE(avg(total_amount), 0) AS avg_total
266+
FROM base
267+
)
268+
SELECT
269+
t.orders_scanned,
270+
t.gross_total,
271+
t.avg_total
272+
FROM totals t;
273+
IF p_apply_discount THEN
274+
v_discount := round(v_gross * GREATEST(LEAST(v_discount_rate + v_jitter, 0.50), 0), p_round_to);
53275
ELSE
54-
RETURN 'small';
276+
v_discount := 0;
277+
END IF;
278+
v_tax := round(GREATEST(v_gross - v_discount, 0) * v_tax_rate, p_round_to);
279+
v_net := round((v_gross - v_discount + v_tax) * power(10::numeric, 0), p_round_to);
280+
SELECT
281+
oi.sku,
282+
sum(oi.quantity)::bigint AS qty
283+
FROM app_public.order_item oi
284+
JOIN app_public.app_order o ON o.id = oi.order_id
285+
WHERE o.org_id = p_org_id
286+
AND o.user_id = p_user_id
287+
AND o.created_at >= p_from_ts
288+
AND o.created_at < p_to_ts
289+
AND o.currency = p_currency
290+
GROUP BY oi.sku
291+
ORDER BY qty DESC, oi.sku ASC
292+
LIMIT 1;
293+
INSERT INTO app_public.order_rollup (
294+
org_id,
295+
user_id,
296+
period_from,
297+
period_to,
298+
currency,
299+
orders_scanned,
300+
gross_total,
301+
discount_total,
302+
tax_total,
303+
net_total,
304+
avg_order_total,
305+
top_sku,
306+
top_sku_qty,
307+
note,
308+
updated_at
309+
)
310+
VALUES (
311+
p_org_id,
312+
p_user_id,
313+
p_from_ts,
314+
p_to_ts,
315+
p_currency,
316+
v_orders_scanned,
317+
v_gross,
318+
v_discount,
319+
v_tax,
320+
v_net,
321+
v_avg,
322+
v_top_sku,
323+
v_top_sku_qty,
324+
p_note,
325+
now()
326+
)
327+
ON CONFLICT (org_id, user_id, period_from, period_to, currency)
328+
DO UPDATE SET
329+
orders_scanned = EXCLUDED.orders_scanned,
330+
gross_total = EXCLUDED.gross_total,
331+
discount_total = EXCLUDED.discount_total,
332+
tax_total = EXCLUDED.tax_total,
333+
net_total = EXCLUDED.net_total,
334+
avg_order_total = EXCLUDED.avg_order_total,
335+
top_sku = EXCLUDED.top_sku,
336+
top_sku_qty = EXCLUDED.top_sku_qty,
337+
note = COALESCE(EXCLUDED.note, app_public.order_rollup.note),
338+
updated_at = now();
339+
GET DIAGNOSTICS v_rowcount = ;
340+
v_orders_upserted := v_rowcount;
341+
v_sql := format(
342+
'SELECT count(*)::int FROM %I.%I WHERE org_id = $1 AND created_at >= $2 AND created_at < $3',
343+
'app_public',
344+
'app_order'
345+
);
346+
EXECUTE v_sql INTO (unnamed row) USING p_org_id, p_from_ts, p_to_ts;
347+
IF p_debug THEN
348+
RAISE NOTICE 'dynamic count(app_order)=%', v_rowcount;
55349
END IF;
350+
org_id := p_org_id;
351+
user_id := p_user_id;
352+
period_from := p_from_ts;
353+
period_to := p_to_ts;
354+
orders_scanned := v_orders_scanned;
355+
orders_upserted := v_orders_upserted;
356+
gross_total := v_gross;
357+
discount_total := v_discount;
358+
tax_total := v_tax;
359+
net_total := v_net;
360+
avg_order_total := round(v_avg, p_round_to);
361+
top_sku := v_top_sku;
362+
top_sku_qty := v_top_sku_qty;
363+
message := format(
364+
'rollup ok: gross=%s discount=%s tax=%s net=%s (discount_rate=%s tax_rate=%s)',
365+
v_gross, v_discount, v_tax, v_net, v_discount_rate, v_tax_rate
366+
);
367+
RETURN NEXT;
368+
RETURN;
369+
END;
56370
RETURN;
57371
END"
58372
`;

0 commit comments

Comments
 (0)