├── META.json
├── Makefile
├── README.md
├── expected
└── test.out
├── pg_readonly--1.0.0.sql
├── pg_readonly.c
├── pg_readonly.conf
├── pg_readonly.control
└── sql
└── test.sql
/META.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pg_readonly",
3 | "abstract": "set all cluster databases in read only",
4 | "version": "1.0.3",
5 | "maintainer": [
6 | "Pierre Forstmann"
7 | ],
8 | "license": {
9 | "PostgreSQL": "http://www.postgresql.org/about/licence"
10 | },
11 | "prereqs": {
12 | "runtime": {
13 | "requires": {
14 | "PostgreSQL": "9.5.21"
15 | },
16 | "recommends": {
17 | "PostgreSQL": "9.5.21"
18 | }
19 | }
20 | },
21 | "provides": {
22 | "pg_readonly": {
23 | "abstract": "set all cluster databases in read only",
24 | "file": "pg_readonly.c",
25 | "docfile": "README.md",
26 | "version": "1.0.3"
27 | }
28 | },
29 | "resources": {
30 | "bugtracker": {
31 | "web": "http://github.com/pierreforstmann/pg_readonly/issues"
32 | },
33 | "repository": {
34 | "url": "https://github.com/pierreforstmann/pg_readonly.git",
35 | "web": "https://github.com/pierreforstmann/pg_readonly",
36 | "type": "git"
37 | }
38 | },
39 | "generated_by": "Pierre Forstmann",
40 | "meta-spec": {
41 | "version": "1.0.0",
42 | "url": "http://pgxn.org/meta/spec.txt"
43 | },
44 | "tags": [
45 | "read only",
46 | "cluster"
47 | ]
48 | }
49 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | #
2 | # Makefile
3 | #
4 | MODULES = pg_readonly
5 | EXTENSION = pg_readonly # the extension's name
6 | DATA = pg_readonly--1.0.0.sql # script file to install
7 |
8 | # make installcheck to run automatic test
9 | REGRESS_OPTS = --temp-instance=/tmp/5555 --port=5555 --temp-config pg_readonly.conf
10 | REGRESS = test
11 |
12 | PG_CONFIG = pg_config
13 | PGXS := $(shell $(PG_CONFIG) --pgxs)
14 | include $(PGXS)
15 |
16 | pgxn:
17 | git archive --format zip --output ../pgxn/pg_readonly/pg_readonly-1.0.3.zip master
18 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # pg_readonly
2 | pg_readonly is a PostgreSQL extension which allows to set all cluster databases read only.
3 |
4 |
5 | # Installation
6 | ## Compiling
7 |
8 | This module can be built using the standard PGXS infrastructure. For this to work, the pg_config program must be available in your $PATH:
9 |
10 | `git clone https://github.com/pierreforstmann/pg_readonly.git`
11 | `cd pg_readonly`
12 | `make`
13 | `make install`
14 |
15 | This extension has been validated with PostgreSQL 9.5, 9.6, 10, 11, 12, 13, 14, 15, 16 and 17.
16 |
17 | ## PostgreSQL setup
18 |
19 | Extension must be loaded at server level with `shared_preload_libraries` parameter:
20 |
21 | `shared_preload_libraries = 'pg_readonly'`
22 |
23 | and it must be created with following SQL statement at server level:
24 |
25 | `create extension pg_readonly;`
26 |
27 |
28 |
29 | ## Usage
30 | pg_readonly has no specific GUC.
31 | The read-only status is managed only in (shared) memory with a global flag. SQL functions are provided to set the flag, to unset the flag and to query the flag.
32 | The current version of the extension does not allow to store the read-only status in a permanent way.
33 | The flag is at cluster level: either all databases are read-only or all database are read-write (the usual setting).
34 | The read-only mode is implemented by filtering SQL statements:
35 |
36 | - SELECT statements are allowed if they don't call functions that write.
37 | - DML (INSERT, UPDATE, DELETE) and DDL statements including TRUNCATE are forbidden entirely.
38 | - DCL statements GRANT and REVOKE are also forbidden.
39 |
40 | This means that the databases are in read-only mode at SQL level: however, the checkpointer, background writer, walwriter and the autovacuum launcher are still running; this means that the database files are not read-only and that in some cases the database may still write to disk.
41 |
42 |
43 | ## Example
44 |
45 | To query the cluster status, call the function get_cluster_readonly which returns true is the cluster is read-only and false if not:
46 |
47 | `# select get_cluster_readonly();`
48 | ` get_cluster_readonly `
49 | `----------------------`
50 | ` f`
51 | `(1 row)`
52 |
53 | To set the cluster read-only, call the function set_cluster_readonly:
54 | `# select set_cluster_readonly();`
55 | ` set_cluster_readonly `
56 | `----------------------`
57 | ` t`
58 | `(1 row)`
59 |
60 | The cluster is now read-only and only SELECT statements are allowed:
61 |
62 | `pierre=# select * from t;`
63 | ` x | y `
64 | `----+-----`
65 | ` 32 | abc`
66 | `(1 row)`
67 |
68 | `# update t set x=33 where y='abc';`
69 | `ERROR: pg_readonly: invalid statement because cluster is read-only`
70 | `# select 1 into tmp;`
71 | `ERROR: pg_readonly: invalid statement because cluster is read-only`
72 | `# create table tmp(c text);`
73 | `ERROR: pg_readonly: invalid statement because cluster is read-only`
74 |
75 | To set the cluster on read-write, call the function unset_cluster_readonly:
76 |
77 | `# select unset_cluster_readonly();`
78 | ` unset_cluster_readonly `
79 | `------------------------`
80 | ` t`
81 | `(1 row)`
82 |
83 | The cluster is now read-write and any DML or DDL statement is allowed:
84 | `# update t set x=33 where y='abc';`
85 | `UPDATE 1`
86 | `# select * from t;`
87 | ` x | y `
88 | `----+-----`
89 | ` 33 | abc`
90 | `(1 row)`
91 |
92 | Note that any open transaction is cancelled by set_cluster_readonly function.
93 | The client is disconnected and gets the following message:
94 | `FATAL: terminating connection due to conflict with recovery`
95 | `DETAIL: User query might have needed to see row versions that must be removed.`
96 | `HINT: In a moment you should be able to reconnect to the database and repeat your command.`
97 | In PostgreSQL log, following messages are written:
98 |
99 | `2020-04-14 16:00:14.531 CEST [29578] LOG: pg_readonly: killing all transactions ...`
100 | `2020-04-14 16:00:14.531 CEST [29578] LOG: pg_readonly: PID 29569 signalled`
101 | `2020-04-14 16:00:14.531 CEST [29578] LOG: pg_readonly: ... done.`
102 | `2020-04-14 16:00:14.531 CEST [29569] FATAL: terminating connection due to conflict with recovery`
103 | `2020-04-14 16:00:14.531 CEST [29569] DETAIL: User query might have needed to see row versions that must be removed.`
104 | `2020-04-14 16:00:14.531 CEST [29569] HINT: In a moment you should be able to reconnect to the database and repeat your command.`
105 |
106 |
107 |
--------------------------------------------------------------------------------
/expected/test.out:
--------------------------------------------------------------------------------
1 | --
2 | -- test.sql
3 | --
4 | create extension pg_readonly;
5 | --
6 | select get_cluster_readonly();
7 | get_cluster_readonly
8 | ----------------------
9 | f
10 | (1 row)
11 |
12 | select unset_cluster_readonly();
13 | unset_cluster_readonly
14 | ------------------------
15 | t
16 | (1 row)
17 |
18 | --
19 | drop table t;
20 | ERROR: table "t" does not exist
21 | drop function f;
22 | ERROR: could not find a function named "f"
23 | --
24 | create table t(i int);
25 | create function f(IN p int, OUT i int) returns int as $$
26 | begin
27 | insert into t(i) values (f.p) returning t.i into f.i;
28 | end$$
29 | language plpgsql;
30 | select set_cluster_readonly();
31 | set_cluster_readonly
32 | ----------------------
33 | t
34 | (1 row)
35 |
36 | select * from t;
37 | i
38 | ---
39 | (0 rows)
40 |
41 | insert into t values (1);
42 | ERROR: pg_readonly: pgro_main: invalid statement because cluster is read-only
43 | select f(1);
44 | ERROR: pg_readonly: pgro_main: invalid statement because cluster is read-only
45 | CONTEXT: SQL statement "insert into t(i) values (f.p) returning t.i"
46 | PL/pgSQL function f(integer) line 3 at SQL statement
47 | -- However, in this same session, if you call f() before entering in read only mode,
48 | -- it is then able to write during read only mode:
49 | select unset_cluster_readonly();
50 | unset_cluster_readonly
51 | ------------------------
52 | t
53 | (1 row)
54 |
55 | select f(1);
56 | f
57 | ---
58 | 1
59 | (1 row)
60 |
61 | select set_cluster_readonly();
62 | set_cluster_readonly
63 | ----------------------
64 | t
65 | (1 row)
66 |
67 | insert into t values (2);
68 | ERROR: pg_readonly: pgro_main: invalid statement because cluster is read-only
69 | select f(2);
70 | ERROR: pg_readonly: pgro_exec: invalid statement because cluster is read-only
71 | CONTEXT: SQL statement "insert into t(i) values (f.p) returning t.i"
72 | PL/pgSQL function f(integer) line 3 at SQL statement
73 | select get_cluster_readonly();
74 | get_cluster_readonly
75 | ----------------------
76 | t
77 | (1 row)
78 |
79 | select f(3);
80 | ERROR: pg_readonly: pgro_exec: invalid statement because cluster is read-only
81 | CONTEXT: SQL statement "insert into t(i) values (f.p) returning t.i"
82 | PL/pgSQL function f(integer) line 3 at SQL statement
83 | select * from t;
84 | i
85 | ---
86 | 1
87 | (1 row)
88 |
89 |
--------------------------------------------------------------------------------
/pg_readonly--1.0.0.sql:
--------------------------------------------------------------------------------
1 | -- \echo Use "CREATE EXTENSION pg_readonly" to load this file . \quit
2 | DROP FUNCTION IF EXISTS set_cluster_readonly();
3 | DROP FUNCTION IF EXISTS unset_cluster_readonly();
4 | DROP FUNCTION IF EXISTS get_cluster_readonly();
5 | --
6 | CREATE FUNCTION set_cluster_readonly() RETURNS bool
7 | AS 'pg_readonly.so', 'pgro_set_readonly'
8 | LANGUAGE C STRICT;
9 | --
10 | CREATE FUNCTION unset_cluster_readonly() RETURNS bool
11 | AS 'pg_readonly.so', 'pgro_unset_readonly'
12 | LANGUAGE C STRICT;
13 | --
14 | CREATE FUNCTION get_cluster_readonly() RETURNS bool
15 | AS 'pg_readonly.so', 'pgro_get_readonly'
16 | LANGUAGE C STRICT;
17 | --
18 |
19 |
--------------------------------------------------------------------------------
/pg_readonly.c:
--------------------------------------------------------------------------------
1 | /*-------------------------------------------------------------------------
2 | *
3 | * pg_readonly is a PostgreSQL extension which allows to set a whole
4 | * cluster read only: no INSERT,UPDATE,DELETE and no DDL can be run.
5 | *
6 | * This program is open source, licensed under the PostgreSQL license.
7 | * For license terms, see the LICENSE file.
8 | *
9 | * Copyright (c) 2020, Pierre Forstmann.
10 | *
11 | *-------------------------------------------------------------------------
12 | */
13 | #include "postgres.h"
14 | #include "parser/analyze.h"
15 | #include "nodes/nodes.h"
16 | #include "storage/proc.h"
17 | #include "access/xact.h"
18 |
19 | #include "tcop/tcopprot.h"
20 | #include "tcop/utility.h"
21 | #include "utils/guc.h"
22 | #include "utils/snapmgr.h"
23 | #include "utils/memutils.h"
24 | #if PG_VERSION_NUM <= 90600
25 | #include "storage/lwlock.h"
26 | #endif
27 | #if PG_VERSION_NUM < 120000
28 | #include "access/transam.h"
29 | #endif
30 |
31 | #include "storage/ipc.h"
32 | #include "storage/spin.h"
33 | #include "miscadmin.h"
34 | #include "storage/procarray.h"
35 | #include "executor/executor.h"
36 |
37 | PG_MODULE_MAGIC;
38 |
39 | /*
40 | * has set_cluster_readonly() been executed
41 | * in the current backend.
42 | */
43 | static bool read_only_flag_has_been_set = false;
44 |
45 | /*
46 | *
47 | * Global shared state
48 | *
49 | */
50 | typedef struct pgroSharedState
51 | {
52 | LWLock *lock; /* self protection */
53 | bool cluster_is_readonly; /* cluster read-only global flag */
54 | } pgroSharedState;
55 |
56 | /* Saved hook values in case of unload */
57 | #if PG_VERSION_NUM >= 150000
58 | static shmem_request_hook_type prev_shmem_request_hook = NULL;
59 | #endif
60 | static post_parse_analyze_hook_type prev_post_parse_analyze_hook = NULL;
61 | static shmem_startup_hook_type prev_shmem_startup_hook = NULL;
62 | static ExecutorStart_hook_type prev_executor_start_hook = NULL;
63 |
64 | /* Links to shared memory state */
65 | static pgroSharedState *pgro= NULL;
66 |
67 | static bool pgro_enabled = false;
68 |
69 | /*---- Function declarations ----*/
70 |
71 | void _PG_init(void);
72 | void _PG_fini(void);
73 |
74 | static void pgro_shmem_request(void);
75 | static void pgro_shmem_startup(void);
76 | static void pgro_shmem_shutdown(int code, Datum arg);
77 | #if PG_VERSION_NUM < 140000
78 | static void pgro_main(ParseState *pstate, Query *query);
79 | #else
80 | static void pgro_main(ParseState *pstate, Query *query, JumbleState *jstate);
81 | #endif
82 | static void pgro_exec(QueryDesc *queryDesc, int eflags);
83 |
84 | static bool pgro_set_readonly_internal();
85 | static bool pgro_unset_readonly_internal();
86 | static bool pgro_get_readonly_internal();
87 |
88 | PG_FUNCTION_INFO_V1(pgro_set_readonly);
89 | PG_FUNCTION_INFO_V1(pgro_unset_readonly);
90 | PG_FUNCTION_INFO_V1(pgro_get_readonly);
91 |
92 |
93 | /*
94 | * set cluster databases to read-only
95 | */
96 |
97 | static bool pgro_set_readonly_internal()
98 | {
99 |
100 | VirtualTransactionId *tvxid;
101 | TransactionId limitXmin = InvalidTransactionId;
102 | bool excludeXmin0 = false;
103 | bool allDbs = true;
104 | int excludeVacuum = 0;
105 | int nvxids;
106 | int i;
107 | pid_t pid;
108 |
109 | elog(LOG, "pg_readonly: killing all transactions ...");
110 | tvxid = GetCurrentVirtualXIDs(
111 | limitXmin,
112 | excludeXmin0,
113 | allDbs,
114 | excludeVacuum,
115 | &nvxids);
116 | for (i=0; i < nvxids; i++)
117 | {
118 | /*
119 | * No adequate ProcSignalReason found
120 | */
121 | pid = CancelVirtualTransaction(
122 | tvxid[i],
123 | PROCSIG_RECOVERY_CONFLICT_SNAPSHOT);
124 | elog(LOG, "pg_readonly: PID %d signalled", pid);
125 | }
126 | elog(LOG, "pg_readonly: ... done.");
127 |
128 |
129 | LWLockAcquire(pgro->lock, LW_EXCLUSIVE);
130 | pgro->cluster_is_readonly = true;
131 | LWLockRelease(pgro->lock);
132 | return true;
133 | }
134 |
135 |
136 | /*
137 | * set cluster databases to read write
138 | */
139 |
140 | static bool pgro_unset_readonly_internal()
141 | {
142 | LWLockAcquire(pgro->lock, LW_EXCLUSIVE);
143 | pgro->cluster_is_readonly = false;
144 | LWLockRelease(pgro->lock);
145 | return true;
146 | }
147 |
148 |
149 | /*
150 | * get cluster databases read-only or
151 | * read-write status
152 | */
153 |
154 | static bool pgro_get_readonly_internal()
155 | {
156 | bool val;
157 |
158 | LWLockAcquire(pgro->lock, LW_SHARED);
159 | val = pgro->cluster_is_readonly;
160 | LWLockRelease(pgro->lock);
161 | return val;
162 | }
163 |
164 | /*
165 | * set cluster databases to read-only
166 | */
167 | Datum pgro_set_readonly(PG_FUNCTION_ARGS)
168 | {
169 | if (pgro_enabled == false)
170 | {
171 | ereport(ERROR, (errmsg("pg_readonly: pgro_set_readonly: pg_readonly is not enabled")));
172 | PG_RETURN_BOOL(false);
173 | }
174 | else
175 | {
176 | elog(DEBUG5, "pg_readonly: pgro_set_readonly: entry");
177 | elog(DEBUG5, "pg_readonly: pgro_set_readonly: exit");
178 | read_only_flag_has_been_set = true;
179 | PG_RETURN_BOOL(pgro_set_readonly_internal());
180 | }
181 | }
182 |
183 | /*
184 | * set cluster databases to read-write
185 | */
186 | Datum pgro_unset_readonly(PG_FUNCTION_ARGS)
187 | {
188 | if (pgro_enabled == false)
189 | {
190 | ereport(ERROR, (errmsg("pg_readonly: pgro_unset_readonly: pg_readonly is not enabled")));
191 | PG_RETURN_BOOL(false);
192 | }
193 | else
194 | {
195 | elog(DEBUG5, "pg_readonly: pgro_unset_readonly: entry");
196 | elog(DEBUG5, "pg_readonly: pgro_unset_readonly: exit");
197 | read_only_flag_has_been_set = false;
198 | PG_RETURN_BOOL(pgro_unset_readonly_internal());
199 | }
200 |
201 | }
202 |
203 | /*
204 | * get cluster databases status
205 | */
206 | Datum pgro_get_readonly(PG_FUNCTION_ARGS)
207 | {
208 | if (pgro_enabled == false)
209 | {
210 | ereport(ERROR, (errmsg("pg_readonly: pgro_get_readonly: pg_readonly is not enabled")));
211 | PG_RETURN_BOOL(false);
212 | }
213 | else
214 | {
215 | elog(DEBUG5, "pg_readonly: pgro_get_readonly: entry");
216 | elog(DEBUG5, "pg_readonly: pgro_get_readonly: exit");
217 | PG_RETURN_BOOL(pgro_get_readonly_internal());
218 | }
219 |
220 | }
221 |
222 | /*
223 | ** Estimate shared memory space needed.
224 | *
225 | **/
226 | static Size
227 | pgro_memsize(void)
228 | {
229 | Size size;
230 |
231 | size = MAXALIGN(sizeof(pgroSharedState));
232 |
233 | return size;
234 | }
235 |
236 | /*
237 | *
238 | * shmen_request_hook
239 | */
240 | static void
241 | pgro_shmem_request(void)
242 | {
243 | /*
244 | * Request additional shared resources. (These are no-ops if we're not in
245 | * the postmaster process.) We'll allocate or attach to the shared
246 | * resources in pgls_shmem_startup().
247 | */
248 |
249 | #if PG_VERSION_NUM >= 150000
250 | if (prev_shmem_request_hook)
251 | prev_shmem_request_hook();
252 | #endif
253 |
254 | RequestAddinShmemSpace(sizeof(pgroSharedState));
255 | #if PG_VERSION_NUM >= 90600
256 | RequestNamedLWLockTranche("pg_readonly", 1);
257 | #endif
258 |
259 | }
260 |
261 |
262 | /*
263 | * shmem_startup hook: allocate or attach to shared memory.
264 | *
265 | */
266 | static void
267 | pgro_shmem_startup(void)
268 | {
269 | bool found;
270 |
271 | elog(DEBUG5, "pg_readonly: pgro_shmem_startup: entry");
272 |
273 | if (prev_shmem_startup_hook)
274 | prev_shmem_startup_hook();
275 |
276 | /* reset in case this is a restart within the postmaster */
277 | pgro = NULL;
278 |
279 |
280 | /*
281 | ** Create or attach to the shared memory state
282 | **/
283 | LWLockAcquire(AddinShmemInitLock, LW_EXCLUSIVE);
284 |
285 | pgro = ShmemInitStruct("pg_readonly",
286 | pgro_memsize(),
287 | &found);
288 |
289 | if (!found)
290 | {
291 | /* First time through ... */
292 | #if PG_VERSION_NUM <= 90600
293 | RequestAddinLWLocks(1);
294 | pgro->lock = LWLockAssign();
295 | #else
296 | pgro->lock = &(GetNamedLWLockTranche("pg_readonly"))->lock;
297 | #endif
298 |
299 | pgro->cluster_is_readonly = false;
300 | }
301 |
302 | LWLockRelease(AddinShmemInitLock);
303 |
304 | /*
305 | * If we're in the postmaster (or a standalone backend...), set up a shmem
306 | * exit hook (no current need ???)
307 | */
308 | if (!IsUnderPostmaster)
309 | on_shmem_exit(pgro_shmem_shutdown, (Datum) 0);
310 |
311 | /*
312 | * Done if some other process already completed our initialization.
313 | */
314 | if (found)
315 | return;
316 |
317 | elog(DEBUG5, "pg_readonly: pgro_shmem_startup: exit");
318 |
319 | }
320 |
321 | /*
322 | *
323 | * shmem_shutdown hook
324 | *
325 | * Note: we don't bother with acquiring lock, because there should be no
326 | * other processes running when this is called.
327 | */
328 | static void
329 | pgro_shmem_shutdown(int code, Datum arg)
330 | {
331 | elog(DEBUG5, "pg_readonly: pgro_shmem_shutdown: entry");
332 |
333 | /* Don't do anything during a crash. */
334 | if (code)
335 | return;
336 |
337 | /* Safety check ... shouldn't get here unless shmem is set up. */
338 | if (!pgro)
339 | return;
340 |
341 | /* currently: no action */
342 |
343 | elog(DEBUG5, "pg_readonly: pgro_shmem_shutdown: exit");
344 | }
345 |
346 |
347 | /*
348 | * Module load callback
349 | */
350 | void
351 | _PG_init(void)
352 | {
353 |
354 | const char *shared_preload_libraries_config;
355 | char *pg_readonly;
356 |
357 | elog(DEBUG5, "pg_readonly: _PG_init(): entry");
358 |
359 | shared_preload_libraries_config = GetConfigOption("shared_preload_libraries", true, false);
360 | pg_readonly = strstr(shared_preload_libraries_config, "pg_readonly");
361 | if (pg_readonly == NULL)
362 | {
363 | ereport(WARNING, (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
364 | errmsg("pg_readonly: pg_readonly is not loaded")));
365 | pgro_enabled = false;
366 | }
367 | else
368 | pgro_enabled = true;
369 |
370 | if (pgro_enabled)
371 | elog(LOG, "pg_readonly:_PG_init(): pg_readonly extension is enabled");
372 | else ereport(LOG, (errmsg("pg_readonly:_PG_init(): pg_readonly is not enabled")));
373 |
374 |
375 | /*
376 | ** Install hooks
377 | */
378 |
379 | if (pgro_enabled)
380 | {
381 | #if PG_VERSION_NUM >= 150000
382 | prev_shmem_request_hook = shmem_request_hook;
383 | shmem_request_hook = pgro_shmem_request;
384 | #else
385 | pgro_shmem_request();
386 | #endif
387 | prev_shmem_startup_hook = shmem_startup_hook;
388 | shmem_startup_hook = pgro_shmem_startup;
389 | prev_post_parse_analyze_hook = post_parse_analyze_hook;
390 | prev_executor_start_hook = ExecutorStart_hook;
391 | post_parse_analyze_hook = pgro_main;
392 | ExecutorStart_hook = pgro_exec;
393 | }
394 |
395 | elog(DEBUG5, "pg_readonly: _PG_init(): exit");
396 | }
397 |
398 |
399 | /*
400 | * Module unload callback
401 | */
402 | void
403 | _PG_fini(void)
404 | {
405 | elog(DEBUG5, "pg_readonly: _PG_fini(): entry");
406 |
407 | /* Uninstall hooks. */
408 | shmem_startup_hook = prev_shmem_startup_hook;
409 | post_parse_analyze_hook = prev_post_parse_analyze_hook;
410 | ExecutorStart_hook = prev_executor_start_hook;
411 |
412 | elog(DEBUG5, "pg_readonly: _PG_fini(): exit");
413 | }
414 |
415 | /*
416 | *
417 | */
418 |
419 | static void
420 | #if PG_VERSION_NUM < 140000
421 | pgro_main(ParseState *pstate, Query *query)
422 | #else
423 | pgro_main(ParseState *pstate, Query *query, JumbleState *jstate)
424 | #endif
425 | {
426 |
427 | char *sekw = "SELECT";
428 | char *inkw = "INSERT";
429 | char *upkw = "UPDATE";
430 | char *dekw = "DELETE";
431 | char *utkw = "UTILITY";
432 | char *nokw = "NOTHING";
433 | char *unkw = "UNKNOWN";
434 | char *kokw = "???????";
435 | char *kw = NULL;
436 | char *expstmt = "EXPLAIN";
437 | char *setvstmt = "SET";
438 | char *showvstmt = "SHOW";
439 | char *prepstmt = "PREPARE";
440 | char *execstmt = "EXECUTE";
441 | char *deallocstmt = "DEALLOC";
442 | char *otherstmt = "OTHER";
443 | char *stmt = NULL;
444 | bool command_is_ro = false;
445 |
446 | elog(DEBUG5, "pg_readonly: pgro_main entry");
447 |
448 | switch (query->commandType) {
449 | case CMD_UNKNOWN:
450 | kw = unkw;
451 | break;
452 | case CMD_SELECT:
453 | command_is_ro = true;
454 | kw = sekw;
455 | break;
456 | case CMD_UPDATE:
457 | kw = upkw;
458 | break;
459 | case CMD_INSERT:
460 | kw = inkw;
461 | break;
462 | case CMD_DELETE:
463 | kw = dekw;
464 | break;
465 | case CMD_UTILITY:
466 | kw = utkw;
467 | /*
468 | * allow ROLLBACK
469 | * for killed transactions
470 | */
471 | if (
472 | strstr((pstate->p_sourcetext), "rollback")
473 | ||
474 | strstr((pstate->p_sourcetext), "ROLLBACK")
475 | )
476 | {
477 | elog(DEBUG1, "pg_readonly: pgro_main: query->querySource=%s",
478 | pstate->p_sourcetext);
479 | command_is_ro = true;
480 | }
481 | break;
482 | case CMD_NOTHING:
483 | kw = nokw;
484 | break;
485 | default:
486 | kw = kokw;
487 | break;
488 |
489 | }
490 | elog(DEBUG1, "pg_readonly: pgro_main: query->commandType=%s", kw);
491 | elog(DEBUG1, "pg_readonly: pgro_main: command_is_ro=%d", command_is_ro);
492 |
493 |
494 |
495 | if (query->commandType == CMD_UTILITY)
496 | {
497 | switch ((nodeTag(query->utilityStmt)))
498 | {
499 | case T_ExplainStmt:
500 | stmt = expstmt;
501 | command_is_ro = true;
502 | break;
503 | case T_VariableSetStmt:
504 | stmt = setvstmt;
505 | command_is_ro = true;
506 | break;
507 | case T_VariableShowStmt:
508 | stmt = showvstmt;
509 | command_is_ro = true;
510 | break;
511 | case T_PrepareStmt:
512 | stmt = prepstmt;
513 | command_is_ro = true;
514 | break;
515 | case T_ExecuteStmt:
516 | stmt = execstmt;
517 | command_is_ro = true;
518 | break;
519 | case T_DeallocateStmt:
520 | stmt = deallocstmt;
521 | command_is_ro = true;
522 | break;
523 | default:
524 | stmt = otherstmt;
525 | break;
526 | }
527 | elog(DEBUG1, "pg_readonly: pgro_main: query->UtilityStmt=%s", stmt);
528 | elog(DEBUG1, "pg_readonly: pgro_main: command_is_ro=%d", command_is_ro);
529 | }
530 |
531 | if (pgro_get_readonly_internal() == true && command_is_ro == false)
532 | ereport(ERROR, (errmsg("pg_readonly: pgro_main: invalid statement because cluster is read-only")));
533 |
534 |
535 | if (prev_post_parse_analyze_hook)
536 | #if PG_VERSION_NUM <= 140000
537 | (*prev_post_parse_analyze_hook)(pstate, query);
538 | #else
539 | (*prev_post_parse_analyze_hook)(pstate, query, jstate);
540 | #endif
541 | /* no "standard" call for else branch */
542 |
543 | elog(DEBUG5, "pg_readonly: pgro_main: exit");
544 | }
545 |
546 | static void
547 | pgro_exec(QueryDesc *queryDesc, int eflags)
548 | {
549 | char *ops="select";
550 | char *opi="insert";
551 | char *opu="update";
552 | char *opd="delete";
553 | char *opo="other";
554 | char *op;
555 | bool command_is_ro = false;
556 |
557 | switch (queryDesc->operation)
558 | {
559 | case CMD_SELECT:
560 | op = ops;
561 | command_is_ro = true;
562 | break;
563 | case CMD_INSERT:
564 | op = opi;
565 | command_is_ro = false;
566 | break;
567 | case CMD_UPDATE:
568 | op = opu;
569 | command_is_ro = false;
570 | break;
571 | case CMD_DELETE:
572 | op = opd;
573 | command_is_ro = false;
574 | break;
575 | default:
576 | op=opo;
577 | command_is_ro = false;
578 | break;
579 | }
580 |
581 | elog(LOG, "pg_readonly: pgro_exec: qd->op %s", op);
582 | if (pgro_get_readonly_internal() == true && command_is_ro == false)
583 | ereport(ERROR, (errmsg("pg_readonly: pgro_exec: invalid statement because cluster is read-only")));
584 |
585 | if (prev_executor_start_hook)
586 | (*prev_executor_start_hook)(queryDesc, eflags);
587 | else standard_ExecutorStart(queryDesc, eflags);
588 |
589 | }
590 |
--------------------------------------------------------------------------------
/pg_readonly.conf:
--------------------------------------------------------------------------------
1 | logging_collector=on
2 | log_statement=all
3 | shared_preload_libraries='pg_readonly'
4 |
--------------------------------------------------------------------------------
/pg_readonly.control:
--------------------------------------------------------------------------------
1 | # pg_readonly postgresql extension
2 | comment = 'cluster database read only'
3 | default_version = '1.0.0'
4 | module_pathname = '$libdir/pg_readonly'
5 | relocatable = false
6 |
--------------------------------------------------------------------------------
/sql/test.sql:
--------------------------------------------------------------------------------
1 | --
2 | -- test.sql
3 | --
4 | create extension pg_readonly;
5 | --
6 | select get_cluster_readonly();
7 | select unset_cluster_readonly();
8 | --
9 | drop table t;
10 | drop function f;
11 | --
12 | create table t(i int);
13 |
14 | create function f(IN p int, OUT i int) returns int as $$
15 | begin
16 | insert into t(i) values (f.p) returning t.i into f.i;
17 | end$$
18 | language plpgsql;
19 |
20 | select set_cluster_readonly();
21 |
22 | select * from t;
23 |
24 | insert into t values (1);
25 |
26 | select f(1);
27 | -- However, in this same session, if you call f() before entering in read only mode,
28 | -- it is then able to write during read only mode:
29 |
30 | select unset_cluster_readonly();
31 |
32 | select f(1);
33 |
34 | select set_cluster_readonly();
35 |
36 | insert into t values (2);
37 |
38 | select f(2);
39 |
40 | select get_cluster_readonly();
41 |
42 | select f(3);
43 |
44 | select * from t;
45 |
--------------------------------------------------------------------------------