-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathinit.sql
More file actions
164 lines (139 loc) · 5.16 KB
/
init.sql
File metadata and controls
164 lines (139 loc) · 5.16 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
-- Account table with unique service account
CREATE TABLE account (
id UUID PRIMARY KEY,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
INSERT INTO account(id)
VALUES ('00000000-0000-0000-0000-000000000000');
-- Main transfer table with constraints, indexes, checks and triggers
CREATE TABLE transfer (
source UUID NOT NULL REFERENCES account(id),
index INTEGER NOT NULL,
destination UUID NOT NULL REFERENCES account(id),
amount DECIMAL NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (source, index),
CONSTRAINT transfer_positive_amount CHECK (amount > 0),
CONSTRAINT transfer_not_same_account CHECK (source != destination)
);
CREATE INDEX transfer_source ON transfer(source);
CREATE INDEX transfer_destination ON transfer(destination);
CREATE INDEX transfer_created_at ON transfer(created_at);
-- Tricky function to remove SQL aggregation mess from service application
CREATE FUNCTION account_metadata(UUID[])
RETURNS TABLE (id UUID, balance NUMERIC, next_transfer_index INTEGER) AS $BODY$
BEGIN
RETURN QUERY
-- If some values not found after left join (NULL), replace them with 0
SELECT
a.id,
COALESCE(incoming_sum, 0) - COALESCE(outgoing_sum, 0) AS balance,
COALESCE(last_index + 1, 0) AS next_transfer_index
-- Enforce accounts existence in account table
FROM (SELECT
a.id
FROM account a
WHERE a.id = ANY($1)) a
-- Join with sum of all incoming transfers until specified timestamp
LEFT JOIN (SELECT
destination AS id,
SUM(amount) AS incoming_sum
FROM transfer
WHERE destination = ANY($1)
GROUP BY destination) i ON i.id = a.id
-- Join with sum of all outgoing transfers and last transfer index until specified timestamp
LEFT JOIN (SELECT
source AS id,
SUM(amount) AS outgoing_sum,
MAX(index) AS last_index
FROM transfer
WHERE source = ANY($1)
GROUP BY source) o ON o.id = a.id;
END;
$BODY$
LANGUAGE PLPGSQL;
-- Trigger to check source balance exactly BEFORE insertion of new transfer
CREATE FUNCTION check_source_balance()
RETURNS TRIGGER AS $BODY$
DECLARE
source_balance DECIMAL;
BEGIN
-- Just ignore service account, because it is a special account
IF NEW.source = '00000000-0000-0000-0000-000000000000' THEN
RETURN NEW;
END IF;
SELECT balance
INTO source_balance
FROM account_metadata(ARRAY[NEW.source]::uuid[]);
IF source_balance < NEW.amount THEN
RAISE EXCEPTION '% balance should be bigger than %', NEW.source, NEW.amount;
END IF;
RETURN NEW;
END;
$BODY$
LANGUAGE PLPGSQL;
CREATE TRIGGER transfer_check_source_balance
BEFORE INSERT ON transfer
FOR EACH ROW EXECUTE PROCEDURE check_source_balance();
-- Trigger to check indexing (order) of transfer BEFORE insertion of new one
CREATE FUNCTION check_indexing()
RETURNS TRIGGER AS $BODY$
DECLARE
expected_index INTEGER;
BEGIN
SELECT COALESCE(MAX(index) + 1, 0)
INTO expected_index
FROM transfer
WHERE source = NEW.source;
IF expected_index != NEW.index THEN
RAISE EXCEPTION 'next index for transfer from % should be %', NEW.source, expected_index;
END IF;
RETURN NEW;
END;
$BODY$
LANGUAGE PLPGSQL;
CREATE TRIGGER transfer_check_indexing
BEFORE INSERT ON transfer
FOR EACH ROW EXECUTE PROCEDURE check_indexing();
-- Naive account initialization to get rid of unnecessary round-trips to DB
CREATE FUNCTION init_account(UUID, NUMERIC)
RETURNS VOID AS $BODY$
DECLARE
next_index INTEGER;
BEGIN
INSERT INTO account(id)
VALUES ($1);
IF $2 != 0 THEN
SELECT COALESCE(MAX(index) + 1, 0)
INTO next_index
FROM transfer
WHERE source = '00000000-0000-0000-0000-000000000000';
INSERT INTO transfer(source, index, destination, amount)
VALUES ('00000000-0000-0000-0000-000000000000', next_index, $1, $2);
END IF;
END;
$BODY$
LANGUAGE PLPGSQL;
-- Testing view to see all accounts and their metadata, not used in service directly
CREATE VIEW combined_metadata AS
SELECT
a.id,
COALESCE(incoming_sum, 0) - COALESCE(outgoing_sum, 0) AS balance,
COALESCE(last_index + 1, 0) AS next_transfer_index
-- Get all account ids
FROM (SELECT
id
FROM account) a
-- Join with sum of all incoming transfers
LEFT JOIN (SELECT
destination AS id,
SUM(amount) AS incoming_sum
FROM transfer
GROUP BY destination) i ON i.id = a.id
-- Join with sum of all outgoing transfers and last transfer index
LEFT JOIN (SELECT
source AS id,
SUM(amount) AS outgoing_sum,
MAX(index) AS last_index
FROM transfer
GROUP BY source) o ON o.id = a.id;