-
Notifications
You must be signed in to change notification settings - Fork 8
Expand file tree
/
Copy pathcookbook.sql
More file actions
192 lines (177 loc) · 6.94 KB
/
Copy pathcookbook.sql
File metadata and controls
192 lines (177 loc) · 6.94 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
-- Every tested recipe from doc/cookbook.md, verbatim. If this test passes,
-- the cookbook's "Tested recipes" section is honest.
-- Validate data with filter_var
CREATE FUNCTION is_valid_email(text) RETURNS boolean
LANGUAGE plphp IMMUTABLE STRICT AS $$
return filter_var($args[0], FILTER_VALIDATE_EMAIL) !== false;
$$;
SELECT is_valid_email('jd@example.com');
SELECT is_valid_email('not-an-email');
CREATE TABLE subscribers (
email text PRIMARY KEY CHECK (is_valid_email(email))
);
INSERT INTO subscribers VALUES ('jd@example.com');
INSERT INTO subscribers VALUES ('bogus@@nope');
-- bcrypt passwords without pgcrypto
CREATE FUNCTION hash_password(text) RETURNS text
LANGUAGE plphp STRICT AS $$
return password_hash($args[0], PASSWORD_BCRYPT);
$$;
CREATE FUNCTION check_password(plain text, hashed text) RETURNS boolean
LANGUAGE plphp STRICT AS $$
return password_verify($args[0], $args[1]);
$$;
SELECT check_password('s3cret', hash_password('s3cret'));
SELECT check_password('wrong', hash_password('s3cret'));
-- HMAC-signed tokens
CREATE FUNCTION sign_token(payload text, secret text) RETURNS text
LANGUAGE plphp STRICT AS $$
return $args[0] . "." . hash_hmac("sha256", $args[0], $args[1]);
$$;
CREATE FUNCTION verify_token(token text, secret text) RETURNS text
LANGUAGE plphp STRICT AS $$
$pos = strrpos($args[0], ".");
if ($pos === false)
return null;
$payload = substr($args[0], 0, $pos);
$mac = substr($args[0], $pos + 1);
return hash_equals(hash_hmac("sha256", $payload, $args[1]), $mac)
? $payload : null;
$$;
SELECT sign_token('user=jd;exp=2038-01-19', 'hunter2');
SELECT verify_token(sign_token('user=jd;exp=2038-01-19', 'hunter2'), 'hunter2');
SELECT verify_token('user=jd;exp=2038-01-19.deadbeef', 'hunter2') IS NULL AS tampered;
-- Reshape JSON recursively
CREATE FUNCTION jsonb_redact(doc jsonb, keys text[]) RETURNS jsonb
LANGUAGE plphp STRICT AS $$
$doc = json_decode($args[0], true);
$keys = $args[1];
$walk = function (&$node) use (&$walk, $keys) {
foreach ($node as $k => &$v) {
if (in_array((string) $k, $keys, true))
$v = "[redacted]";
elseif (is_array($v))
$walk($v);
}
};
$walk($doc);
return json_encode($doc);
$$;
SELECT jsonb_redact(
'{"user":"jd","password":"x","profile":{"api_key":"y","city":"Olympia"}}',
ARRAY['password', 'api_key']);
-- Extract with a regex, return a set
CREATE FUNCTION extract_urls(text) RETURNS SETOF text
LANGUAGE plphp IMMUTABLE STRICT AS $$
preg_match_all('~https?://[^\s<>")\']+~', $args[0], $m);
foreach ($m[0] as $url)
return_next($url);
return;
$$;
SELECT extract_urls('see https://example.com/a and http://foo.bar/b?x=1 today');
-- A generic audit trigger
CREATE TABLE audit_log (tab text, op text, changed jsonb);
CREATE TABLE accounts (id int primary key, owner text, balance int);
CREATE FUNCTION audit() RETURNS trigger LANGUAGE plphp AS $$
global $_SHARED;
$old = $_TD['event'] == 'INSERT' ? array() : $_TD['old'];
$new = $_TD['event'] == 'DELETE' ? array() : $_TD['new'];
$diff = array();
foreach ($new as $k => $v)
if (!array_key_exists($k, $old) || $old[$k] !== $v)
$diff[$k] = array("from" => $old[$k] ?? null, "to" => $v);
foreach ($old as $k => $v)
if (!array_key_exists($k, $new))
$diff[$k] = array("from" => $v, "to" => null);
if (!isset($_SHARED['audit_plan']))
$_SHARED['audit_plan'] = spi_prepare(
'insert into audit_log values ($1, $2, $3)',
'text', 'text', 'jsonb');
spi_exec_prepared($_SHARED['audit_plan'],
$_TD['relname'], $_TD['event'],
json_encode($diff ?: new stdClass()));
return;
$$;
CREATE TRIGGER t_audit AFTER INSERT OR UPDATE OR DELETE ON accounts
FOR EACH ROW EXECUTE PROCEDURE audit();
INSERT INTO accounts VALUES (1, 'jd', 100);
UPDATE accounts SET balance = 250 WHERE id = 1;
UPDATE accounts SET balance = 250 WHERE id = 1; -- no-op change
DELETE FROM accounts WHERE id = 1;
SELECT * FROM audit_log;
-- Batch processing with periodic commits
CREATE TABLE queue (id int primary key, done boolean default false);
INSERT INTO queue SELECT g FROM generate_series(1, 10) g;
CREATE PROCEDURE process_queue(batch_size int) LANGUAGE plphp AS $$
while (true) {
$r = spi_exec("update queue set done = true
where id in (select id from queue
where not done
order by id limit {$args[0]})");
if (spi_processed($r) == 0)
break;
pg_raise('notice', 'processed ' . spi_processed($r) . ' rows');
spi_commit();
}
$$;
CALL process_queue(4);
SELECT count(*) FILTER (WHERE done) AS done, count(*) AS total FROM queue;
-- Stream a big scan, stop early
CREATE TABLE ledger (id int primary key, amount numeric);
INSERT INTO ledger SELECT g, 10 FROM generate_series(1, 100000) g;
CREATE FUNCTION first_exceeding(threshold numeric) RETURNS int
LANGUAGE plphp STRICT AS $$
$c = spi_query("select id, amount from ledger order by id");
$total = 0;
while ($row = spi_fetchrow($c)) {
$total += $row['amount'];
if ($total > $args[0]) {
spi_cursor_close($c);
return $row['id'];
}
}
return null;
$$;
SELECT first_exceeding(255);
-- Read a CSV file into rows
CREATE FUNCTION write_demo_csv(path text) RETURNS void LANGUAGE plphp STRICT AS $$
file_put_contents($args[0],
"bolt,\"14 mm, zinc\",9\nnut,\"quoted \"\"name\"\"\",4\n");
$$;
CREATE FUNCTION load_csv(path text)
RETURNS TABLE (a text, b text, c text)
LANGUAGE plphp STRICT AS $$
$fh = fopen($args[0], "r");
if ($fh === false)
pg_raise('error', "cannot open {$args[0]}");
while (($rec = fgetcsv($fh, null, ",", "\"", "\\")) !== false)
return_next($rec);
fclose($fh);
return;
$$;
SELECT write_demo_csv('/tmp/plphp_cookbook.csv');
SELECT * FROM load_csv('/tmp/plphp_cookbook.csv');
-- Shred XML into rows
CREATE FUNCTION xml_items(doc xml)
RETURNS TABLE (name text, qty int)
LANGUAGE plphp STRICT AS $$
$doc = simplexml_load_string($args[0]);
foreach ($doc->item as $item)
return_next(array((string) $item['name'], (int) $item['qty']));
return;
$$;
SELECT * FROM xml_items('<order><item name="bolt" qty="9"/><item name="nut" qty="4"/></order>');
-- Compress large text into bytea
CREATE FUNCTION gz(text) RETURNS bytea
LANGUAGE plphp IMMUTABLE STRICT AS $$
return "\\x" . bin2hex(gzcompress($args[0], 9));
$$;
CREATE FUNCTION gunz(bytea) RETURNS text
LANGUAGE plphp IMMUTABLE STRICT AS $$
return gzuncompress(hex2bin(substr($args[0], 2)));
$$;
SELECT gunz(gz(repeat('squeeze me, ', 500))) = repeat('squeeze me, ', 500) AS roundtrip,
octet_length(gz(repeat('squeeze me, ', 500))) < 100 AS compressed;
-- clean up
DO $$ @unlink('/tmp/plphp_cookbook.csv'); $$ LANGUAGE plphp;
DROP TABLE subscribers, audit_log, accounts, queue, ledger;