From 36dd819c39fb193fbc90797bba7d144beae0bdbe Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Sat, 3 Jan 2026 02:38:31 +0000 Subject: [PATCH] fix(plpgsql-deparser): indent all lines of multi-line statements Previously, the indent() function only indented the first line of text. This caused incorrect formatting for nested IF/WHILE/LOOP blocks where subsequent lines would not be properly indented. The fix uses text.replace(/\n/g, '\n' + indent) to ensure all lines are indented, matching the behavior of the SQL implementation in constructive-db. --- .../__snapshots__/hydrate-demo.test.ts.snap | 288 ++++----- .../__snapshots__/plpgsql-pretty.test.ts.snap | 604 +++++++++--------- .../plpgsql-deparser/src/plpgsql-deparser.ts | 5 +- 3 files changed, 449 insertions(+), 448 deletions(-) diff --git a/packages/plpgsql-deparser/__tests__/__snapshots__/hydrate-demo.test.ts.snap b/packages/plpgsql-deparser/__tests__/__snapshots__/hydrate-demo.test.ts.snap index 8fc18103..a2e36417 100644 --- a/packages/plpgsql-deparser/__tests__/__snapshots__/hydrate-demo.test.ts.snap +++ b/packages/plpgsql-deparser/__tests__/__snapshots__/hydrate-demo.test.ts.snap @@ -53,150 +53,150 @@ exports[`hydrate demonstration with big-function.sql should parse, hydrate, modi sqlerrm CONSTANT text; BEGIN BEGIN - IF p_org_id IS NULL OR p_user_id IS NULL THEN - RAISE EXCEPTION 'p_org_id and p_user_id are required'; -END IF; - IF p_from_ts > p_to_ts THEN - RAISE EXCEPTION 'p_from_ts (%) must be <= p_to_ts (%)', p_from_ts, p_to_ts; -END IF; - IF p_max_rows < 1 OR p_max_rows > 10000 THEN - RAISE EXCEPTION 'p_max_rows out of range: %', p_max_rows; -END IF; - IF p_round_to < 0 OR p_round_to > 6 THEN - RAISE EXCEPTION 'p_round_to out of range: %', p_round_to; -END IF; - IF p_lock THEN - PERFORM SELECT pg_advisory_xact_lock(v_lock_key); -END IF; - IF p_debug THEN - 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; -END IF; - WITH - base AS (SELECT - o.id, - o.total_amount::numeric AS total_amount, - o.currency, - o.created_at - FROM app_public.app_order AS o - WHERE - o.org_id = p_org_id - AND o.user_id = p_user_id - AND o.created_at >= p_from_ts - AND o.created_at < p_to_ts - AND o.total_amount::numeric >= v_min_total - AND o.currency = p_currency - ORDER BY - o.created_at DESC - LIMIT p_max_rows), - totals AS (SELECT - (count(*))::int AS orders_scanned, - COALESCE(sum(total_amount), 0) AS gross_total, - COALESCE(avg(total_amount), 0) AS avg_total - FROM base) -SELECT - t.orders_scanned, - t.gross_total, - t.avg_total -FROM totals AS t; - IF p_apply_discount THEN - v_rebate := round(v_gross * GREATEST(LEAST(v_discount_rate + v_jitter, 0.50), 0), p_round_to); -ELSE - v_discount := 0; -END IF; - v_levy := round(GREATEST(v_gross - v_discount, 0) * v_tax_rate, p_round_to); - v_net := round((v_gross - v_discount + v_tax) * power(10::numeric, 0), p_round_to); - SELECT - oi.sku, - CAST(sum(oi.quantity) AS bigint) AS qty -FROM app_public.order_item AS oi -JOIN app_public.app_order AS o ON o.id = oi.order_id -WHERE - o.org_id = p_org_id - AND o.user_id = p_user_id - AND o.created_at >= p_from_ts - AND o.created_at < p_to_ts - AND o.currency = p_currency -GROUP BY - oi.sku -ORDER BY - qty DESC, - oi.sku ASC -LIMIT 1; - INSERT INTO app_public.order_rollup ( - org_id, - user_id, - period_from, - period_to, - currency, - orders_scanned, - gross_total, - discount_total, - tax_total, - net_total, - avg_order_total, - top_sku, - top_sku_qty, - note, - updated_at -) VALUES - ( - p_org_id, - p_user_id, - p_from_ts, - p_to_ts, - p_currency, - v_orders_scanned, - v_gross, - v_discount, - v_tax, - v_net, - v_avg, - v_top_sku, - v_top_sku_qty, - p_note, - now() - ) ON CONFLICT (org_id, user_id, period_from, period_to, currency) DO UPDATE SET - orders_scanned = excluded.orders_scanned, - gross_total = excluded.gross_total, - discount_total = excluded.discount_total, - tax_total = excluded.tax_total, - net_total = excluded.net_total, - avg_order_total = excluded.avg_order_total, - top_sku = excluded.top_sku, - top_sku_qty = excluded.top_sku_qty, - note = COALESCE(excluded.note, app_public.order_rollup.note), - updated_at = now(); - GET DIAGNOSTICS v_rowcount = ; - v_orders_upserted := v_rowcount; - v_sql := format( - 'SELECT count(*)::int FROM %I.%I WHERE org_id = $1 AND created_at >= $2 AND created_at < $3', - 'app_public', - 'app_order' - ); - EXECUTE v_sql INTO (unnamed row) USING p_org_id, p_from_ts, p_to_ts; - IF p_debug THEN - RAISE NOTICE 'dynamic count(app_order)=%', v_rowcount; -END IF; - org_id := p_org_id; - user_id := p_user_id; - period_from := p_from_ts; - period_to := p_to_ts; - orders_scanned := v_orders_scanned; - orders_upserted := v_orders_upserted; - gross_total := v_gross; - discount_total := v_discount; - tax_total := v_tax; - net_total := v_net; - avg_order_total := round(v_avg, p_round_to); - top_sku := v_top_sku; - top_sku_qty := v_top_sku_qty; - message := format( - 'rollup ok: gross=%s discount=%s tax=%s net=%s (discount_rate=%s tax_rate=%s)', - v_gross, v_discount, v_tax, v_net, v_discount_rate, v_tax_rate - ); - RETURN NEXT; - RETURN; -END; + IF p_org_id IS NULL OR p_user_id IS NULL THEN + RAISE EXCEPTION 'p_org_id and p_user_id are required'; + END IF; + IF p_from_ts > p_to_ts THEN + RAISE EXCEPTION 'p_from_ts (%) must be <= p_to_ts (%)', p_from_ts, p_to_ts; + END IF; + IF p_max_rows < 1 OR p_max_rows > 10000 THEN + RAISE EXCEPTION 'p_max_rows out of range: %', p_max_rows; + END IF; + IF p_round_to < 0 OR p_round_to > 6 THEN + RAISE EXCEPTION 'p_round_to out of range: %', p_round_to; + END IF; + IF p_lock THEN + PERFORM SELECT pg_advisory_xact_lock(v_lock_key); + END IF; + IF p_debug THEN + 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; + END IF; + WITH + base AS (SELECT + o.id, + o.total_amount::numeric AS total_amount, + o.currency, + o.created_at + FROM app_public.app_order AS o + WHERE + o.org_id = p_org_id + AND o.user_id = p_user_id + AND o.created_at >= p_from_ts + AND o.created_at < p_to_ts + AND o.total_amount::numeric >= v_min_total + AND o.currency = p_currency + ORDER BY + o.created_at DESC + LIMIT p_max_rows), + totals AS (SELECT + (count(*))::int AS orders_scanned, + COALESCE(sum(total_amount), 0) AS gross_total, + COALESCE(avg(total_amount), 0) AS avg_total + FROM base) + SELECT + t.orders_scanned, + t.gross_total, + t.avg_total + FROM totals AS t; + IF p_apply_discount THEN + v_rebate := round(v_gross * GREATEST(LEAST(v_discount_rate + v_jitter, 0.50), 0), p_round_to); + ELSE + v_discount := 0; + END IF; + v_levy := round(GREATEST(v_gross - v_discount, 0) * v_tax_rate, p_round_to); + v_net := round((v_gross - v_discount + v_tax) * power(10::numeric, 0), p_round_to); + SELECT + oi.sku, + CAST(sum(oi.quantity) AS bigint) AS qty + FROM app_public.order_item AS oi + JOIN app_public.app_order AS o ON o.id = oi.order_id + WHERE + o.org_id = p_org_id + AND o.user_id = p_user_id + AND o.created_at >= p_from_ts + AND o.created_at < p_to_ts + AND o.currency = p_currency + GROUP BY + oi.sku + ORDER BY + qty DESC, + oi.sku ASC + LIMIT 1; + INSERT INTO app_public.order_rollup ( + org_id, + user_id, + period_from, + period_to, + currency, + orders_scanned, + gross_total, + discount_total, + tax_total, + net_total, + avg_order_total, + top_sku, + top_sku_qty, + note, + updated_at + ) VALUES + ( + p_org_id, + p_user_id, + p_from_ts, + p_to_ts, + p_currency, + v_orders_scanned, + v_gross, + v_discount, + v_tax, + v_net, + v_avg, + v_top_sku, + v_top_sku_qty, + p_note, + now() + ) ON CONFLICT (org_id, user_id, period_from, period_to, currency) DO UPDATE SET + orders_scanned = excluded.orders_scanned, + gross_total = excluded.gross_total, + discount_total = excluded.discount_total, + tax_total = excluded.tax_total, + net_total = excluded.net_total, + avg_order_total = excluded.avg_order_total, + top_sku = excluded.top_sku, + top_sku_qty = excluded.top_sku_qty, + note = COALESCE(excluded.note, app_public.order_rollup.note), + updated_at = now(); + GET DIAGNOSTICS v_rowcount = ; + v_orders_upserted := v_rowcount; + v_sql := format( + 'SELECT count(*)::int FROM %I.%I WHERE org_id = $1 AND created_at >= $2 AND created_at < $3', + 'app_public', + 'app_order' + ); + EXECUTE v_sql INTO (unnamed row) USING p_org_id, p_from_ts, p_to_ts; + IF p_debug THEN + RAISE NOTICE 'dynamic count(app_order)=%', v_rowcount; + END IF; + org_id := p_org_id; + user_id := p_user_id; + period_from := p_from_ts; + period_to := p_to_ts; + orders_scanned := v_orders_scanned; + orders_upserted := v_orders_upserted; + gross_total := v_gross; + discount_total := v_discount; + tax_total := v_tax; + net_total := v_net; + avg_order_total := round(v_avg, p_round_to); + top_sku := v_top_sku; + top_sku_qty := v_top_sku_qty; + message := format( + 'rollup ok: gross=%s discount=%s tax=%s net=%s (discount_rate=%s tax_rate=%s)', + v_gross, v_discount, v_tax, v_net, v_discount_rate, v_tax_rate + ); + RETURN NEXT; + RETURN; + END; RETURN; END$$" `; diff --git a/packages/plpgsql-deparser/__tests__/pretty/__snapshots__/plpgsql-pretty.test.ts.snap b/packages/plpgsql-deparser/__tests__/pretty/__snapshots__/plpgsql-pretty.test.ts.snap index 44c0941c..f6810cc3 100644 --- a/packages/plpgsql-deparser/__tests__/pretty/__snapshots__/plpgsql-pretty.test.ts.snap +++ b/packages/plpgsql-deparser/__tests__/pretty/__snapshots__/plpgsql-pretty.test.ts.snap @@ -23,149 +23,149 @@ exports[`lowercase: big-function.sql 1`] = ` sqlerrm constant text; begin begin - if p_org_id IS NULL OR p_user_id IS NULL then - raise exception 'p_org_id and p_user_id are required'; -end if; - if p_from_ts > p_to_ts then - raise exception 'p_from_ts (%) must be <= p_to_ts (%)', p_from_ts, p_to_ts; -end if; - if p_max_rows < 1 OR p_max_rows > 10000 then - raise exception 'p_max_rows out of range: %', p_max_rows; -end if; - if p_round_to < 0 OR p_round_to > 6 then - raise exception 'p_round_to out of range: %', p_round_to; -end if; - if p_lock then - perform SELECT pg_advisory_xact_lock(v_lock_key); -end if; - if p_debug then - 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; -end if; - WITH base AS ( - SELECT - o.id, - o.total_amount::numeric AS total_amount, - o.currency, - o.created_at - FROM app_public.app_order o - WHERE o.org_id = p_org_id - AND o.user_id = p_user_id - AND o.created_at >= p_from_ts - AND o.created_at < p_to_ts - AND o.total_amount::numeric >= v_min_total - AND o.currency = p_currency - ORDER BY o.created_at DESC - LIMIT p_max_rows - ), - totals AS ( - SELECT - count(*)::int AS orders_scanned, - COALESCE(sum(total_amount), 0) AS gross_total, - COALESCE(avg(total_amount), 0) AS avg_total - FROM base - ) - SELECT - t.orders_scanned, - t.gross_total, - t.avg_total - FROM totals t; - if p_apply_discount then - v_discount := round(v_gross * GREATEST(LEAST(v_discount_rate + v_jitter, 0.50), 0), p_round_to); -else - v_discount := 0; -end if; - v_tax := round(GREATEST(v_gross - v_discount, 0) * v_tax_rate, p_round_to); - v_net := round((v_gross - v_discount + v_tax) * power(10::numeric, 0), p_round_to); - SELECT - oi.sku, - sum(oi.quantity)::bigint AS qty - FROM app_public.order_item oi - JOIN app_public.app_order o ON o.id = oi.order_id - WHERE o.org_id = p_org_id - AND o.user_id = p_user_id - AND o.created_at >= p_from_ts - AND o.created_at < p_to_ts - AND o.currency = p_currency - GROUP BY oi.sku - ORDER BY qty DESC, oi.sku ASC - LIMIT 1; - INSERT INTO app_public.order_rollup ( - org_id, - user_id, - period_from, - period_to, - currency, - orders_scanned, - gross_total, - discount_total, - tax_total, - net_total, - avg_order_total, - top_sku, - top_sku_qty, - note, - updated_at - ) - VALUES ( - p_org_id, - p_user_id, - p_from_ts, - p_to_ts, - p_currency, - v_orders_scanned, - v_gross, - v_discount, - v_tax, - v_net, - v_avg, - v_top_sku, - v_top_sku_qty, - p_note, - now() - ) - ON CONFLICT (org_id, user_id, period_from, period_to, currency) - DO UPDATE SET - orders_scanned = EXCLUDED.orders_scanned, - gross_total = EXCLUDED.gross_total, - discount_total = EXCLUDED.discount_total, - tax_total = EXCLUDED.tax_total, - net_total = EXCLUDED.net_total, - avg_order_total = EXCLUDED.avg_order_total, - top_sku = EXCLUDED.top_sku, - top_sku_qty = EXCLUDED.top_sku_qty, - note = COALESCE(EXCLUDED.note, app_public.order_rollup.note), - updated_at = now(); - get diagnostics v_rowcount = ; - v_orders_upserted := v_rowcount; - v_sql := format( - 'SELECT count(*)::int FROM %I.%I WHERE org_id = $1 AND created_at >= $2 AND created_at < $3', - 'app_public', - 'app_order' - ); - execute v_sql into (unnamed row) using p_org_id, p_from_ts, p_to_ts; - if p_debug then - raise notice 'dynamic count(app_order)=%', v_rowcount; -end if; - org_id := p_org_id; - user_id := p_user_id; - period_from := p_from_ts; - period_to := p_to_ts; - orders_scanned := v_orders_scanned; - orders_upserted := v_orders_upserted; - gross_total := v_gross; - discount_total := v_discount; - tax_total := v_tax; - net_total := v_net; - avg_order_total := round(v_avg, p_round_to); - top_sku := v_top_sku; - top_sku_qty := v_top_sku_qty; - message := format( - 'rollup ok: gross=%s discount=%s tax=%s net=%s (discount_rate=%s tax_rate=%s)', - v_gross, v_discount, v_tax, v_net, v_discount_rate, v_tax_rate - ); - return next; - return; -end; + if p_org_id IS NULL OR p_user_id IS NULL then + raise exception 'p_org_id and p_user_id are required'; + end if; + if p_from_ts > p_to_ts then + raise exception 'p_from_ts (%) must be <= p_to_ts (%)', p_from_ts, p_to_ts; + end if; + if p_max_rows < 1 OR p_max_rows > 10000 then + raise exception 'p_max_rows out of range: %', p_max_rows; + end if; + if p_round_to < 0 OR p_round_to > 6 then + raise exception 'p_round_to out of range: %', p_round_to; + end if; + if p_lock then + perform SELECT pg_advisory_xact_lock(v_lock_key); + end if; + if p_debug then + 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; + end if; + WITH base AS ( + SELECT + o.id, + o.total_amount::numeric AS total_amount, + o.currency, + o.created_at + FROM app_public.app_order o + WHERE o.org_id = p_org_id + AND o.user_id = p_user_id + AND o.created_at >= p_from_ts + AND o.created_at < p_to_ts + AND o.total_amount::numeric >= v_min_total + AND o.currency = p_currency + ORDER BY o.created_at DESC + LIMIT p_max_rows + ), + totals AS ( + SELECT + count(*)::int AS orders_scanned, + COALESCE(sum(total_amount), 0) AS gross_total, + COALESCE(avg(total_amount), 0) AS avg_total + FROM base + ) + SELECT + t.orders_scanned, + t.gross_total, + t.avg_total + FROM totals t; + if p_apply_discount then + v_discount := round(v_gross * GREATEST(LEAST(v_discount_rate + v_jitter, 0.50), 0), p_round_to); + else + v_discount := 0; + end if; + v_tax := round(GREATEST(v_gross - v_discount, 0) * v_tax_rate, p_round_to); + v_net := round((v_gross - v_discount + v_tax) * power(10::numeric, 0), p_round_to); + SELECT + oi.sku, + sum(oi.quantity)::bigint AS qty + FROM app_public.order_item oi + JOIN app_public.app_order o ON o.id = oi.order_id + WHERE o.org_id = p_org_id + AND o.user_id = p_user_id + AND o.created_at >= p_from_ts + AND o.created_at < p_to_ts + AND o.currency = p_currency + GROUP BY oi.sku + ORDER BY qty DESC, oi.sku ASC + LIMIT 1; + INSERT INTO app_public.order_rollup ( + org_id, + user_id, + period_from, + period_to, + currency, + orders_scanned, + gross_total, + discount_total, + tax_total, + net_total, + avg_order_total, + top_sku, + top_sku_qty, + note, + updated_at + ) + VALUES ( + p_org_id, + p_user_id, + p_from_ts, + p_to_ts, + p_currency, + v_orders_scanned, + v_gross, + v_discount, + v_tax, + v_net, + v_avg, + v_top_sku, + v_top_sku_qty, + p_note, + now() + ) + ON CONFLICT (org_id, user_id, period_from, period_to, currency) + DO UPDATE SET + orders_scanned = EXCLUDED.orders_scanned, + gross_total = EXCLUDED.gross_total, + discount_total = EXCLUDED.discount_total, + tax_total = EXCLUDED.tax_total, + net_total = EXCLUDED.net_total, + avg_order_total = EXCLUDED.avg_order_total, + top_sku = EXCLUDED.top_sku, + top_sku_qty = EXCLUDED.top_sku_qty, + note = COALESCE(EXCLUDED.note, app_public.order_rollup.note), + updated_at = now(); + get diagnostics v_rowcount = ; + v_orders_upserted := v_rowcount; + v_sql := format( + 'SELECT count(*)::int FROM %I.%I WHERE org_id = $1 AND created_at >= $2 AND created_at < $3', + 'app_public', + 'app_order' + ); + execute v_sql into (unnamed row) using p_org_id, p_from_ts, p_to_ts; + if p_debug then + raise notice 'dynamic count(app_order)=%', v_rowcount; + end if; + org_id := p_org_id; + user_id := p_user_id; + period_from := p_from_ts; + period_to := p_to_ts; + orders_scanned := v_orders_scanned; + orders_upserted := v_orders_upserted; + gross_total := v_gross; + discount_total := v_discount; + tax_total := v_tax; + net_total := v_net; + avg_order_total := round(v_avg, p_round_to); + top_sku := v_top_sku; + top_sku_qty := v_top_sku_qty; + message := format( + 'rollup ok: gross=%s discount=%s tax=%s net=%s (discount_rate=%s tax_rate=%s)', + v_gross, v_discount, v_tax, v_net, v_discount_rate, v_tax_rate + ); + return next; + return; + end; return; end" `; @@ -173,12 +173,12 @@ end" exports[`lowercase: if-else-function.sql 1`] = ` "begin if val > 100 then - return 'large'; -elsif val > 10 then - return 'medium'; -else - return 'small'; -end if; + return 'large'; + elsif val > 10 then + return 'medium'; + else + return 'small'; + end if; return; end" `; @@ -189,8 +189,8 @@ exports[`lowercase: loop-function.sql 1`] = ` i integer; begin for i in 1..n loop - total := total + i; -end loop; + total := total + i; + end loop; return total; end" `; @@ -224,149 +224,149 @@ exports[`uppercase: big-function.sql 1`] = ` sqlerrm CONSTANT text; BEGIN BEGIN - IF p_org_id IS NULL OR p_user_id IS NULL THEN - RAISE EXCEPTION 'p_org_id and p_user_id are required'; -END IF; - IF p_from_ts > p_to_ts THEN - RAISE EXCEPTION 'p_from_ts (%) must be <= p_to_ts (%)', p_from_ts, p_to_ts; -END IF; - IF p_max_rows < 1 OR p_max_rows > 10000 THEN - RAISE EXCEPTION 'p_max_rows out of range: %', p_max_rows; -END IF; - IF p_round_to < 0 OR p_round_to > 6 THEN - RAISE EXCEPTION 'p_round_to out of range: %', p_round_to; -END IF; - IF p_lock THEN - PERFORM SELECT pg_advisory_xact_lock(v_lock_key); -END IF; - IF p_debug THEN - 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; -END IF; - WITH base AS ( - SELECT - o.id, - o.total_amount::numeric AS total_amount, - o.currency, - o.created_at - FROM app_public.app_order o - WHERE o.org_id = p_org_id - AND o.user_id = p_user_id - AND o.created_at >= p_from_ts - AND o.created_at < p_to_ts - AND o.total_amount::numeric >= v_min_total - AND o.currency = p_currency - ORDER BY o.created_at DESC - LIMIT p_max_rows - ), - totals AS ( - SELECT - count(*)::int AS orders_scanned, - COALESCE(sum(total_amount), 0) AS gross_total, - COALESCE(avg(total_amount), 0) AS avg_total - FROM base - ) - SELECT - t.orders_scanned, - t.gross_total, - t.avg_total - FROM totals t; - IF p_apply_discount THEN - v_discount := round(v_gross * GREATEST(LEAST(v_discount_rate + v_jitter, 0.50), 0), p_round_to); -ELSE - v_discount := 0; -END IF; - v_tax := round(GREATEST(v_gross - v_discount, 0) * v_tax_rate, p_round_to); - v_net := round((v_gross - v_discount + v_tax) * power(10::numeric, 0), p_round_to); - SELECT - oi.sku, - sum(oi.quantity)::bigint AS qty - FROM app_public.order_item oi - JOIN app_public.app_order o ON o.id = oi.order_id - WHERE o.org_id = p_org_id - AND o.user_id = p_user_id - AND o.created_at >= p_from_ts - AND o.created_at < p_to_ts - AND o.currency = p_currency - GROUP BY oi.sku - ORDER BY qty DESC, oi.sku ASC - LIMIT 1; - INSERT INTO app_public.order_rollup ( - org_id, - user_id, - period_from, - period_to, - currency, - orders_scanned, - gross_total, - discount_total, - tax_total, - net_total, - avg_order_total, - top_sku, - top_sku_qty, - note, - updated_at - ) - VALUES ( - p_org_id, - p_user_id, - p_from_ts, - p_to_ts, - p_currency, - v_orders_scanned, - v_gross, - v_discount, - v_tax, - v_net, - v_avg, - v_top_sku, - v_top_sku_qty, - p_note, - now() - ) - ON CONFLICT (org_id, user_id, period_from, period_to, currency) - DO UPDATE SET - orders_scanned = EXCLUDED.orders_scanned, - gross_total = EXCLUDED.gross_total, - discount_total = EXCLUDED.discount_total, - tax_total = EXCLUDED.tax_total, - net_total = EXCLUDED.net_total, - avg_order_total = EXCLUDED.avg_order_total, - top_sku = EXCLUDED.top_sku, - top_sku_qty = EXCLUDED.top_sku_qty, - note = COALESCE(EXCLUDED.note, app_public.order_rollup.note), - updated_at = now(); - GET DIAGNOSTICS v_rowcount = ; - v_orders_upserted := v_rowcount; - v_sql := format( - 'SELECT count(*)::int FROM %I.%I WHERE org_id = $1 AND created_at >= $2 AND created_at < $3', - 'app_public', - 'app_order' - ); - EXECUTE v_sql INTO (unnamed row) USING p_org_id, p_from_ts, p_to_ts; - IF p_debug THEN - RAISE NOTICE 'dynamic count(app_order)=%', v_rowcount; -END IF; - org_id := p_org_id; - user_id := p_user_id; - period_from := p_from_ts; - period_to := p_to_ts; - orders_scanned := v_orders_scanned; - orders_upserted := v_orders_upserted; - gross_total := v_gross; - discount_total := v_discount; - tax_total := v_tax; - net_total := v_net; - avg_order_total := round(v_avg, p_round_to); - top_sku := v_top_sku; - top_sku_qty := v_top_sku_qty; - message := format( - 'rollup ok: gross=%s discount=%s tax=%s net=%s (discount_rate=%s tax_rate=%s)', - v_gross, v_discount, v_tax, v_net, v_discount_rate, v_tax_rate - ); - RETURN NEXT; - RETURN; -END; + IF p_org_id IS NULL OR p_user_id IS NULL THEN + RAISE EXCEPTION 'p_org_id and p_user_id are required'; + END IF; + IF p_from_ts > p_to_ts THEN + RAISE EXCEPTION 'p_from_ts (%) must be <= p_to_ts (%)', p_from_ts, p_to_ts; + END IF; + IF p_max_rows < 1 OR p_max_rows > 10000 THEN + RAISE EXCEPTION 'p_max_rows out of range: %', p_max_rows; + END IF; + IF p_round_to < 0 OR p_round_to > 6 THEN + RAISE EXCEPTION 'p_round_to out of range: %', p_round_to; + END IF; + IF p_lock THEN + PERFORM SELECT pg_advisory_xact_lock(v_lock_key); + END IF; + IF p_debug THEN + 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; + END IF; + WITH base AS ( + SELECT + o.id, + o.total_amount::numeric AS total_amount, + o.currency, + o.created_at + FROM app_public.app_order o + WHERE o.org_id = p_org_id + AND o.user_id = p_user_id + AND o.created_at >= p_from_ts + AND o.created_at < p_to_ts + AND o.total_amount::numeric >= v_min_total + AND o.currency = p_currency + ORDER BY o.created_at DESC + LIMIT p_max_rows + ), + totals AS ( + SELECT + count(*)::int AS orders_scanned, + COALESCE(sum(total_amount), 0) AS gross_total, + COALESCE(avg(total_amount), 0) AS avg_total + FROM base + ) + SELECT + t.orders_scanned, + t.gross_total, + t.avg_total + FROM totals t; + IF p_apply_discount THEN + v_discount := round(v_gross * GREATEST(LEAST(v_discount_rate + v_jitter, 0.50), 0), p_round_to); + ELSE + v_discount := 0; + END IF; + v_tax := round(GREATEST(v_gross - v_discount, 0) * v_tax_rate, p_round_to); + v_net := round((v_gross - v_discount + v_tax) * power(10::numeric, 0), p_round_to); + SELECT + oi.sku, + sum(oi.quantity)::bigint AS qty + FROM app_public.order_item oi + JOIN app_public.app_order o ON o.id = oi.order_id + WHERE o.org_id = p_org_id + AND o.user_id = p_user_id + AND o.created_at >= p_from_ts + AND o.created_at < p_to_ts + AND o.currency = p_currency + GROUP BY oi.sku + ORDER BY qty DESC, oi.sku ASC + LIMIT 1; + INSERT INTO app_public.order_rollup ( + org_id, + user_id, + period_from, + period_to, + currency, + orders_scanned, + gross_total, + discount_total, + tax_total, + net_total, + avg_order_total, + top_sku, + top_sku_qty, + note, + updated_at + ) + VALUES ( + p_org_id, + p_user_id, + p_from_ts, + p_to_ts, + p_currency, + v_orders_scanned, + v_gross, + v_discount, + v_tax, + v_net, + v_avg, + v_top_sku, + v_top_sku_qty, + p_note, + now() + ) + ON CONFLICT (org_id, user_id, period_from, period_to, currency) + DO UPDATE SET + orders_scanned = EXCLUDED.orders_scanned, + gross_total = EXCLUDED.gross_total, + discount_total = EXCLUDED.discount_total, + tax_total = EXCLUDED.tax_total, + net_total = EXCLUDED.net_total, + avg_order_total = EXCLUDED.avg_order_total, + top_sku = EXCLUDED.top_sku, + top_sku_qty = EXCLUDED.top_sku_qty, + note = COALESCE(EXCLUDED.note, app_public.order_rollup.note), + updated_at = now(); + GET DIAGNOSTICS v_rowcount = ; + v_orders_upserted := v_rowcount; + v_sql := format( + 'SELECT count(*)::int FROM %I.%I WHERE org_id = $1 AND created_at >= $2 AND created_at < $3', + 'app_public', + 'app_order' + ); + EXECUTE v_sql INTO (unnamed row) USING p_org_id, p_from_ts, p_to_ts; + IF p_debug THEN + RAISE NOTICE 'dynamic count(app_order)=%', v_rowcount; + END IF; + org_id := p_org_id; + user_id := p_user_id; + period_from := p_from_ts; + period_to := p_to_ts; + orders_scanned := v_orders_scanned; + orders_upserted := v_orders_upserted; + gross_total := v_gross; + discount_total := v_discount; + tax_total := v_tax; + net_total := v_net; + avg_order_total := round(v_avg, p_round_to); + top_sku := v_top_sku; + top_sku_qty := v_top_sku_qty; + message := format( + 'rollup ok: gross=%s discount=%s tax=%s net=%s (discount_rate=%s tax_rate=%s)', + v_gross, v_discount, v_tax, v_net, v_discount_rate, v_tax_rate + ); + RETURN NEXT; + RETURN; + END; RETURN; END" `; @@ -374,12 +374,12 @@ END" exports[`uppercase: if-else-function.sql 1`] = ` "BEGIN IF val > 100 THEN - RETURN 'large'; -ELSIF val > 10 THEN - RETURN 'medium'; -ELSE - RETURN 'small'; -END IF; + RETURN 'large'; + ELSIF val > 10 THEN + RETURN 'medium'; + ELSE + RETURN 'small'; + END IF; RETURN; END" `; @@ -390,8 +390,8 @@ exports[`uppercase: loop-function.sql 1`] = ` i integer; BEGIN FOR i IN 1..n LOOP - total := total + i; -END LOOP; + total := total + i; + END LOOP; RETURN total; END" `; diff --git a/packages/plpgsql-deparser/src/plpgsql-deparser.ts b/packages/plpgsql-deparser/src/plpgsql-deparser.ts index 6510077b..85bdb30e 100644 --- a/packages/plpgsql-deparser/src/plpgsql-deparser.ts +++ b/packages/plpgsql-deparser/src/plpgsql-deparser.ts @@ -1460,11 +1460,12 @@ export class PLpgSQLDeparser { } /** - * Apply indentation + * Apply indentation to all lines of text + * This ensures proper formatting for multi-line statements (nested IF/WHILE/LOOP blocks) */ private indent(text: string, level: number): string { const indent = this.options.indent!.repeat(level); - return indent + text; + return indent + text.replace(/\n/g, '\n' + indent); } /**