-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmvcc_demo_schema.sql.sql
More file actions
259 lines (231 loc) · 9.06 KB
/
mvcc_demo_schema.sql.sql
File metadata and controls
259 lines (231 loc) · 9.06 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
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
-- ===========================================================
-- MVCC, temporal versioning for donations
-- Pattern: base history table + updatable view + INSTEAD OF triggers
-- Guarantees a single current version per business key
-- Includes helpers to query history, as of time, and restore versions
-- ===========================================================
-- Safety, create schema placeholder if you use a dedicated schema
-- CREATE SCHEMA IF NOT EXISTS charity;
-- 1) Base history table, stores every version
CREATE TABLE IF NOT EXISTS donations_hist (
donation_id BIGINT NOT NULL, -- business key, stable across versions
version_id BIGSERIAL PRIMARY KEY, -- technical version identifier
donor_id BIGINT NOT NULL,
project_id BIGINT NULL,
campaign_id BIGINT NULL,
orphan_id BIGINT NULL,
amount NUMERIC(12,2) NOT NULL CHECK (amount > 0),
currency TEXT NOT NULL DEFAULT 'USD',
donation_date DATE NOT NULL,
-- system time validity window
valid_from TIMESTAMPTZ NOT NULL DEFAULT now(),
valid_to TIMESTAMPTZ NOT NULL DEFAULT 'infinity',
-- metadata
txid BIGINT NOT NULL DEFAULT txid_current(),
op CHAR(1) NOT NULL CHECK (op IN ('I','U','D')),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
created_by TEXT NULL -- optional, fill from app or session
);
-- Optional FKs, comment out if the referenced tables are not present
-- ALTER TABLE donations_hist
-- ADD CONSTRAINT fk_donations_hist_donor FOREIGN KEY (donor_id) REFERENCES donors(donor_id),
-- ADD CONSTRAINT fk_donations_hist_project FOREIGN KEY (project_id) REFERENCES projects(project_id),
-- ADD CONSTRAINT fk_donations_hist_campaign FOREIGN KEY (campaign_id)REFERENCES campaigns(campaign_id),
-- ADD CONSTRAINT fk_donations_hist_orphan FOREIGN KEY (orphan_id) REFERENCES orphans(orphan_id);
-- Only one current version per donation_id
CREATE UNIQUE INDEX IF NOT EXISTS ux_donations_hist_current
ON donations_hist(donation_id)
WHERE valid_to = 'infinity';
-- Helpful lookup indexes
CREATE INDEX IF NOT EXISTS ix_donations_hist_id_from ON donations_hist(donation_id, valid_from);
CREATE INDEX IF NOT EXISTS ix_donations_hist_from_to ON donations_hist(valid_from, valid_to);
CREATE INDEX IF NOT EXISTS ix_donations_hist_txid ON donations_hist(txid);
-- 2) Updatable view that always shows the current snapshot
CREATE OR REPLACE VIEW donations AS
SELECT
donation_id,
donor_id,
project_id,
campaign_id,
orphan_id,
amount,
currency,
donation_date
FROM donations_hist
WHERE valid_to = 'infinity';
COMMENT ON VIEW donations IS 'Current snapshot of donations, backed by donations_hist MVCC table';
-- 3) Trigger functions that implement versioning on the view
-- Insert, always creates a new current version
CREATE OR REPLACE FUNCTION trg_donations_ins()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO donations_hist(
donation_id, donor_id, project_id, campaign_id, orphan_id,
amount, currency, donation_date,
valid_from, valid_to, op, created_by
)
VALUES (
NEW.donation_id, NEW.donor_id, NEW.project_id, NEW.campaign_id, NEW.orphan_id,
NEW.amount, COALESCE(NEW.currency, 'USD'), NEW.donation_date,
now(), 'infinity', 'I', current_user
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Update, closes the current version, inserts a new one
CREATE OR REPLACE FUNCTION trg_donations_upd()
RETURNS TRIGGER AS $$
DECLARE
v_now timestamptz := now();
BEGIN
-- Close the current version window
UPDATE donations_hist
SET valid_to = v_now, op = 'U'
WHERE donation_id = OLD.donation_id
AND valid_to = 'infinity';
-- Insert the new version
INSERT INTO donations_hist(
donation_id, donor_id, project_id, campaign_id, orphan_id,
amount, currency, donation_date,
valid_from, valid_to, op, created_by
)
VALUES (
OLD.donation_id,
COALESCE(NEW.donor_id, OLD.donor_id),
COALESCE(NEW.project_id, OLD.project_id),
COALESCE(NEW.campaign_id, OLD.campaign_id),
COALESCE(NEW.orphan_id, OLD.orphan_id),
COALESCE(NEW.amount, OLD.amount),
COALESCE(NEW.currency, OLD.currency),
COALESCE(NEW.donation_date, OLD.donation_date),
v_now, 'infinity', 'U', current_user
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Delete, closes the current version, optionally writes a tombstone
CREATE OR REPLACE FUNCTION trg_donations_del()
RETURNS TRIGGER AS $$
BEGIN
UPDATE donations_hist
SET valid_to = now(), op = 'D'
WHERE donation_id = OLD.donation_id
AND valid_to = 'infinity';
-- Optional, uncomment to write an explicit tombstone row
-- INSERT INTO donations_hist(
-- donation_id, donor_id, project_id, campaign_id, orphan_id,
-- amount, currency, donation_date,
-- valid_from, valid_to, op, created_by
-- ) VALUES (
-- OLD.donation_id, OLD.donor_id, OLD.project_id, OLD.campaign_id, OLD.orphan_id,
-- OLD.amount, OLD.currency, OLD.donation_date,
-- now(), now(), 'D', current_user
-- );
RETURN OLD;
END;
$$ LANGUAGE plpgsql;
-- 4) INSTEAD OF triggers on the view
DROP TRIGGER IF EXISTS ins_donations_mvcc ON donations;
DROP TRIGGER IF EXISTS upd_donations_mvcc ON donations;
DROP TRIGGER IF EXISTS del_donations_mvcc ON donations;
CREATE TRIGGER ins_donations_mvcc
INSTEAD OF INSERT ON donations
FOR EACH ROW EXECUTE FUNCTION trg_donations_ins();
CREATE TRIGGER upd_donations_mvcc
INSTEAD OF UPDATE ON donations
FOR EACH ROW EXECUTE FUNCTION trg_donations_upd();
CREATE TRIGGER del_donations_mvcc
INSTEAD OF DELETE ON donations
FOR EACH ROW EXECUTE FUNCTION trg_donations_del();
-- 5) Helper functions
-- 5a, full history for a donation_id
CREATE OR REPLACE FUNCTION get_donation_history(p_donation_id BIGINT)
RETURNS TABLE (
version_id BIGINT,
donation_id BIGINT,
donor_id BIGINT,
project_id BIGINT,
campaign_id BIGINT,
orphan_id BIGINT,
amount NUMERIC,
currency TEXT,
donation_date DATE,
valid_from TIMESTAMPTZ,
valid_to TIMESTAMPTZ,
op CHAR(1),
txid BIGINT,
created_at TIMESTAMPTZ,
created_by TEXT
) AS $$
BEGIN
RETURN QUERY
SELECT
h.version_id, h.donation_id, h.donor_id, h.project_id, h.campaign_id, h.orphan_id,
h.amount, h.currency, h.donation_date, h.valid_from, h.valid_to, h.op, h.txid, h.created_at, h.created_by
FROM donations_hist h
WHERE h.donation_id = p_donation_id
ORDER BY h.valid_from;
END;
$$ LANGUAGE plpgsql STABLE;
-- 5b, as of timeline query, returns the snapshot at a given time
CREATE OR REPLACE FUNCTION donations_as_of(p_as_of TIMESTAMPTZ)
RETURNS TABLE (
donation_id BIGINT,
donor_id BIGINT,
project_id BIGINT,
campaign_id BIGINT,
orphan_id BIGINT,
amount NUMERIC,
currency TEXT,
donation_date DATE
) AS $$
BEGIN
RETURN QUERY
SELECT donation_id, donor_id, project_id, campaign_id, orphan_id, amount, currency, donation_date
FROM donations_hist
WHERE valid_from <= p_as_of
AND valid_to > p_as_of;
END;
$$ LANGUAGE plpgsql STABLE;
-- 5c, restore a specific historic version to be the new current one
CREATE OR REPLACE FUNCTION restore_donation_version(p_donation_id BIGINT, p_version_id BIGINT)
RETURNS VOID AS $$
DECLARE
v_row donations_hist%ROWTYPE;
v_now timestamptz := now();
BEGIN
SELECT * INTO v_row
FROM donations_hist
WHERE donation_id = p_donation_id
AND version_id = p_version_id;
IF NOT FOUND THEN
RAISE EXCEPTION 'Version % for donation % not found', p_version_id, p_donation_id;
END IF;
-- Close current version, if any
UPDATE donations_hist
SET valid_to = v_now, op = 'U'
WHERE donation_id = p_donation_id
AND valid_to = 'infinity';
-- Insert a copy as the new current version
INSERT INTO donations_hist(
donation_id, donor_id, project_id, campaign_id, orphan_id,
amount, currency, donation_date,
valid_from, valid_to, op, created_by
)
VALUES (
v_row.donation_id, v_row.donor_id, v_row.project_id, v_row.campaign_id, v_row.orphan_id,
v_row.amount, v_row.currency, v_row.donation_date,
v_now, 'infinity', 'U', current_user
);
END;
$$ LANGUAGE plpgsql;
-- 6) Quick demo, uncomment to test
-- INSERT INTO donations(donation_id, donor_id, amount, donation_date) VALUES (1001, 1, 50.00, CURRENT_DATE);
-- UPDATE donations SET amount = 75.00 WHERE donation_id = 1001;
-- DELETE FROM donations WHERE donation_id = 1001;
-- SELECT * FROM get_donation_history(1001);
-- SELECT * FROM donations_as_of(now() - interval '5 minutes');
-- Notes
-- Use the donations view for all CRUD in your app
-- The system enforces one current version per donation_id by partial unique index
-- History is immutable, only new versions are added, current is closed by setting valid_to