From 8186db33b7275110b52668753a850301c7fbede8 Mon Sep 17 00:00:00 2001 From: Bobbie Soedirgo Date: Fri, 26 Jan 2024 18:50:45 +0800 Subject: [PATCH 1/4] feat: extensions parameter overrides --- Makefile | 3 +- nix/pg_cron.nix | 32 ++++++ nix/withTmpDb.sh.in | 8 +- shell.nix | 1 + src/extensions_parameter_overrides.c | 134 ++++++++++++++++++++++++ src/extensions_parameter_overrides.h | 34 ++++++ src/privileged_extensions.c | 41 +++++++- src/privileged_extensions.h | 13 +-- src/supautils.c | 62 +++++++++-- test/expected/privileged_extensions.out | 10 ++ test/sql/privileged_extensions.sql | 5 + 11 files changed, 322 insertions(+), 21 deletions(-) create mode 100644 nix/pg_cron.nix create mode 100644 src/extensions_parameter_overrides.c create mode 100644 src/extensions_parameter_overrides.h diff --git a/Makefile b/Makefile index 7da490d..267ab05 100644 --- a/Makefile +++ b/Makefile @@ -8,9 +8,8 @@ else endif MODULE_big = supautils -OBJS = src/supautils.o src/privileged_extensions.o src/constrained_extensions.o src/utils.o +OBJS = src/supautils.o src/privileged_extensions.o src/constrained_extensions.o src/extensions_parameter_overrides.o src/utils.o -PG_VERSION = $(strip $(shell $(PG_CONFIG) --version | $(GREP) -oP '(?<=PostgreSQL )[0-9]+')) SYSTEM = $(shell uname -s) ifneq ($(SYSTEM), Linux) diff --git a/nix/pg_cron.nix b/nix/pg_cron.nix new file mode 100644 index 0000000..9fa75d5 --- /dev/null +++ b/nix/pg_cron.nix @@ -0,0 +1,32 @@ +{ lib, stdenv, fetchFromGitHub, postgresql }: + +stdenv.mkDerivation rec { + pname = "pg_cron"; + version = "1.5.2"; + + buildInputs = [ postgresql ]; + + src = fetchFromGitHub { + owner = "citusdata"; + repo = pname; + rev = "v${version}"; + hash = "sha256-+quVWbKJy6wXpL/zwTk5FF7sYwHA7I97WhWmPO/HSZ4="; + }; + + installPhase = '' + mkdir -p $out/{lib,share/postgresql/extension} + + cp *.so $out/lib + cp *.sql $out/share/postgresql/extension + cp *.control $out/share/postgresql/extension + ''; + + meta = with lib; { + description = "Run Cron jobs through PostgreSQL"; + homepage = "https://github.com/citusdata/pg_cron"; + changelog = "https://github.com/citusdata/pg_cron/raw/v${version}/CHANGELOG.md"; + maintainers = with maintainers; [ thoughtpolice ]; + platforms = postgresql.meta.platforms; + license = licenses.postgresql; + }; +} diff --git a/nix/withTmpDb.sh.in b/nix/withTmpDb.sh.in index 872fc69..94c8ad5 100644 --- a/nix/withTmpDb.sh.in +++ b/nix/withTmpDb.sh.in @@ -11,11 +11,11 @@ trap 'pg_ctl stop -m i && rm -rf "$tmpdir"' sigint sigterm exit PGTZ=UTC initdb --no-locale --encoding=UTF8 --nosync -U "$PGUSER" -options="-F -c listen_addresses=\"\" -k $PGDATA -c shared_preload_libraries=\"pg_tle, supautils\" -c wal_level=logical" +options="-F -c listen_addresses=\"\" -k $PGDATA -c shared_preload_libraries=\"pg_cron, pg_tle, supautils\" -c wal_level=logical -c cron.database_name=postgres" reserved_roles="supabase_storage_admin, anon, reserved_but_not_yet_created, authenticator*" reserved_memberships="pg_read_server_files, pg_write_server_files, pg_execute_server_program, role_with_reserved_membership" -privileged_extensions="autoinc, citext, hstore, postgres_fdw, pg_tle" +privileged_extensions="autoinc, citext, hstore, pg_cron, pg_tle, postgres_fdw" privileged_extensions_custom_scripts_path="$tmpdir/privileged_extensions_custom_scripts" privileged_role="privileged_role" privileged_role_allowed_configs="session_replication_role, pgrst.*, other.nested.*" @@ -25,7 +25,9 @@ placeholder_stuff_options='-c supautils.placeholders="response.headers, another. cexts_option='-c supautils.constrained_extensions="{\"adminpack\": { \"cpu\": 64}, \"cube\": { \"mem\": \"17 GB\"}, \"lo\": { \"disk\": \"20 GB\"}, \"amcheck\": { \"cpu\": 2, \"mem\": \"100 MB\", \"disk\": \"100 MB\"}}"' -pg_ctl start -o "$options" -o "$reserved_stuff_options" -o "$placeholder_stuff_options" -o "$cexts_option" +epos_option='-c supautils.extensions_parameter_overrides="{\"pg_cron\":{\"schema\":\"pg_catalog\"}}"' + +pg_ctl start -o "$options" -o "$reserved_stuff_options" -o "$placeholder_stuff_options" -o "$cexts_option" -o "$epos_option" # print notice when creating a TLE mkdir -p "$tmpdir/privileged_extensions_custom_scripts" diff --git a/shell.nix b/shell.nix index 3870b41..6ed6b6b 100644 --- a/shell.nix +++ b/shell.nix @@ -11,6 +11,7 @@ let ]; pgWithExt = { postgresql }: postgresql.withPackages (p: [ (callPackage ./nix/supautils.nix { inherit postgresql; extraMakeFlags = "TEST=1"; }) + (callPackage ./nix/pg_cron.nix { inherit postgresql; }) (callPackage ./nix/pg_tle.nix { inherit postgresql; }) ]); pgScriptAll = map (x: callPackage ./nix/pgScript.nix { postgresql = pgWithExt { postgresql = x;}; }) supportedPgVersions; diff --git a/src/extensions_parameter_overrides.c b/src/extensions_parameter_overrides.c new file mode 100644 index 0000000..7a2893a --- /dev/null +++ b/src/extensions_parameter_overrides.c @@ -0,0 +1,134 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "extensions_parameter_overrides.h" + +static void json_array_start(void *state) { + json_extension_parameter_overrides_parse_state *parse = state; + + parse->state = JEPO_UNEXPECTED_ARRAY; + parse->error_msg = "unexpected array"; +} + +static void json_object_start(void *state) { + json_extension_parameter_overrides_parse_state *parse = state; + + switch (parse->state) { + case JEPO_EXPECT_TOPLEVEL_START: + parse->state = JEPO_EXPECT_TOPLEVEL_FIELD; + break; + case JEPO_EXPECT_SCHEMA: + parse->error_msg = "unexpected object for schema, expected a value"; + parse->state = JEPO_UNEXPECTED_OBJECT; + break; + default: + break; + } +} + +static void json_object_end(void *state) { + json_extension_parameter_overrides_parse_state *parse = state; + + switch (parse->state) { + case JEPO_EXPECT_PARAMETER_OVERRIDES_START: + parse->state = JEPO_EXPECT_TOPLEVEL_FIELD; + (parse->total_epos)++; + break; + default: + break; + } +} + +static void json_object_field_start(void *state, char *fname, bool isnull) { + json_extension_parameter_overrides_parse_state *parse = state; + extension_parameter_overrides *x = &parse->epos[parse->total_epos]; + + switch (parse->state) { + case JEPO_EXPECT_TOPLEVEL_FIELD: + x->name = MemoryContextStrdup(TopMemoryContext, fname); + parse->state = JEPO_EXPECT_PARAMETER_OVERRIDES_START; + break; + + case JEPO_EXPECT_PARAMETER_OVERRIDES_START: + if (strcmp(fname, "schema") == 0) + parse->state = JEPO_EXPECT_SCHEMA; + else { + parse->state = JEPO_UNEXPECTED_FIELD; + parse->error_msg = "unexpected field, only schema is allowed"; + } + break; + + default: + break; + } +} + +static void json_scalar(void *state, char *token, JsonTokenType tokentype) { + json_extension_parameter_overrides_parse_state *parse = state; + extension_parameter_overrides *x = &parse->epos[parse->total_epos]; + + switch (parse->state) { + case JEPO_EXPECT_SCHEMA: + if (tokentype == JSON_TOKEN_STRING) { + x->schema = MemoryContextStrdup(TopMemoryContext, token); + parse->state = JEPO_EXPECT_PARAMETER_OVERRIDES_START; + } else { + parse->state = JEPO_UNEXPECTED_SCHEMA_VALUE; + parse->error_msg = "unexpected schema value, expected a string"; + } + break; + + case JEPO_EXPECT_TOPLEVEL_START: + parse->state = JEPO_UNEXPECTED_SCALAR; + parse->error_msg = "unexpected scalar, expected an object"; + break; + + case JEPO_EXPECT_PARAMETER_OVERRIDES_START: + parse->state = JEPO_UNEXPECTED_SCALAR; + parse->error_msg = "unexpected scalar, expected an object"; + break; + + default: + break; + } +} + +json_extension_parameter_overrides_parse_state +parse_extensions_parameter_overrides(const char *str, + extension_parameter_overrides *epos) { + JsonLexContext *lex; + JsonParseErrorType json_error; + JsonSemAction sem; + + json_extension_parameter_overrides_parse_state state = { + JEPO_EXPECT_TOPLEVEL_START, NULL, 0, epos}; + + lex = + makeJsonLexContextCstringLen(pstrdup(str), strlen(str), PG_UTF8, true); + + sem.semstate = &state; + sem.object_start = json_object_start; + sem.object_end = json_object_end; + sem.array_start = json_array_start; + sem.array_end = NULL; + sem.object_field_start = json_object_field_start; + sem.object_field_end = NULL; + sem.array_element_start = NULL; + sem.array_element_end = NULL; + sem.scalar = json_scalar; + + json_error = pg_parse_json(lex, &sem); + + if (json_error != JSON_SUCCESS) + state.error_msg = "invalid json"; + + return state; +} diff --git a/src/extensions_parameter_overrides.h b/src/extensions_parameter_overrides.h new file mode 100644 index 0000000..358c0a0 --- /dev/null +++ b/src/extensions_parameter_overrides.h @@ -0,0 +1,34 @@ +#ifndef EXTENSIONS_PARAMETER_OVERRIDES_H +#define EXTENSIONS_PARAMETER_OVERRIDES_H + +#include + +typedef struct { + char *name; + char *schema; +} extension_parameter_overrides; + +typedef enum { + JEPO_EXPECT_TOPLEVEL_START, + JEPO_EXPECT_TOPLEVEL_FIELD, + JEPO_EXPECT_PARAMETER_OVERRIDES_START, + JEPO_EXPECT_SCHEMA, + JEPO_UNEXPECTED_FIELD, + JEPO_UNEXPECTED_ARRAY, + JEPO_UNEXPECTED_SCALAR, + JEPO_UNEXPECTED_OBJECT, + JEPO_UNEXPECTED_SCHEMA_VALUE +} json_extension_parameter_overrides_semantic_state; + +typedef struct { + json_extension_parameter_overrides_semantic_state state; + char *error_msg; + int total_epos; + extension_parameter_overrides *epos; +} json_extension_parameter_overrides_parse_state; + +extern json_extension_parameter_overrides_parse_state +parse_extensions_parameter_overrides(const char *str, + extension_parameter_overrides *epos); + +#endif diff --git a/src/privileged_extensions.c b/src/privileged_extensions.c index 653aaf7..bab0d83 100644 --- a/src/privileged_extensions.c +++ b/src/privileged_extensions.c @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -16,6 +17,7 @@ #include #include +#include "extensions_parameter_overrides.h" #include "privileged_extensions.h" #include "utils.h" @@ -91,7 +93,8 @@ void handle_create_extension( void (*process_utility_hook)(PROCESS_UTILITY_PARAMS), PROCESS_UTILITY_PARAMS, const char *privileged_extensions, const char *privileged_extensions_superuser, - const char *privileged_extensions_custom_scripts_path) { + const char *privileged_extensions_custom_scripts_path, + const extension_parameter_overrides *epos, const size_t total_epos) { CreateExtensionStmt *stmt = (CreateExtensionStmt *)pstmt->utilityStmt; char *filename = (char *)palloc(MAXPGPATH); @@ -173,6 +176,42 @@ void handle_create_extension( } } + // Apply overrides. + for (size_t i = 0; i < total_epos; i++) { + if (strcmp(epos[i].name, stmt->extname) == 0) { + const extension_parameter_overrides *epo = &epos[i]; + DefElem *schema_option = NULL; + DefElem *schema_override_option = NULL; + ListCell *option_cell; + + if (epo->schema != NULL) { + Node *schema_node = (Node *)makeString(pstrdup(epo->schema)); + schema_override_option = makeDefElem("schema", schema_node, -1); + } + + foreach (option_cell, stmt->options) { + DefElem *defel = (DefElem *)lfirst(option_cell); + + if (strcmp(defel->defname, "schema") == 0) { + if (schema_option != NULL) { + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("conflicting or redundant options"))); + } + schema_option = defel; + } + } + + if (schema_override_option != NULL) { + if (schema_option != NULL) { + stmt->options = + list_delete_ptr(stmt->options, schema_option); + } + stmt->options = lappend(stmt->options, schema_override_option); + } + } + } + // Run `CREATE EXTENSION`. if (!superuser() && is_string_in_comma_delimited_string( stmt->extname, privileged_extensions)) { diff --git a/src/privileged_extensions.h b/src/privileged_extensions.h index 5acd4bb..4877fca 100644 --- a/src/privileged_extensions.h +++ b/src/privileged_extensions.h @@ -1,14 +1,15 @@ #ifndef PRIVILEGED_EXTENSIONS_H #define PRIVILEGED_EXTENSIONS_H +#include "extensions_parameter_overrides.h" #include "utils.h" -extern void -handle_create_extension(void (*process_utility_hook)(PROCESS_UTILITY_PARAMS), - PROCESS_UTILITY_PARAMS, - const char *privileged_extensions, - const char *privileged_extensions_superuser, - const char *privileged_extensions_custom_scripts_path); +extern void handle_create_extension( + void (*process_utility_hook)(PROCESS_UTILITY_PARAMS), + PROCESS_UTILITY_PARAMS, const char *privileged_extensions, + const char *privileged_extensions_superuser, + const char *privileged_extensions_custom_scripts_path, + const extension_parameter_overrides *epos, const size_t total_epos); extern void handle_alter_extension(void (*process_utility_hook)(PROCESS_UTILITY_PARAMS), diff --git a/src/supautils.c b/src/supautils.c index 914c39d..b5c2728 100644 --- a/src/supautils.c +++ b/src/supautils.c @@ -19,6 +19,7 @@ #include #include "constrained_extensions.h" +#include "extensions_parameter_overrides.h" #include "privileged_extensions.h" #include "utils.h" @@ -40,7 +41,8 @@ errmsg("parameter \"%s\" must be a comma-separated list of " \ "identifiers", name))); -#define MAX_CONSTRAINED_EXTENSIONS 100 +#define MAX_CONSTRAINED_EXTENSIONS 100 +#define MAX_EXTENSIONS_PARAMETER_OVERRIDES 100 /* required macro for extension libraries to work */ PG_MODULE_MAGIC; @@ -60,10 +62,15 @@ static ProcessUtility_hook_type prev_hook = NULL; static char *constrained_extensions_str = NULL; static constrained_extension cexts[MAX_CONSTRAINED_EXTENSIONS] = {0}; static size_t total_cexts = 0; - static void constrained_extensions_assign_hook(const char *newval, void *extra); +static char *extensions_parameter_overrides_str = NULL; +static extension_parameter_overrides epos[MAX_EXTENSIONS_PARAMETER_OVERRIDES] = {0}; +static size_t total_epos = 0; +static bool +extensions_parameter_overrides_check_hook(char **newval, void **extra, GucSource source); + void _PG_init(void); void _PG_fini(void); @@ -111,6 +118,16 @@ _PG_init(void) /* Set our hook */ ProcessUtility_hook = supautils_hook; + DefineCustomStringVariable("supautils.extensions_parameter_overrides", + "Overrides for CREATE EXTENSION parameters", + NULL, + &extensions_parameter_overrides_str, + NULL, + PGC_SIGHUP, 0, + &extensions_parameter_overrides_check_hook, + NULL, + NULL); + DefineCustomStringVariable("supautils.reserved_roles", "Comma-separated list of roles that cannot be modified", NULL, @@ -161,20 +178,20 @@ _PG_init(void) NULL, NULL); - DefineCustomStringVariable("supautils.privileged_extensions_superuser", - "Superuser to install extensions in supautils.privileged_extensions as", + DefineCustomStringVariable("supautils.privileged_extensions_custom_scripts_path", + "Path to load privileged extensions' custom scripts from", NULL, - &privileged_extensions_superuser, + &privileged_extensions_custom_scripts_path, NULL, PGC_SIGHUP, 0, NULL, NULL, NULL); - DefineCustomStringVariable("supautils.privileged_extensions_custom_scripts_path", - "Path to load privileged extensions' custom scripts from", + DefineCustomStringVariable("supautils.privileged_extensions_superuser", + "Superuser to install extensions in supautils.privileged_extensions as", NULL, - &privileged_extensions_custom_scripts_path, + &privileged_extensions_superuser, NULL, PGC_SIGHUP, 0, NULL, @@ -551,7 +568,8 @@ supautils_hook(PROCESS_UTILITY_PARAMS) PROCESS_UTILITY_ARGS, privileged_extensions, privileged_extensions_superuser, - privileged_extensions_custom_scripts_path); + privileged_extensions_custom_scripts_path, + epos, total_epos); return; } @@ -829,6 +847,32 @@ supautils_hook(PROCESS_UTILITY_PARAMS) run_process_utility_hook(prev_hook); } +static bool +extensions_parameter_overrides_check_hook(char **newval, void **extra, GucSource source) +{ + char *val = *newval; + + if (total_epos > 0) { + for (size_t i = 0; i < total_epos; i++){ + pfree(epos[i].name); + pfree(epos[i].schema); + } + total_epos = 0; + } + + if (val) { + json_extension_parameter_overrides_parse_state state = parse_extensions_parameter_overrides(val, epos); + if (state.error_msg) { + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("supautils.extensions_parameter_overrides: %s", state.error_msg))); + } + total_epos = state.total_epos; + } + + return true; +} + static bool reserved_roles_check_hook(char **newval, void **extra, GucSource source) { diff --git a/test/expected/privileged_extensions.out b/test/expected/privileged_extensions.out index 3914388..3c46ea7 100644 --- a/test/expected/privileged_extensions.out +++ b/test/expected/privileged_extensions.out @@ -78,3 +78,13 @@ select current_role; extensions_role (1 row) +\echo + +-- can force pg_cron to be installed in pg_catalog +create extension pg_cron schema public; +select extnamespace::regnamespace from pg_extension where extname = 'pg_cron'; + extnamespace +-------------- + pg_catalog +(1 row) + diff --git a/test/sql/privileged_extensions.sql b/test/sql/privileged_extensions.sql index 5b719ef..1ca2563 100644 --- a/test/sql/privileged_extensions.sql +++ b/test/sql/privileged_extensions.sql @@ -50,3 +50,8 @@ create extension file_fdw; -- original role is restored on nested switch_to_superuser() create extension autoinc; select current_role; +\echo + +-- can force pg_cron to be installed in pg_catalog +create extension pg_cron schema public; +select extnamespace::regnamespace from pg_extension where extname = 'pg_cron'; From 8c2fd7adb0c0c59ef0f19c1c894d6dcadcfa28da Mon Sep 17 00:00:00 2001 From: Bobbie Soedirgo Date: Mon, 29 Jan 2024 09:57:45 +0800 Subject: [PATCH 2/4] fix(ci): don't copy the `.control` file It no longer exists --- .github/workflows/release.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e35cc98..2d609d5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -66,7 +66,6 @@ jobs: mkdir -p ${package_dir}/usr/lib/postgresql/lib mkdir -p ${package_dir}/var/lib/postgresql/extension cp *.so ${package_dir}/usr/lib/postgresql/lib - cp *.control ${package_dir}/var/lib/postgresql/extension # symlinks to Copy files into directory structure mkdir -p ${package_dir}/usr/lib/postgresql/${{ matrix.postgres }}/lib @@ -76,7 +75,6 @@ jobs: mkdir -p ${package_dir}/usr/share/postgresql/${{ matrix.postgres }}/extension cd ${package_dir}/usr/share/postgresql/${{ matrix.postgres }}/extension - cp -s ../../../../../var/lib/postgresql/extension/${{ matrix.extension_name }}.control . cd ../../../../../.. # Create install control file From b1265010a139aaaf3cd99e4360a7a60138130bca Mon Sep 17 00:00:00 2001 From: Bobbie Soedirgo Date: Mon, 29 Jan 2024 11:19:56 +0800 Subject: [PATCH 3/4] fix: switch to privileged_extensions_superuser even if superuser For extensions like pg_cron, the permissions depend on the owner of the extension object, which in turn depends on which role runs the `CREATE EXTENSION`. One might want to temporarily make the user role a superuser during pg_restore. This would break pg_cron because once the user role is reverted to non-superuser, the extension owner (i.e. the non-superuser user role) would not have permissions to execute pg_cron functions. This is not an issue if we make the `CREATE EXTENSION` run as the privileged_extensions_superuser unconditionally, since by definition it would always be a superuser. --- src/privileged_extensions.c | 4 ++-- test/expected/privileged_extensions.out | 20 ++++++++++++++++++++ test/sql/privileged_extensions.sql | 16 ++++++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/src/privileged_extensions.c b/src/privileged_extensions.c index bab0d83..73e85f4 100644 --- a/src/privileged_extensions.c +++ b/src/privileged_extensions.c @@ -213,8 +213,8 @@ void handle_create_extension( } // Run `CREATE EXTENSION`. - if (!superuser() && is_string_in_comma_delimited_string( - stmt->extname, privileged_extensions)) { + if (is_string_in_comma_delimited_string(stmt->extname, + privileged_extensions)) { bool already_switched_to_superuser = false; switch_to_superuser(privileged_extensions_superuser, &already_switched_to_superuser); diff --git a/test/expected/privileged_extensions.out b/test/expected/privileged_extensions.out index 3c46ea7..7858da8 100644 --- a/test/expected/privileged_extensions.out +++ b/test/expected/privileged_extensions.out @@ -88,3 +88,23 @@ select extnamespace::regnamespace from pg_extension where extname = 'pg_cron'; pg_catalog (1 row) +drop extension pg_cron; +\echo + +-- switch to privileged_extensions_superuser even if superuser +reset role; +create role another_superuser superuser; +set role another_superuser; +create extension pg_cron; +select extowner::regrole from pg_extension where extname = 'pg_cron'; + extowner +---------- + postgres +(1 row) + +reset role; +drop extension pg_cron; +drop role another_superuser; +set role extensions_role; +\echo + diff --git a/test/sql/privileged_extensions.sql b/test/sql/privileged_extensions.sql index 1ca2563..25ae2c7 100644 --- a/test/sql/privileged_extensions.sql +++ b/test/sql/privileged_extensions.sql @@ -55,3 +55,19 @@ select current_role; -- can force pg_cron to be installed in pg_catalog create extension pg_cron schema public; select extnamespace::regnamespace from pg_extension where extname = 'pg_cron'; + +drop extension pg_cron; +\echo + +-- switch to privileged_extensions_superuser even if superuser +reset role; +create role another_superuser superuser; +set role another_superuser; +create extension pg_cron; +select extowner::regrole from pg_extension where extname = 'pg_cron'; + +reset role; +drop extension pg_cron; +drop role another_superuser; +set role extensions_role; +\echo From 09bd91b7b36ee1dee0e53ebcc438bdc1d0a3c4f4 Mon Sep 17 00:00:00 2001 From: Bobbie Soedirgo Date: Tue, 30 Jan 2024 12:52:07 +0800 Subject: [PATCH 4/4] docs: extensions parameter overrides --- README.md | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 526e4c6..d4c6ac0 100644 --- a/README.md +++ b/README.md @@ -156,7 +156,28 @@ DETAIL: required CPUs: 16 HINT: upgrade to an instance with higher resources ``` -This feature is only available from PostgreSQL 13 onwards. +## Extensions Parameter Overrides + +You can override `CREATE EXTENSION` parameters like so: + +``` +supautils.extensions_parameter_overrides = '{ "pg_cron": { "schema": "pg_catalog" } }' +``` + +Currently, only the `schema` parameter is supported. + +These overrides will apply on `CREATE EXTENSION`, e.g.: + +```sql +postgres=> create extension pg_cron schema public; +CREATE EXTENSION +postgres=> \dx pg_cron + List of installed extensions + Name | Version | Schema | Description +---------+---------+------------+------------------------------ + pg_cron | 1.5 | pg_catalog | Job scheduler for PostgreSQL +(1 row) +``` ## Development