Skip to content

Commit bc56b00

Browse files
committed
Make age extension usable from shared_preload_libraries
When AGE is loaded via shared_preload_libraries, its hooks (post_parse_analyze, set_rel_pathlist, object_access) are active before CREATE EXTENSION age is run. This causes errors when non-Cypher queries trigger those hooks and ag_catalog does not yet exist. Changes: - Add is_age_extension_exist() with a relcache callback cache so that checking pg_extension is not repeated on every hook invocation. - Guard post_parse_analyze, set_rel_pathlist, and object_access hooks with is_age_extension_exist() so they become no-ops when the extension is not installed. - Refactor ag_ProcessUtility_hook to detect CREATE/DROP EXTENSION age and broadcast a relcache invalidation via CacheInvalidateRelcacheByRelid(ExtensionRelationId) so other backends update their cached extension state. - Wrap DROP EXTENSION processing in PG_TRY/PG_CATCH to restore object_access_hook if the drop fails (e.g. dependent objects). - Skip _PG_init during pg_upgrade (IsBinaryUpgrade) to avoid hook registration when the binary-upgrade machinery is running. - Add regression tests that verify hooks do not error when ag_catalog schema is absent.
1 parent 23cbe57 commit bc56b00

7 files changed

Lines changed: 178 additions & 58 deletions

File tree

regress/expected/drop.out

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@ SELECT tablename FROM pg_catalog.pg_tables WHERE schemaname = 'ag_catalog';
4040
-----------
4141
(0 rows)
4242

43+
-- When ag_catalog is missing extension hooks shouldn't fail with the
44+
-- ERROR schema "ag_catalog" does not exist.
45+
-- It might happen when 'age' is loaded but extension isn't created yet.
46+
DROP SCHEMA ag_catalog;
47+
CREATE SCHEMA _regress_drop;
48+
DROP SCHEMA _regress_drop; -- shouldn't produce the ERROR
4349
-- Recreate the extension and validate we can recreate a graph
4450
CREATE EXTENSION age;
4551
SELECT create_graph('drop');

regress/sql/drop.sql

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@ SELECT nspname FROM pg_catalog.pg_namespace WHERE nspname = 'drop';
2828

2929
SELECT tablename FROM pg_catalog.pg_tables WHERE schemaname = 'ag_catalog';
3030

31+
-- When ag_catalog is missing extension hooks shouldn't fail with the
32+
-- ERROR schema "ag_catalog" does not exist.
33+
-- It might happen when 'age' is loaded but extension isn't created yet.
34+
DROP SCHEMA ag_catalog;
35+
CREATE SCHEMA _regress_drop;
36+
DROP SCHEMA _regress_drop; -- shouldn't produce the ERROR
37+
3138
-- Recreate the extension and validate we can recreate a graph
3239
CREATE EXTENSION age;
3340

src/backend/age.c

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
* under the License.
1818
*/
1919

20+
#include "postgres.h"
21+
#include "miscadmin.h"
22+
2023
#include "catalog/ag_catalog.h"
2124
#include "nodes/ag_nodes.h"
2225
#include "optimizer/cypher_paths.h"
@@ -25,7 +28,6 @@
2528
#include "utils/age_global_graph.h"
2629

2730
#if PG_VERSION_NUM < 170000
28-
#include "miscadmin.h"
2931

3032
/* saved hook pointers for PG < 17 shmem path */
3133
static shmem_request_hook_type prev_shmem_request_hook = NULL;
@@ -56,6 +58,9 @@ void _PG_init(void);
5658

5759
void _PG_init(void)
5860
{
61+
if (IsBinaryUpgrade)
62+
return;
63+
5964
register_ag_nodes();
6065
set_rel_pathlist_init();
6166
object_access_hook_init();

src/backend/catalog/ag_catalog.c

Lines changed: 149 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -19,21 +19,27 @@
1919

2020
#include "postgres.h"
2121

22+
#include "access/xact.h"
2223
#include "catalog/dependency.h"
2324
#include "catalog/namespace.h"
2425
#include "catalog/objectaccess.h"
2526
#include "catalog/pg_class_d.h"
27+
#include "catalog/pg_extension_d.h"
2628
#include "catalog/pg_namespace_d.h"
2729
#include "commands/defrem.h"
30+
#include "commands/extension.h"
2831
#include "nodes/parsenodes.h"
2932
#include "tcop/utility.h"
33+
#include "utils/inval.h"
3034
#include "utils/lsyscache.h"
3135

3236
#include "catalog/ag_graph.h"
3337
#include "catalog/ag_label.h"
3438
#include "utils/ag_cache.h"
3539
#include "utils/age_global_graph.h"
3640

41+
static bool extension_cache_is_valid = false;
42+
static bool age_extension_exists = false;
3743
static object_access_hook_type prev_object_access_hook;
3844
static ProcessUtility_hook_type prev_process_utility_hook;
3945
static bool prev_object_hook_is_set;
@@ -45,8 +51,44 @@ void ag_ProcessUtility_hook(PlannedStmt *pstmt, const char *queryString, bool re
4551
QueryEnvironment *queryEnv, DestReceiver *dest,
4652
QueryCompletion *qc);
4753

48-
static bool is_age_drop(PlannedStmt *pstmt);
49-
static void drop_age_extension(DropStmt *stmt);
54+
static bool is_age_drop(DropStmt *drop_stmt);
55+
56+
static void
57+
invalidate_extension_cache_callback(Datum argument, Oid relationId)
58+
{
59+
if (relationId == ExtensionRelationId)
60+
extension_cache_is_valid = false;
61+
}
62+
63+
/*
64+
* We don't want most of hooks to do anything if the "age" extension isn't
65+
* created. However, scanning pg_extension is a costly operation, therefore we
66+
* implement a caching mechanism and reset it with the help of the relcache
67+
* callback mechanism.
68+
*
69+
* Please also see ag_ProcessUtility_hook() function for more details.
70+
*/
71+
bool
72+
is_age_extension_exist(void)
73+
{
74+
static bool callback_registered = false;
75+
76+
if (extension_cache_is_valid)
77+
return age_extension_exists;
78+
79+
if (!callback_registered)
80+
{
81+
CacheRegisterRelcacheCallback(invalidate_extension_cache_callback,
82+
(Datum) 0);
83+
callback_registered = true;
84+
}
85+
86+
age_extension_exists = OidIsValid(get_extension_oid("age", true));
87+
88+
extension_cache_is_valid = true;
89+
90+
return age_extension_exists;
91+
}
5092

5193
void object_access_hook_init(void)
5294
{
@@ -86,50 +128,97 @@ void process_utility_hook_fini(void)
86128
* information in the indexes and tables being dropped. To prevent an error
87129
* from being thrown, we need to disable the object_access_hook before dropping
88130
* the extension.
131+
*
132+
* Besides that, we want to notify other backends about the fact that "age"
133+
* extension was probably created/dropped so that they can enable/disable
134+
* hooks.
89135
*/
90136
void ag_ProcessUtility_hook(PlannedStmt *pstmt, const char *queryString,
91137
bool readOnlyTree, ProcessUtilityContext context,
92138
ParamListInfo params, QueryEnvironment *queryEnv,
93139
DestReceiver *dest, QueryCompletion *qc)
94140
{
95-
if (is_age_drop(pstmt))
96-
{
97-
drop_age_extension((DropStmt *)pstmt->utilityStmt);
98-
}
99-
else
141+
bool creating_age = false;
142+
bool dropping_age = false;
143+
144+
if (!IsAbortedTransactionBlockState())
100145
{
101-
/*
102-
* Check for TRUNCATE on graph label tables. If any truncated
103-
* table is a graph label table, increment the version counter
104-
* for that graph to invalidate VLE caches. We do this before
105-
* the truncate executes so the cache is invalidated regardless.
106-
*/
107-
if (IsA(pstmt->utilityStmt, TruncateStmt))
146+
Node *parsetree = pstmt->utilityStmt;
147+
148+
switch (nodeTag(parsetree))
108149
{
109-
TruncateStmt *tstmt = (TruncateStmt *) pstmt->utilityStmt;
110-
ListCell *lc;
150+
case T_CreateExtensionStmt:
151+
{
152+
CreateExtensionStmt *stmt =
153+
(CreateExtensionStmt *) parsetree;
154+
creating_age = strcmp(stmt->extname, "age") == 0;
155+
}
156+
break;
157+
case T_DropStmt:
158+
{
159+
DropStmt *stmt = (DropStmt *) parsetree;
111160

112-
foreach(lc, tstmt->relations)
113-
{
114-
RangeVar *rv = (RangeVar *) lfirst(lc);
115-
Oid rel_oid = RangeVarGetRelid(rv, AccessShareLock, true);
161+
if (stmt->removeType != OBJECT_EXTENSION)
162+
break;
116163

117-
if (OidIsValid(rel_oid))
118-
{
119-
Oid graph_oid = get_graph_oid_for_table(rel_oid);
164+
if (!is_age_drop(stmt))
165+
break;
120166

121-
if (OidIsValid(graph_oid))
167+
dropping_age = true;
168+
}
169+
break;
170+
case T_TruncateStmt:
171+
{
172+
/*
173+
* Check for TRUNCATE on graph label tables. If any
174+
* truncated table is a graph label table, increment the
175+
* version counter for that graph to invalidate VLE caches.
176+
* We do this before the truncate executes so the cache is
177+
* invalidated regardless.
178+
*/
179+
TruncateStmt *tstmt = (TruncateStmt *) parsetree;
180+
ListCell *lc;
181+
182+
foreach(lc, tstmt->relations)
122183
{
123-
increment_graph_version(graph_oid);
184+
RangeVar *rv = (RangeVar *) lfirst(lc);
185+
Oid rel_oid = RangeVarGetRelid(rv, AccessShareLock,
186+
true);
187+
188+
if (OidIsValid(rel_oid))
189+
{
190+
Oid graph_oid =
191+
get_graph_oid_for_table(rel_oid);
192+
193+
if (OidIsValid(graph_oid))
194+
{
195+
increment_graph_version(graph_oid);
196+
}
197+
}
124198
}
125199
}
126-
}
200+
break;
201+
default:
202+
break;
127203
}
204+
}
128205

206+
if (dropping_age)
207+
{
208+
/* Remove all graphs */
209+
drop_graphs(get_graphnames());
210+
211+
/* Remove the object access hook */
212+
object_access_hook_fini();
213+
}
214+
215+
PG_TRY();
216+
{
129217
if (prev_process_utility_hook)
130218
{
131219
(*prev_process_utility_hook) (pstmt, queryString, readOnlyTree,
132-
context, params, queryEnv, dest, qc);
220+
context, params, queryEnv, dest,
221+
qc);
133222
}
134223
else
135224
{
@@ -141,39 +230,46 @@ void ag_ProcessUtility_hook(PlannedStmt *pstmt, const char *queryString,
141230
params, queryEnv, dest, qc);
142231
}
143232
}
144-
}
145-
146-
static void drop_age_extension(DropStmt *stmt)
147-
{
148-
/* Remove all graphs */
149-
drop_graphs(get_graphnames());
233+
PG_CATCH();
234+
{
235+
if (dropping_age)
236+
{
237+
/*
238+
* We have to restore the disabled object_access_hook if
239+
* DROP EXTENSION age failed.
240+
*/
241+
object_access_hook_init();
242+
}
243+
PG_RE_THROW();
244+
}
245+
PG_END_TRY();
150246

151-
/* Remove the object access hook */
152-
object_access_hook_fini();
247+
if (dropping_age)
248+
{
249+
/* reset global variables for OIDs */
250+
clear_global_Oids_AGTYPE();
251+
clear_global_Oids_GRAPHID();
252+
clear_global_Oids_VERTEX_EDGE();
153253

154-
/*
155-
* Run Postgres' logic to perform the remaining work to drop the
156-
* extension.
157-
*/
158-
RemoveObjects(stmt);
254+
/* Restore the object access hook */
255+
object_access_hook_init();
256+
}
159257

160-
/* reset global variables for OIDs */
161-
clear_global_Oids_AGTYPE();
162-
clear_global_Oids_GRAPHID();
163-
clear_global_Oids_VERTEX_EDGE();
258+
if (creating_age || dropping_age)
259+
{
260+
/* Notify all backends that pg_extension was modified. */
261+
CacheInvalidateRelcacheByRelid(ExtensionRelationId);
262+
}
164263
}
165264

166265
/* Check to see if the Utility Command is to drop the AGE Extension. */
167-
static bool is_age_drop(PlannedStmt *pstmt)
266+
static bool is_age_drop(DropStmt *drop_stmt)
168267
{
169268
ListCell *lc;
170-
DropStmt *drop_stmt;
171269

172-
if (!IsA(pstmt->utilityStmt, DropStmt))
270+
if (!is_age_extension_exist())
173271
return false;
174272

175-
drop_stmt = (DropStmt *)pstmt->utilityStmt;
176-
177273
foreach(lc, drop_stmt->objects)
178274
{
179275
Node *obj = lfirst(lc);
@@ -183,7 +279,7 @@ static bool is_age_drop(PlannedStmt *pstmt)
183279
String *val = (String *)obj;
184280
char *str = val->sval;
185281

186-
if (!pg_strcasecmp(str, "age"))
282+
if (strcmp(str, "age") == 0)
187283
return true;
188284
}
189285
}
@@ -205,15 +301,11 @@ static void object_access(ObjectAccessType access, Oid class_id, Oid object_id,
205301
if (prev_object_access_hook)
206302
prev_object_access_hook(access, class_id, object_id, sub_id, arg);
207303

208-
/* We are interested in DROP SCHEMA and DROP TABLE commands. */
209-
if (access != OAT_DROP)
304+
if (!is_age_extension_exist())
210305
return;
211306

212-
/*
213-
* Age might be installed into shared_preload_libraries before extension is
214-
* created. In this case we must bail out from this hook.
215-
*/
216-
if (!OidIsValid(get_namespace_oid("ag_catalog", true)))
307+
/* We are interested in DROP SCHEMA and DROP TABLE commands. */
308+
if (access != OAT_DROP)
217309
return;
218310

219311
drop_arg = arg;

src/backend/optimizer/cypher_paths.c

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
#include "postgres.h"
2121

22+
#include "catalog/ag_catalog.h"
2223
#include "optimizer/pathnode.h"
2324
#include "optimizer/paths.h"
2425

@@ -66,6 +67,9 @@ static void set_rel_pathlist(PlannerInfo *root, RelOptInfo *rel, Index rti,
6667
if (prev_set_rel_pathlist_hook)
6768
prev_set_rel_pathlist_hook(root, rel, rti, rte);
6869

70+
if (!is_age_extension_exist())
71+
return;
72+
6973
switch (get_cypher_clause_kind(rte))
7074
{
7175
case CYPHER_CLAUSE_CREATE:

src/backend/parser/cypher_analyze.c

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
#include "postgres.h"
2121

22+
#include "catalog/ag_catalog.h"
2223
#include "nodes/makefuncs.h"
2324
#include "nodes/nodeFuncs.h"
2425
#include "parser/analyze.h"
@@ -86,6 +87,9 @@ static void post_parse_analyze(ParseState *pstate, Query *query, JumbleState *js
8687
prev_post_parse_analyze_hook(pstate, query, jstate);
8788
}
8889

90+
if (!is_age_extension_exist())
91+
return;
92+
8993
/*
9094
* extra_node is set in the parsing stage to keep track of EXPLAIN.
9195
* So it needs to be set to NULL prior to any cypher parsing.

src/include/catalog/ag_catalog.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424

2525
#include "utils/agtype.h"
2626

27+
bool is_age_extension_exist(void);
28+
2729
void object_access_hook_init(void);
2830
void object_access_hook_fini(void);
2931

0 commit comments

Comments
 (0)