-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathschema.sql
More file actions
223 lines (188 loc) · 7.72 KB
/
schema.sql
File metadata and controls
223 lines (188 loc) · 7.72 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
-- Demo schema for dryrun
--
-- Naive/small app with deliberate schema issues to demostrate dryrun
BEGIN;
DROP SCHEMA IF EXISTS public CASCADE;
CREATE SCHEMA public;
-------------------------------------------------------------------
-- organizations (formerly teams)
--
-- Modern SaaS multi-tenancy with external_id and slug.
-------------------------------------------------------------------
CREATE TABLE organizations (
organization_id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
external_id uuid NOT NULL DEFAULT gen_random_uuid() UNIQUE,
name text NOT NULL,
slug text NOT NULL UNIQUE,
settings jsonb NOT NULL DEFAULT '{}'::jsonb,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
-------------------------------------------------------------------
-- users
--
-- varchar(255) on email: a Rails/Django habit that dryrun flags.
-- Missing updated_at.
-------------------------------------------------------------------
CREATE TABLE users (
user_id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
email varchar(255) NOT NULL UNIQUE,
name text NOT NULL,
organization_id bigint REFERENCES organizations (organization_id),
created_at timestamptz NOT NULL DEFAULT now()
);
-------------------------------------------------------------------
-- projects
--
-- serial instead of identity - the old way.
-- timestamp without time zone - loses timezone context.
-------------------------------------------------------------------
CREATE TABLE projects (
project_id serial PRIMARY KEY,
organization_id bigint NOT NULL REFERENCES organizations (organization_id),
name text NOT NULL,
status text NOT NULL DEFAULT 'active',
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
created_at timestamp NOT NULL DEFAULT now(),
updated_at timestamp NOT NULL DEFAULT now()
);
CREATE INDEX projects_by_organization ON projects (organization_id);
-------------------------------------------------------------------
-- tasks
--
-- Foreign keys on project_id and assignee_id without indexes.
-- Unnamed CHECK constraint on status.
-------------------------------------------------------------------
CREATE TABLE tasks (
task_id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
project_id bigint NOT NULL REFERENCES projects (project_id),
assignee_id bigint REFERENCES users (user_id),
title text NOT NULL,
status text NOT NULL DEFAULT 'open',
priority integer NOT NULL DEFAULT 0,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
CHECK (status IN ('open', 'in_progress', 'done', 'cancelled'))
);
-------------------------------------------------------------------
-- task_comments (formerly comments)
--
-- No primary key.
-------------------------------------------------------------------
CREATE TABLE task_comments (
task_id bigint NOT NULL REFERENCES tasks (task_id),
user_id bigint NOT NULL REFERENCES users (user_id),
body text NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX task_comments_by_task ON task_comments (task_id);
CREATE INDEX task_comments_by_user ON task_comments (user_id);
-------------------------------------------------------------------
-- tags / task_tags
-------------------------------------------------------------------
CREATE TABLE tags (
tag_id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name text NOT NULL UNIQUE,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE task_tags (
task_id bigint NOT NULL REFERENCES tasks (task_id),
tag_id bigint NOT NULL REFERENCES tags (tag_id),
PRIMARY KEY (task_id, tag_id)
);
-------------------------------------------------------------------
-- audit_log (partitioned)
--
-- Range-partitioned by created_at. Q3 2024 is missing - a gap
-- that will cause INSERT failures for July–September dates.
-- No DEFAULT partition.
-------------------------------------------------------------------
CREATE TABLE audit_log (
log_id bigint GENERATED ALWAYS AS IDENTITY,
user_id bigint,
action text NOT NULL,
detail text,
detail_meta jsonb,
created_at timestamptz NOT NULL DEFAULT now()
) PARTITION BY RANGE (created_at);
CREATE TABLE audit_log_2024q1 PARTITION OF audit_log
FOR VALUES FROM ('2024-01-01') TO ('2024-04-01');
CREATE TABLE audit_log_2024q2 PARTITION OF audit_log
FOR VALUES FROM ('2024-04-01') TO ('2024-07-01');
-- Q3 intentionally missing
CREATE TABLE audit_log_2024q4 PARTITION OF audit_log
FOR VALUES FROM ('2024-10-01') TO ('2025-01-01');
CREATE TABLE audit_log_2025q1 PARTITION OF audit_log
FOR VALUES FROM ('2025-01-01') TO ('2025-04-01');
-------------------------------------------------------------------
-- user_notifications (formerly notifications)
--
-- varchar columns where text would do. No created_at, no updated_at.
-- Duplicate index on user_id under two names.
-------------------------------------------------------------------
CREATE TABLE user_notifications (
notification_id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
user_id bigint NOT NULL REFERENCES users (user_id),
channel varchar(50) NOT NULL DEFAULT 'email',
message varchar(1000) NOT NULL,
read boolean NOT NULL DEFAULT false
);
CREATE INDEX user_notifications_by_user ON user_notifications (user_id);
CREATE INDEX user_notifications_user_lookup ON user_notifications (user_id);
COMMIT;
-------------------------------------------------------------------
-- Seed data
-------------------------------------------------------------------
INSERT INTO organizations (name, slug, settings)
VALUES
('Engineering', 'engineering', '{"tier": "enterprise", "seats": 50}'::jsonb),
('Product', 'product', '{"tier": "pro", "seats": 20}'::jsonb),
('Design', 'design', '{"tier": "pro", "seats": 15}'::jsonb);
INSERT INTO users (email, name, organization_id)
SELECT
format('user%s@example.com', n),
format('User %s', n),
1 + (n % 3)
FROM generate_series(1, 50) AS s(n);
INSERT INTO projects (organization_id, name, status, metadata)
SELECT
1 + (n % 3),
format('Project %s', n),
CASE WHEN n % 5 = 0 THEN 'archived' ELSE 'active' END,
jsonb_build_object('priority', CASE WHEN n % 3 = 0 THEN 'high' ELSE 'normal' END, 'budget', n * 1000)
FROM generate_series(1, 10) AS s(n);
INSERT INTO tasks (project_id, assignee_id, title, status, priority)
SELECT
1 + (n % 10),
1 + (n % 50),
format('Task %s', n),
(ARRAY['open', 'in_progress', 'done'])[1 + (n % 3)],
n % 4
FROM generate_series(1, 200) AS s(n);
INSERT INTO task_comments (task_id, user_id, body)
SELECT
1 + (n % 200),
1 + (n % 50),
format('Comment on task from user %s', n)
FROM generate_series(1, 500) AS s(n);
INSERT INTO tags (name)
VALUES ('bug'), ('feature'), ('urgent'), ('docs'), ('tech-debt');
INSERT INTO task_tags (task_id, tag_id)
SELECT DISTINCT 1 + (n % 200), 1 + (n % 5)
FROM generate_series(1, 300) AS s(n);
INSERT INTO audit_log (user_id, action, detail, detail_meta, created_at)
SELECT
1 + (n % 50),
(ARRAY['login', 'create', 'update', 'delete'])[1 + (n % 4)],
format('Action detail %s', n),
jsonb_build_object('ip', format('10.0.%s.%s', n % 256, (n * 7) % 256), 'source', CASE WHEN n % 2 = 0 THEN 'web' ELSE 'api' END),
'2024-01-15'::timestamptz + make_interval(hours => n)
FROM generate_series(1, 1000) AS s(n);
INSERT INTO user_notifications (user_id, channel, message)
SELECT
1 + (n % 50),
CASE WHEN n % 3 = 0 THEN 'slack' ELSE 'email' END,
format('Notification %s', n)
FROM generate_series(1, 100) AS s(n);
ANALYZE;