Thread: BUG #18637: CREATE INDEX won't look up operator classes in search_path if PARTITION BY is specified
BUG #18637: CREATE INDEX won't look up operator classes in search_path if PARTITION BY is specified
From
PG Bug reporting form
Date:
The following bug has been logged on the website: Bug reference: 18637 Logged by: CREATE INDEX won't look up operator classes in search_path if PARTITION BY is specified Email address: usamoi@outlook.com PostgreSQL version: 17.0 Operating system: Linux 6.10 Description: This SQL works in PostgreSQL 16.4 but not in PostgreSQL 17.0. Here `vector_l2_ops` is an operator class in schema `vectors`. ``` SET search_path TO public, vectors; CREATE TABLE items (val vector(3), category_id int) PARTITION BY LIST(category_id); CREATE TABLE id_123 PARTITION OF items FOR VALUES IN (1, 2, 3); CREATE TABLE id_456 PARTITION OF items FOR VALUES IN (4, 5, 6); CREATE TABLE id_789 PARTITION OF items FOR VALUES IN (7, 8, 9); CREATE INDEX ON items USING vectors (val vector_l2_ops); ``` The error message is: `ERROR: operator class "vector_l2_ops" does not exist for access method "vectors"` If I do not use PARTITION BY and CREATE TABLE OF, it works and the message is `CREATE INDEX`. After debugging, I find that `DefineIndex` would call `RestrictSearchPath` in `indexcmds.c:1234` and `indexcmds.c:1334` before entering recursion, so that the nested `DefineIndex` would be only able to look up operator classes in `pg_catalog, pg_temp`. Since the behavior between `CREATE INDEX` and `CREATE INDEX PARTITION BY` is different, it should be a bug.
Re: BUG #18637: CREATE INDEX won't look up operator classes in search_path if PARTITION BY is specified
From
David Rowley
Date:
On Sat, 28 Sept 2024 at 02:59, PG Bug reporting form <noreply@postgresql.org> wrote: > After debugging, I find that `DefineIndex` would call `RestrictSearchPath` > in `indexcmds.c:1234` and `indexcmds.c:1334` before entering recursion, so > that the nested `DefineIndex` would be only able to look up operator classes > in `pg_catalog, pg_temp`. > > Since the behavior between `CREATE INDEX` and `CREATE INDEX PARTITION BY` is > different, it should be a bug. There's an item in the release notes [1] which should be expended to mention this as an incompatibility, namely: "Change functions to use a safe search_path during maintenance operations (Jeff Davis) § This prevents maintenance operations (ANALYZE, CLUSTER, REFRESH MATERIALIZED VIEW, REINDEX, or VACUUM) from performing unsafe access. Functions used by expression indexes and materialized views that need to reference non-default schemas must specify a search path during function creation." That doesn't quite mention CREATE INDEX. There's a discussion about fixing that by adding CREATE INDEX to the list of commands. See [2]. I think you can now safely assume that you'll need to provide the schema name for the opclass you've specified. Also please see the CREATE INDEX documentation [3] where it says: "While CREATE INDEX is running, the search_path is temporarily changed to pg_catalog, pg_temp." David [1] https://www.postgresql.org/docs/release/17.0/ [2] https://www.postgresql.org/message-id/20240926141921.57d0b430fa53ac4389344847%40sraoss.co.jp [3] https://www.postgresql.org/docs/17/sql-createindex.html
Re: BUG #18637: CREATE INDEX won't look up operator classes in search_path if PARTITION BY is specified
From
Tom Lane
Date:
David Rowley <dgrowleyml@gmail.com> writes: > There's an item in the release notes [1] which should be expended to > mention this as an incompatibility, namely: > "Change functions to use a safe search_path during maintenance > operations (Jeff Davis) § I have not dug into the code, but I think this may actually be a bug; not because of the change of search path, but because it looks like the opclass is probably being looked up more than once during the command, with different search paths. That has security implications, and not good ones. We should fix this so that the opclass is looked up exactly once, and passed down to the partitions by OID not name. regards, tom lane
Re: BUG #18637: CREATE INDEX won't look up operator classes in search_path if PARTITION BY is specified
From
Tom Lane
Date:
I wrote: > I have not dug into the code, but I think this may actually be a > bug; not because of the change of search path, but because it looks > like the opclass is probably being looked up more than once during > the command, with different search paths. That has security > implications, and not good ones. We should fix this so that the > opclass is looked up exactly once, and passed down to the partitions > by OID not name. Here's an example that doesn't rely on any outside extension: regression=# create extension cube; CREATE EXTENSION regression=# create table items(category_id int, val cube) partition by list(category_id); CREATE TABLE regression=# CREATE INDEX ON items (val cube_ops); CREATE INDEX regression=# CREATE TABLE id_123 PARTITION OF items FOR VALUES IN (1, 2); CREATE TABLE regression=# CREATE INDEX items_idx_2 ON items (val cube_ops); ERROR: operator class "cube_ops" does not exist for access method "btree" So that's pretty awful: creating the partitioned index by itself is willing to look up the opclass in the current search path, and then adding a new partition will play nice with that, but creating a partitioned index when there's already a partition will not. It's got to be considered a bug that the two paths for making a child index behave differently. As far as I understand, the change of search path is only supposed to affect user-defined code within the expressions of an expression index. Operands of the command itself should consistently be interpreted in the current search path. regards, tom lane
Re: BUG #18637: CREATE INDEX won't look up operator classes in search_path if PARTITION BY is specified
From
Tom Lane
Date:
I wrote: > So that's pretty awful: creating the partitioned index by itself is > willing to look up the opclass in the current search path, and then > adding a new partition will play nice with that, but creating a > partitioned index when there's already a partition will not. > It's got to be considered a bug that the two paths for making a > child index behave differently. I looked into this and found that * When adding a partition to a pre-existing partitioned index, we use generateClonedIndexStmt() to construct the IndexStmt for DefineIndex to use. This is proof against the current problem because it will always schema-qualify the opclass name, cf. get_opclass(). * However, if CREATE INDEX recurses to create a child partition, it does this: IndexStmt *childStmt = copyObject(stmt); followed by some pretty ad-hoc cleanup. If the original IndexStmt wasn't written in a search-path-independent fashion then we've got trouble. It seems clear to me that the right fix for this is to use generateClonedIndexStmt in this code path too. I am not claiming that generateClonedIndexStmt is entirely free of related issues, but to the extent it has any we'd have to fix them anyway. Building the IndexStmt in two fundamentally different ways is just asking for trouble. I hacked this up and was amused to discover that it also fixes a TODO left over from c01eb619a: if there's a comment for the parent index, we've been incorrectly applying it to children too. generateClonedIndexStmt knows better than to fill idxcomment for the child IndexStmt, but copyObject does not. This could stand an additional regression test case perhaps, but the core tests don't have a suitable opclass at hand. Maybe we could test it in some contrib module, but that seems a shade hacky. Thoughts? regards, tom lane diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 130cebd658..e33ad81529 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -51,6 +51,7 @@ #include "optimizer/optimizer.h" #include "parser/parse_coerce.h" #include "parser/parse_oper.h" +#include "parser/parse_utilcmd.h" #include "partitioning/partdesc.h" #include "pgstat.h" #include "rewrite/rewriteManip.h" @@ -1465,55 +1466,20 @@ DefineIndex(Oid tableId, */ if (!found) { - IndexStmt *childStmt = copyObject(stmt); - bool found_whole_row; - ListCell *lc; + IndexStmt *childStmt; ObjectAddress childAddr; /* - * We can't use the same index name for the child index, - * so clear idxname to let the recursive invocation choose - * a new name. Likewise, the existing target relation - * field is wrong, and if indexOid or oldNumber are set, - * they mustn't be applied to the child either. + * Build an IndexStmt describing the desired child index + * in the same way that we do during ATTACH PARTITION. + * Notably, we rely on generateClonedIndexStmt to produce + * a search-path-independent representation, which the + * original IndexStmt might not be. */ - childStmt->idxname = NULL; - childStmt->relation = NULL; - childStmt->indexOid = InvalidOid; - childStmt->oldNumber = InvalidRelFileNumber; - childStmt->oldCreateSubid = InvalidSubTransactionId; - childStmt->oldFirstRelfilelocatorSubid = InvalidSubTransactionId; - - /* - * Adjust any Vars (both in expressions and in the index's - * WHERE clause) to match the partition's column numbering - * in case it's different from the parent's. - */ - foreach(lc, childStmt->indexParams) - { - IndexElem *ielem = lfirst(lc); - - /* - * If the index parameter is an expression, we must - * translate it to contain child Vars. - */ - if (ielem->expr) - { - ielem->expr = - map_variable_attnos((Node *) ielem->expr, - 1, 0, attmap, - InvalidOid, - &found_whole_row); - if (found_whole_row) - elog(ERROR, "cannot convert whole-row table reference"); - } - } - childStmt->whereClause = - map_variable_attnos(stmt->whereClause, 1, 0, - attmap, - InvalidOid, &found_whole_row); - if (found_whole_row) - elog(ERROR, "cannot convert whole-row table reference"); + childStmt = generateClonedIndexStmt(NULL, + parentIndex, + attmap, + NULL); /* * Recurse as the starting user ID. Callee will use that diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index fb8db37623..143ae7c09c 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -2215,7 +2215,6 @@ select conname, obj_description(oid, 'pg_constraint') as desc (3 rows) alter table at_partitioned alter column name type varchar(127); --- Note: these tests currently show the wrong behavior for comments :-( select relname, c.oid = oldoid as orig_oid, case relfilenode @@ -2232,9 +2231,9 @@ select relname, ------------------------------+----------+---------+-------------- at_partitioned | t | none | at_partitioned_0 | t | own | - at_partitioned_0_id_name_key | f | own | parent index + at_partitioned_0_id_name_key | f | own | at_partitioned_1 | t | own | - at_partitioned_1_id_name_key | f | own | parent index + at_partitioned_1_id_name_key | f | own | at_partitioned_id_name_key | f | none | parent index (6 rows) diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index cba15ebfec..c5dd43a15c 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -1496,8 +1496,6 @@ select conname, obj_description(oid, 'pg_constraint') as desc alter table at_partitioned alter column name type varchar(127); --- Note: these tests currently show the wrong behavior for comments :-( - select relname, c.oid = oldoid as orig_oid, case relfilenode
Re: BUG #18637: CREATE INDEX won't look up operator classes in search_path if PARTITION BY is specified
From
Noah Misch
Date:
On Thu, Oct 03, 2024 at 02:17:15PM -0400, Tom Lane wrote: > This could stand an additional regression test case perhaps, > but the core tests don't have a suitable opclass at hand. > Maybe we could test it in some contrib module, but that > seems a shade hacky. > > Thoughts? For contrib/citext/sql/create_index_acl.sql, I settled on testing in contrib. I valued the test fidelity of a real opclass more than I disliked the use of contrib. The distinction between the various parts of check-world is blurry to non-hackers, so I expect it won't bother them.
Re: BUG #18637: CREATE INDEX won't look up operator classes in search_path if PARTITION BY is specified
From
Tom Lane
Date:
Noah Misch <noah@leadboat.com> writes: > On Thu, Oct 03, 2024 at 02:17:15PM -0400, Tom Lane wrote: >> This could stand an additional regression test case perhaps, >> but the core tests don't have a suitable opclass at hand. >> Maybe we could test it in some contrib module, but that >> seems a shade hacky. > For contrib/citext/sql/create_index_acl.sql, I settled on testing in contrib. > I valued the test fidelity of a real opclass more than I disliked the use of > contrib. The distinction between the various parts of check-world is blurry > to non-hackers, so I expect it won't bother them. Agreed. Pushed it with a test added to contrib/seg, just to be different. regards, tom lane