Skip to content

Commit f284317

Browse files
authored
Merge pull request #5 from constructive-io/feat/readme-io-links
docs: input → output table in README; packed single-file module projection
2 parents 90078fe + 72b706d commit f284317

3 files changed

Lines changed: 191 additions & 4 deletions

File tree

README.md

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,32 @@ See the planning issues:
2424
[constructive-planning#1329](https://github.com/constructive-io/constructive-planning/issues/1329)
2525
(three-dial roadmap).
2626

27+
## At a glance: input → output
28+
29+
Every artifact in this repo is generated by one command. Click through:
30+
31+
| Input | Command | Output |
32+
|---|---|---|
33+
| [`sources/shop.v1.sql`](sources/shop.v1.sql) — a raw pg_dump | `pgpm import` | [`packages/shop/`](packages/shop) — a full pgpm module (deploy/revert/verify per object) |
34+
| [`packages/shop/`](packages/shop) — the module | `pgpm transform --emit-sql` | [`sources/shop.module.sql`](sources/shop.module.sql) — one clean, packed SQL file |
35+
| [`packages/shop/`](packages/shop) | `pgpm transform --granularity atomic` | [`packages/shop-atomic/`](packages/shop-atomic) — same schema, one statement per change |
36+
| [`packages/shop/`](packages/shop) | `pgpm transform --granularity consolidated` | [`packages/shop-consolidated/`](packages/shop-consolidated) — same schema, compact history |
37+
| [`packages/shop/`](packages/shop) + [`partition.json`](partition.json) | `pgpm transform --partition` | [`packages/shop-app/`](packages/shop-app) + [`packages/shop-security/`](packages/shop-security) — app/security split with cross-package requires |
38+
| [`packages/shop/`](packages/shop) vs [`sources/shop.v2.sql`](sources/shop.v2.sql) | `pgpm diff` | [`packages/shop-v1-to-v2/`](packages/shop-v1-to-v2) — migration module, and [`sources/shop.v1-to-v2.sql`](sources/shop.v1-to-v2.sql) — the same delta as one SQL file |
39+
40+
[CI](.github/workflows/ci.yml) deploys every output and proves they are all
41+
the **same schema** (identical catalogs), and that v1 + the generated
42+
migration equals v2 loaded fresh.
43+
2744
## Layout
2845

2946
```
3047
migration-examples/ # pgpm init workspace
3148
├── sources/
3249
│ ├── shop.v1.sql # hand-written pg_dump-style dump: 2 schemas, FKs, function, trigger, RLS, grants, sequence, comments
33-
│ └── shop.v2.sql # evolved v1: +table, +column, -column, changed function body, +policy, -index, changed constraint
50+
│ ├── shop.v2.sql # evolved v1: +table, +column, -column, changed function body, +policy, -index, changed constraint
51+
│ ├── shop.module.sql # generated: the shop module packed into one linear SQL file (--emit-sql)
52+
│ └── shop.v1-to-v2.sql # generated: the v1 -> v2 delta as one linear SQL file (--emit-sql)
3453
├── packages/
3554
│ ├── shop/ # canonical module, imported from the v1 dump (object granularity)
3655
│ ├── shop-atomic/ # same schema, one change per statement
@@ -62,6 +81,19 @@ comments ride with their host object.
6281

6382
> Status: generated — 53 statements → 15 changes (12 preamble statements skipped, 0 warnings).
6483
84+
### 1b. `sources/shop.module.sql` — the module, packed back into one file
85+
86+
```sh
87+
pgpm transform --granularity object --cwd packages/shop --out /tmp/roundtrip \
88+
--emit-sql "$PWD/sources/shop.module.sql"
89+
```
90+
91+
The inverse projection: the whole module deparsed in plan order into a single
92+
clean SQL script — [`sources/shop.module.sql`](sources/shop.module.sql).
93+
Dump in, module out (§1); module in, one file out (§1b).
94+
95+
> Status: generated.
96+
6597
### 2. `packages/shop-atomic` — the granularity dial, turned all the way down
6698

6799
```sh
@@ -141,9 +173,11 @@ service container:
141173
2. all granularity/partition variants produce **byte-identical normalized
142174
catalogs** (`pg_dump --schema-only`, noise-stripped, table columns
143175
order-normalized) — different shapes, same schema;
144-
3. deploying shop@v1 then `shop-v1-to-v2` yields the same catalog as loading
145-
`sources/shop.v2.sql` fresh;
146-
4. `pgpm revert` unwinds every module and leaves the database clean.
176+
3. loading the packed single-file projection (`sources/shop.module.sql`)
177+
fresh yields the same catalog as deploying the module;
178+
4. deploying shop@v1 then `shop-v1-to-v2` yields the same catalog as loading
179+
`sources/shop.v2.sql` fresh (and likewise for the linear delta SQL);
180+
5. `pgpm revert` unwinds every module and leaves the database clean.
147181

148182
Checks whose packages haven't been generated yet are skipped with a notice,
149183
so CI is green at every stage of the incremental build-out.

scripts/acceptance.sh

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,18 @@ else
157157
skip "catalog equivalence (packages/shop)"
158158
fi
159159

160+
# ---------------------------------------------------------------------------
161+
# 3b. the packed single-file projection of the module == the module
162+
# ---------------------------------------------------------------------------
163+
if [ -f sources/shop.module.sql ] && [ -d packages/shop ]; then
164+
note "load sources/shop.module.sql fresh -> shop_packed"
165+
$PSQL -d postgres -c 'CREATE DATABASE shop_packed'
166+
$PSQL -d shop_packed -v ON_ERROR_STOP=1 -f sources/shop.module.sql
167+
assert_same_catalog shop_object shop_packed "object vs packed single-file SQL"
168+
else
169+
skip "packed single-file SQL check (sources/shop.module.sql)"
170+
fi
171+
160172
# ---------------------------------------------------------------------------
161173
# 4. shop@v1 + shop-v1-to-v2 migration == shop.v2.sql deployed fresh
162174
# ---------------------------------------------------------------------------

sources/shop.module.sql

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
CREATE SCHEMA shop;
2+
3+
COMMENT ON SCHEMA shop IS 'Storefront: customers, products, orders.';
4+
5+
GRANT USAGE ON SCHEMA shop TO app_user;
6+
7+
CREATE SCHEMA audit;
8+
9+
COMMENT ON SCHEMA audit IS 'Append-only change log for shop tables.';
10+
11+
CREATE SEQUENCE shop.order_number_seq START 1000 INCREMENT 1 NO MINVALUE NO MAXVALUE CACHE 1;
12+
13+
CREATE TABLE shop.customers (
14+
id uuid DEFAULT gen_random_uuid() NOT NULL,
15+
email text NOT NULL,
16+
full_name text NOT NULL,
17+
phone text,
18+
created_at timestamptz DEFAULT now() NOT NULL,
19+
CONSTRAINT customers_pkey PRIMARY KEY (id),
20+
CONSTRAINT customers_email_key
21+
UNIQUE (email)
22+
);
23+
24+
COMMENT ON TABLE shop.customers IS 'Registered storefront customers.';
25+
26+
COMMENT ON COLUMN shop.customers.email IS 'Unique login email, lowercased.';
27+
28+
GRANT SELECT ON shop.customers TO app_user;
29+
30+
CREATE TABLE shop.products (
31+
id uuid DEFAULT gen_random_uuid() NOT NULL,
32+
sku text NOT NULL,
33+
name text NOT NULL,
34+
description text,
35+
price_cents int NOT NULL,
36+
created_at timestamptz DEFAULT now() NOT NULL,
37+
CONSTRAINT products_pkey PRIMARY KEY (id),
38+
CONSTRAINT products_sku_key
39+
UNIQUE (sku),
40+
CONSTRAINT products_price_cents_check
41+
CHECK (price_cents >= 0)
42+
);
43+
44+
COMMENT ON TABLE shop.products IS 'Sellable products with price in cents.';
45+
46+
GRANT SELECT ON shop.products TO app_user;
47+
48+
CREATE TABLE shop.orders (
49+
id uuid DEFAULT gen_random_uuid() NOT NULL,
50+
order_number bigint DEFAULT nextval(CAST('shop.order_number_seq' AS regclass)) NOT NULL,
51+
customer_id uuid NOT NULL,
52+
status text DEFAULT 'pending' NOT NULL,
53+
placed_at timestamptz DEFAULT now() NOT NULL,
54+
CONSTRAINT orders_pkey PRIMARY KEY (id),
55+
CONSTRAINT orders_order_number_key
56+
UNIQUE (order_number),
57+
CONSTRAINT orders_status_check
58+
CHECK (status IN ('pending', 'paid', 'shipped', 'cancelled'))
59+
);
60+
61+
ALTER TABLE ONLY shop.orders
62+
ADD CONSTRAINT orders_customer_id_fkey
63+
FOREIGN KEY(customer_id)
64+
REFERENCES shop.customers (id);
65+
66+
ALTER TABLE shop.orders
67+
ENABLE ROW LEVEL SECURITY;
68+
69+
COMMENT ON TABLE shop.orders IS 'Customer orders; one row per checkout.';
70+
71+
GRANT SELECT, INSERT, UPDATE ON shop.orders TO app_user;
72+
73+
CREATE TABLE shop.order_items (
74+
id uuid DEFAULT gen_random_uuid() NOT NULL,
75+
order_id uuid NOT NULL,
76+
product_id uuid NOT NULL,
77+
quantity int DEFAULT 1 NOT NULL,
78+
unit_price_cents int NOT NULL,
79+
CONSTRAINT order_items_pkey PRIMARY KEY (id)
80+
);
81+
82+
ALTER TABLE ONLY shop.order_items
83+
ADD CONSTRAINT order_items_order_id_fkey
84+
FOREIGN KEY(order_id)
85+
REFERENCES shop.orders (id)
86+
ON DELETE CASCADE;
87+
88+
ALTER TABLE ONLY shop.order_items
89+
ADD CONSTRAINT order_items_product_id_fkey
90+
FOREIGN KEY(product_id)
91+
REFERENCES shop.products (id);
92+
93+
GRANT SELECT, INSERT ON shop.order_items TO app_user;
94+
95+
CREATE TABLE audit.change_log (
96+
id bigint GENERATED ALWAYS AS IDENTITY NOT NULL,
97+
table_name text NOT NULL,
98+
row_id uuid NOT NULL,
99+
operation text NOT NULL,
100+
changed_at timestamptz DEFAULT now() NOT NULL,
101+
CONSTRAINT change_log_pkey PRIMARY KEY (id)
102+
);
103+
104+
CREATE FUNCTION shop.order_total(
105+
p_order_id uuid
106+
) RETURNS int LANGUAGE sql STABLE AS $EOFCODE$
107+
SELECT COALESCE(SUM(quantity * unit_price_cents), 0)::integer
108+
FROM shop.order_items
109+
WHERE order_id = p_order_id;
110+
$EOFCODE$;
111+
112+
COMMENT ON FUNCTION shop.order_total(p_order_id uuid) IS 'Sum of line totals for an order, in cents.';
113+
114+
CREATE FUNCTION audit.log_order_change() RETURNS trigger LANGUAGE plpgsql AS $EOFCODE$
115+
BEGIN
116+
INSERT INTO audit.change_log (table_name, row_id, operation)
117+
VALUES (TG_TABLE_NAME, NEW.id, TG_OP);
118+
RETURN NEW;
119+
END;
120+
$EOFCODE$;
121+
122+
CREATE INDEX idx_orders_customer_id ON shop.orders (customer_id);
123+
124+
CREATE INDEX idx_orders_placed_at ON shop.orders (placed_at);
125+
126+
CREATE INDEX idx_order_items_order_id ON shop.order_items (order_id);
127+
128+
CREATE TRIGGER orders_audit_trigger
129+
AFTER INSERT OR UPDATE
130+
ON shop.orders
131+
FOR EACH ROW
132+
EXECUTE PROCEDURE audit.log_order_change();
133+
134+
CREATE POLICY orders_select_own
135+
ON shop.orders
136+
AS PERMISSIVE
137+
FOR SELECT
138+
TO PUBLIC
139+
USING (
140+
customer_id = (current_setting('app.current_customer_id', true))::uuid
141+
);

0 commit comments

Comments
 (0)