Skip to content

Commit 85a1ad4

Browse files
1010 db draft
1 parent 53de914 commit 85a1ad4

File tree

1 file changed

+176
-0
lines changed

1 file changed

+176
-0
lines changed
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
/*
2+
================================================================================
3+
Grida Canvas — Node Storage (STI single-table pattern)
4+
--------------------------------------------------------------------------------
5+
Why this exists
6+
- We need a strict, evolvable, relational source of truth for very large
7+
documents (100k+ nodes) while keeping development solo-friendly.
8+
- “Relational” joins across many small tables per node-type are costly to load.
9+
- “NoSQL/JSON” makes migrations and long-term correctness hard.
10+
- This file implements a pragmatic middle ground: a SINGLE “nodes” table (STI)
11+
with strongly-typed nullable columns for capabilities we understand today,
12+
plus a few shared side tables (fills/strokes/effects/links) for arrays.
13+
14+
Core idea (STI: Single Table Inheritance)
15+
- All node kinds live in one table: grida_canvas.canvas_node
16+
- Columns are grouped by capability (state, identity, blend, geometry…).
17+
- Columns are nullable unless universally applicable.
18+
- Type-specific validity is enforced by CHECK constraints (added in later
19+
migrations) and/or by application validators.
20+
- Arrays/variable-length attributes (fills, strokes, effects, guides, etc.)
21+
live in separate “capability tables” keyed by (doc_id, node_row_id, ord).
22+
23+
Why STI over alternatives
24+
- ✅ Fast open-path: a single narrow scan (clustered by doc_id) streams rows.
25+
- ✅ Strict typing & easy migrations: ALTER TABLE ADD COLUMN (NULL default)
26+
is metadata-only in Postgres; we can evolve safely as the engine grows.
27+
- ✅ Simple ergonomics: fewer joins, clear column names, predictable code-gen.
28+
- ⚠️ Many NULLs are expected; that’s OK (NULLs are compact in Postgres).
29+
- ⚠️ The table can grow wide; mitigate by:
30+
- Keeping the row “hot” (only immediately-needed scalars live here).
31+
- Moving bulky/rare fields (e.g. huge text/html/expressions) to side tables.
32+
- Using capability tables for lists (fills/strokes/effects).
33+
34+
CRDT identity model (high level)
35+
- Runtime/CRDT uses a compact 32-bit id (actor:8 | counter:24).
36+
- Database rows use an immutable UUID primary key (row_id) for stability.
37+
- Store CRDT (actor, counter, packed_int, string) alongside row_id with a
38+
UNIQUE(doc_id, actor, counter). All FKs in capability tables point to row_id.
39+
- This lets us rewrite offline actor 0 → assigned actor without FK churn.
40+
41+
How to load big docs efficiently (100k+ rows)
42+
- SELECT only “hot” columns (avoid SELECT *) and stream with server-side cursors.
43+
- Cluster/partition by doc_id for contiguous I/O.
44+
- Fetch capability tables (fills/strokes/links) in parallel or lazily by viewport.
45+
- Optional: mirror the “first fill” on the node row for zero-join paint preview.
46+
47+
Schema evolution playbook
48+
1) Add a new scalar prop? -> ALTER TABLE ADD COLUMN … NULL; (fast)
49+
2) Make it type-specific? -> Add a CHECK that forbids it for other types.
50+
3) New list-like prop? -> New capability table (doc_id, node_row_id, ord, …).
51+
4) Unsure if it’s hot? -> Start in a side table; promote later if it becomes hot.
52+
5) Renaming? -> Add new column, backfill once, dual-write briefly,
53+
remove old column in a later migration.
54+
6) Deprecation? -> Keep column NULL and stop writing; drop only when safe.
55+
56+
Naming & style guidelines
57+
- snake_case, explicit, descriptive: data_state_locked, data_blend_opacity, …
58+
- Prefer consistent prefixes per capability (data_state_*, data_blend_*, …).
59+
- Keep column names < ~60 chars (PG identifier limit is 63 bytes).
60+
- Avoid reserved words; avoid quoted identifiers.
61+
- Default values should match engine defaults to reduce payload size.
62+
63+
Constraints & validation (add in later migrations)
64+
- CHECKs to guard type/capability: e.g., forbid text-only columns on non-text nodes.
65+
- Range checks for normalized values (opacity 0..1, colors 0..255).
66+
- UNIQUE(doc_id, crdt_actor, crdt_counter) when CRDT fields are present.
67+
- FKs from capability tables to canvas_node(row_id) with ON DELETE CASCADE.
68+
69+
Indexing suggestions
70+
- Primary scan path: (doc_id) → sequential/bitmap scan is ideal when clustered.
71+
- Partial indexes for hot filters (e.g., WHERE data_state_active).
72+
- (doc_id, crdt_packed) for client→DB translations.
73+
- (doc_id, type) or small partials if you frequently address subsets.
74+
75+
Hierarchy & ordering
76+
- Do NOT store “children arrays” in this table. Use a link table:
77+
link(doc_id, parent_row_id, child_row_id, ord)
78+
with a fractional ord key (e.g., “a”, “aM”, “b”) to avoid mass reindexing on
79+
reorder. Enforce acyclicity with a small trigger if needed.
80+
81+
Durable Objects (DO) / realtime server compatibility
82+
- DO holds the authoritative in-memory doc state for realtime.
83+
- DO loads from Postgres on cold start using the narrow select described above.
84+
- DO periodically snapshots back to Postgres (or on commit points).
85+
- CRDT ids are used at the edge; DB uses UUID row_id for relational stability.
86+
87+
When to split more tables
88+
- Only for repeatables (fills/strokes/effects/guides/edges).
89+
- Or when a capability family would add many rarely-used columns to this table.
90+
- Keep the number of capability tables small (3–8), shared by all node types.
91+
92+
When NOT to split
93+
- Scalar, frequently-read properties in the initial render path.
94+
- Anything you need to filter/sort by routinely.
95+
96+
SQLite/edge portability (optional)
97+
- If you also run SQLite at the edge: avoid PG-only types; store UUIDs as TEXT;
98+
keep JSON as TEXT with expression indexes; use the same logical shape.
99+
100+
FAQ
101+
- “Is a 100k-row document load OK?” → Yes, if you stream a narrow
102+
projection and cluster by doc_id (expect tens of MBs at most).
103+
- “Will hundreds of NULLs bloat storage?” → Not significantly in PG; NULLs are
104+
bitmap-tracked. Keep wide TEXT/VARCHAR out of the hot row.
105+
- “Why not JSONB?” → Harder long-term migrations; weaker
106+
guarantees. We want strict evolution + predictable queries.
107+
- “Can we enforce per-type columns strictly in DB?” → Yes, with CHECKs; complement with
108+
application validators for friendlier errors.
109+
110+
--------------------------------------------------------------------------------
111+
Everything below this header defines the initial columns for shared capabilities.
112+
Add columns incrementally with small, focused migrations. Keep the hot row narrow,
113+
arrays in capability tables, and validate aggressively.
114+
115+
*/
116+
117+
118+
119+
create type grida_canvas.affine2d as (
120+
a double precision,
121+
b double precision,
122+
c double precision,
123+
d double precision,
124+
e double precision,
125+
f double precision
126+
);
127+
128+
129+
-- [sti-pattern single table for design canvas document nodes]
130+
create table grida_canvas.canvas_node (
131+
id int not null,
132+
133+
data_state_locked boolean not null default false,
134+
data_state_active boolean not null default true,
135+
136+
-- name of the node, optionally set by user
137+
data_name text null,
138+
139+
-- blend
140+
blend_opacity real not null default 1.0,
141+
blend_mode text not null default 'normal',
142+
143+
-- geometry - dimension
144+
geometry_width_real real not null default 0.0,
145+
geometry_height__real real not null default 0.0,
146+
geometry_width_max real not null default 0.0,
147+
geometry_height_max real not null default 0.0,
148+
geometry_width_min real not null default 0.0,
149+
geometry_height_min real not null default 0.0,
150+
151+
-- geometry - position
152+
geometry_transform_relative grida_canvas.affine2d not null default row(1,0,0,1,0,0)
153+
154+
-- layout - padding
155+
layout_padding_left real null,
156+
layout_padding_right real null,
157+
layout_padding_top real null,
158+
layout_padding_bottom real null,
159+
160+
161+
162+
-- style - stroke
163+
164+
-- style - text
165+
text_font_size real null,
166+
text_text_transform text null,
167+
text_opentype_features jsonb null,
168+
text_letter_spacing real null,
169+
text_text_align text null,
170+
text_text_align_vertical text null,
171+
172+
173+
-- service extensions
174+
ext_guides jsonb null,
175+
ext_export_settings jsonb null,
176+
);

0 commit comments

Comments
 (0)