From f63fdab9c1c6493adf7c0b635eb36513aefcc902 Mon Sep 17 00:00:00 2001 From: Hunaid2000 Date: Fri, 9 Oct 2026 18:43:24 +0500 Subject: [PATCH v3] Implement SELECT * EXCLUDE/REPLACE/RENAME clauses --- doc/src/sgml/ref/select.sgml | 147 ++- src/backend/parser/analyze.c | 2 +- src/backend/parser/gram.y | 159 ++- src/backend/parser/parse_relation.c | 253 ++++- src/backend/parser/parse_target.c | 104 +- src/include/nodes/parsenodes.h | 40 + src/include/parser/parse_relation.h | 2 +- .../regress/expected/select_staroptions.out | 965 ++++++++++++++++++ src/test/regress/parallel_schedule | 2 +- src/test/regress/sql/select_staroptions.sql | 519 ++++++++++ src/tools/pgindent/typedefs.list | 4 + 11 files changed, 2176 insertions(+), 21 deletions(-) create mode 100644 src/test/regress/expected/select_staroptions.out create mode 100644 src/test/regress/sql/select_staroptions.sql diff --git a/doc/src/sgml/ref/select.sgml b/doc/src/sgml/ref/select.sgml index 18392f7caec..72bdbc94e0c 100644 --- a/doc/src/sgml/ref/select.sgml +++ b/doc/src/sgml/ref/select.sgml @@ -34,7 +34,7 @@ PostgreSQL documentation [ WITH [ RECURSIVE ] with_query [, ...] ] SELECT [ ALL | DISTINCT [ ON ( expression [, ...] ) ] ] - [ { * | expression [ [ AS ] output_name ] } [, ...] ] + [ { * [ ( staroption ) ] | expression [ [ AS ] output_name ] } [, ...] ] [ FROM from_item [, ...] ] [ WHERE condition ] [ GROUP BY [ ALL | DISTINCT ] grouping_element [, ...] ] @@ -78,6 +78,15 @@ SELECT [ ALL | DISTINCT [ ON ( expressioncolumn_name [, ...] SET search_seq_col_name ] [ CYCLE column_name [, ...] SET cycle_mark_col_name [ TO cycle_mark_value DEFAULT cycle_mark_default ] USING cycle_path_col_name ] +and staroption is: + + EXCLUDE ( column_name [, ...] ) + [ REPLACE ( column_name WITH expression [, ...] ) ] + [ RENAME ( column_name AS output_name [, ...] ) ] + | REPLACE ( column_name WITH expression [, ...] ) + [ RENAME ( column_name AS output_name [, ...] ) ] + | RENAME ( column_name AS output_name [, ...] ) + TABLE [ ONLY ] table_name [ * ] @@ -103,6 +112,30 @@ TABLE [ ONLY ] table_name [ * ] + + + If the EXCLUDE clause is specified, all + columns listed there are excluded from the output when + * is used in the SELECT list. + + + + + + If the REPLACE clause is specified, all columns + listed there are replaced with the corresponding expressions when + * is used in the SELECT list. + + + + + + If the RENAME clause is specified, all columns + listed there are renamed to the corresponding output names when + * is used in the SELECT list. + + + All elements in the FROM list are computed. @@ -382,6 +415,118 @@ TABLE [ ONLY ] table_name [ * ] + + Star Options + + + Star options allow you to exclude columns, replace their values, or + rename them when using * or a qualified + * in the SELECT list. + The options are specified in parentheses following the asterisk. + The sequence of the options is important, EXCLUDE + is applied first, then REPLACE, and finally + RENAME. At least one option must be specified + in the parentheses. + + + + Column names specified in star options may be qualified or + unqualified. Qualification uses the table name or alias specified + in the FROM clause. An unqualified column name + matches all columns with that name produced by the asterisk + expansion. The specified operation is applied to all matching + columns and no ambiguity error is raised. A qualified column name + matches only columns belonging to the specified table or alias. + + + + Each column name specified in a star option must match at least + one column produced by the asterisk expansion. Otherwise, an error + is raised. Duplicate column names within an individual option are + not allowed. + + + + A column specified in EXCLUDE cannot also be + specified in REPLACE or + RENAME. A column may, however, be specified + in both REPLACE and RENAME. + + + + The columns in the REPLACE and RENAME + clauses are marked for SELECT privilege checks, + whereas the columns in the EXCLUDE clause are not. + + + + For example, the following query excludes columns + a and b, + replaces the value of column c, + and renames column e: + +SELECT * (EXCLUDE (a, b) + REPLACE (c WITH x + y) + RENAME (e AS ee)) +FROM t; + + + + + The qualified-asterisk variant is also supported: + +SELECT t.* (EXCLUDE ...) FROM t ... + + + + + + <literal>EXCLUDE</literal> Clause + + + The EXCLUDE clause removes specified columns + from the output of an asterisk expansion. This is useful when + selecting from tables with many columns when only a few columns + need to be omitted. + + + + The following examples illustrate different ways of excluding + columns from the output, producing the same result: + +-- Exclude specific columns without table qualification +SELECT * EXCLUDE (ta.c_ta, tb.c_tb) ... + +-- Exclude specific columns with table qualification +SELECT ta.* EXCLUDE (c_ta), tb.* EXCLUDE (c_tb) ... + + + + + + <literal>REPLACE</literal> Clause + + + The REPLACE clause replaces the values of + specified columns in the output of an asterisk expansion with + the results of the specified expressions. The replacement + expressions are specified using the + column_name WITH expression syntax. + Replacement does not change the names of the columns. + + + + + <literal>RENAME</literal> Clause + + + The RENAME clause changes the output names + of specified columns in an asterisk expansion. The new names + are specified using the column_name AS output_name + syntax. Renaming a column does not change its value. + + + <literal>FROM</literal> Clause diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index 2dc81a8d3b2..5bea0119f95 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -1739,7 +1739,7 @@ transformValuesClause(ParseState *pstate, SelectStmt *stmt) * Generate a targetlist as though expanding "*" */ Assert(pstate->p_next_resno == 1); - qry->targetList = expandNSItemAttrs(pstate, nsitem, 0, true, -1); + qry->targetList = expandNSItemAttrs(pstate, nsitem, 0, true, -1, NULL); /* * The grammar allows attaching ORDER BY, LIMIT, and FOR UPDATE to a diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 0563453fe24..8ef187dddfe 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -455,6 +455,9 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); vacuum_relation_list opt_vacuum_relation_list drop_option_list pub_obj_list pub_all_obj_type_list pub_except_obj_list opt_pub_except_clause + exclude_list opt_replace replace_list opt_rename rename_list + +%type exclude_item replace_item rename_item opt_star_options star_options %type returning_clause %type returning_option @@ -12654,6 +12657,27 @@ returning_clause: RETURNING returning_with_clause target_list { ReturningClause *n = makeNode(ReturningClause); + ListCell *lc; + + /* Check for RETURNING with star options, which is not supported */ + foreach(lc, $3) + { + ResTarget *res = (ResTarget *) lfirst(lc); + if (IsA(res->val, ColumnRef)) + { + ColumnRef *cref = (ColumnRef *) res->val; + + if (IsA(llast(cref->fields), A_Star)) + { + A_Star *star = (A_Star *) llast(cref->fields); + if (star->staroptions != NULL) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("RETURNING with star options is not supported"), + parser_errposition(cref->location))); + } + } + } n->options = $2; n->exprs = $3; @@ -17331,6 +17355,13 @@ indirection_el: { $$ = (Node *) makeNode(A_Star); } + | '.' '*' star_options + { + A_Star *s = makeNode(A_Star); + + s->staroptions = (StarOptions *) $3; + $$ = (Node *) s; + } | '[' a_expr ']' { A_Indices *ai = makeNode(A_Indices); @@ -17659,11 +17690,13 @@ target_el: a_expr AS ColLabel $$->val = (Node *) $1; $$->location = @1; } - | '*' + | '*' opt_star_options { ColumnRef *n = makeNode(ColumnRef); + A_Star *s = makeNode(A_Star); + s->staroptions = (StarOptions *) $2; - n->fields = list_make1(makeNode(A_Star)); + n->fields = list_make1(s); n->location = @1; $$ = makeNode(ResTarget); @@ -17674,6 +17707,128 @@ target_el: a_expr AS ColLabel } ; +star_options: + '(' EXCLUDE '(' exclude_list ')' opt_replace opt_rename ')' + { + StarOptions *n = makeNode(StarOptions); + + n->exclude_list = $4; + n->replace_list = $6; + n->rename_list = $7; + + $$ = (Node *) n; + } + | '(' REPLACE '(' replace_list ')' opt_rename ')' + { + StarOptions *n = makeNode(StarOptions); + + n->exclude_list = NIL; + n->replace_list = $4; + n->rename_list = $6; + + $$ = (Node *) n; + } + | '(' RENAME '(' rename_list ')' ')' + { + StarOptions *n = makeNode(StarOptions); + + n->exclude_list = NIL; + n->replace_list = NIL; + n->rename_list = $4; + + $$ = (Node *) n; + } + ; + +opt_star_options: + star_options { $$ = $1; } + | /* EMPTY */ { $$ = NULL; } + ; + +exclude_list: + exclude_item + { $$ = list_make1($1); } + | exclude_list ',' exclude_item + { $$ = lappend($1, $3); } + ; + +exclude_item: + columnref + { + StarExcludeItem *n = makeNode(StarExcludeItem); + ColumnRef *c = (ColumnRef *) $1; + c->staroptcol_match = false; + + check_qualified_name(c->fields, yyscanner); + + n->name = c; + n->location = @1; + + $$ = (Node *) n; + } + ; + +replace_list: + replace_item + { $$ = list_make1($1); } + | replace_list ',' replace_item + { $$ = lappend($1, $3); } + ; + +replace_item: + columnref WITH a_expr + { + StarReplaceItem *n = makeNode(StarReplaceItem); + ColumnRef *c = (ColumnRef *) $1; + c->staroptcol_match = false; + + check_qualified_name(c->fields, yyscanner); + + n->name = c; + n->expr = (Node *) $3; + n->location = @1; + + $$ = (Node *) n; + } + ; + +opt_replace: + REPLACE '(' replace_list ')' + { $$ = $3; } + | /* EMPTY */ + { $$ = NIL; } + ; + +rename_list: + rename_item + { $$ = list_make1($1); } + | rename_list ',' rename_item + { $$ = lappend($1, $3); } + ; + +rename_item: + columnref AS ColId + { + StarRenameItem *n = makeNode(StarRenameItem); + ColumnRef *c = (ColumnRef *) $1; + c->staroptcol_match = false; + + check_qualified_name(c->fields, yyscanner); + + n->name = c; + n->aliasname = $3; + n->location = @1; + + $$ = (Node *) n; + } + ; + +opt_rename: + RENAME '(' rename_list ')' + { $$ = $3; } + | /* EMPTY */ + { $$ = NIL; } + ; /***************************************************************************** * diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 8aeb3e219ac..7af152d1e9b 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -22,9 +22,11 @@ #include "catalog/heap.h" #include "catalog/namespace.h" #include "funcapi.h" +#include "miscadmin.h" #include "nodes/makefuncs.h" #include "nodes/nodeFuncs.h" #include "parser/parse_enr.h" +#include "parser/parse_expr.h" #include "parser/parse_relation.h" #include "parser/parse_type.h" #include "parser/parsetree.h" @@ -3299,6 +3301,239 @@ expandNSItemVars(ParseState *pstate, ParseNamespaceItem *nsitem, return result; } +/* + * Check whether a column name matches any column in the + * EXCLUDE, REPLACE, or RENAME lists. + * + * For unqualified column names, match by column name regardless + * of which relation the column comes from. + * + * For qualified column names, resolve the relation qualifier using + * PostgreSQL's namespace lookup rules and compare the resulting RTE + * with the syntactic RTE of the current column (varnosyn). This avoids + * scanning the namespace item's columns and also handles aliases and + * join RTEs correctly. + */ +static bool +starOptionMatchesColumn(ParseState *pstate, + ColumnRef *cref, + Var *varnode, + char *label) +{ + ParseNamespaceItem *refnsitem = NULL; + char *schemaname = NULL; + char *relname = NULL; + char *colname = NULL; + + switch (list_length(cref->fields)) + { + case 1: + /* unqualified column name */ + colname = strVal(linitial(cref->fields)); + break; + case 2: + /* relation.column */ + relname = strVal(linitial(cref->fields)); + colname = strVal(lsecond(cref->fields)); + break; + case 3: + /* schema.relation.column */ + schemaname = strVal(linitial(cref->fields)); + relname = strVal(lsecond(cref->fields)); + colname = strVal(lthird(cref->fields)); + break; + case 4: + { + /* catalog.schema.relation.column */ + char *catalogname = strVal(linitial(cref->fields)); + + schemaname = strVal(lsecond(cref->fields)); + relname = strVal(lthird(cref->fields)); + colname = strVal(lfourth(cref->fields)); + + if (strcmp(catalogname, get_database_name(MyDatabaseId)) != 0) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cross-database references are not implemented: %s", + NameListToString(cref->fields)), + parser_errposition(pstate, cref->location))); + break; + } + default: + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("improper qualified name (too many dotted names): %s", + NameListToString(cref->fields)), + parser_errposition(pstate, cref->location))); + } + + /* + * Match the column name first. + */ + if (strcmp(colname, label) != 0) + return false; + + /* + * Unqualified column name matches regardless of which RTE it came from. + */ + if (relname == NULL) + return true; + + /* Locate the referenced nsitem */ + refnsitem = refnameNamespaceItem(pstate, schemaname, relname, + cref->location, + 0); + + if (refnsitem == NULL) + errorMissingRTE(pstate, makeRangeVar(schemaname, relname, + cref->location)); + + /* + * For a qualified name, make sure the column belongs to the RTE + * identified by the qualifier. Compare the RTE identified by the + * qualifier with the RTE from which the current expanded column + * originated. + */ + return refnsitem->p_rtindex == varnode->varnosyn; +} + +/* + * Apply the EXCLUDE, REPLACE, and RENAME options from staroptions to a + * column being expanded. The expression and output name are updated as + * appropriate. Also, check for conflicts between the options. + * + * Returns true if the column is excluded, false otherwise. + */ +static bool +applyStarOptions(ParseState *pstate, + Var *varnode, + char *label, + Node **expr, + char **output_name, + StarOptions *staroptions) +{ + List *exclude_list = staroptions->exclude_list; + List *replace_list = staroptions->replace_list; + List *rename_list = staroptions->rename_list; + ListCell *lc; + bool excluded = false; + + /* EXCLUDE */ + if (exclude_list) + { + bool exclude_col_seen = false; + + foreach(lc, exclude_list) + { + StarExcludeItem *item = (StarExcludeItem *) lfirst(lc); + ColumnRef *cref = item->name; + + if (starOptionMatchesColumn(pstate, cref, varnode, label)) + { + if (exclude_col_seen) + ereport(ERROR, + (errcode(ERRCODE_DUPLICATE_COLUMN), + errmsg("duplicate column \"%s\" in EXCLUDE list", + NameListToString(cref->fields)))); + + exclude_col_seen = true; + excluded = true; + cref->staroptcol_match = true; + } + } + } + + /* An excluded column cannot also occur in REPLACE or RENAME lists */ + if (excluded) + { + if (replace_list) + { + foreach(lc, replace_list) + { + StarReplaceItem *item = (StarReplaceItem *) lfirst(lc); + ColumnRef *cref = item->name; + + if (starOptionMatchesColumn(pstate, cref, varnode, label)) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("column \"%s\" cannot occur in both EXCLUDE and REPLACE list", + NameListToString(cref->fields)))); + } + } + + if (rename_list) + { + foreach(lc, rename_list) + { + StarRenameItem *item = (StarRenameItem *) lfirst(lc); + ColumnRef *cref = item->name; + + if (starOptionMatchesColumn(pstate, cref, varnode, label)) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("column \"%s\" cannot occur in both EXCLUDE and RENAME list", + NameListToString(cref->fields)))); + } + } + + return true; + } + + /* REPLACE */ + if (replace_list) + { + bool replace_col_seen = false; + + foreach(lc, replace_list) + { + StarReplaceItem *item = (StarReplaceItem *) lfirst(lc); + ColumnRef *cref = (ColumnRef *) item->name; + + if (starOptionMatchesColumn(pstate, cref, varnode, label)) + { + if (replace_col_seen) + ereport(ERROR, + (errcode(ERRCODE_DUPLICATE_COLUMN), + errmsg("duplicate column \"%s\" in REPLACE list", + NameListToString(cref->fields)))); + + *expr = transformExpr(pstate, + (Node *) item->expr, + EXPR_KIND_SELECT_TARGET); + cref->staroptcol_match = true; + replace_col_seen = true; + } + } + } + + /* RENAME */ + if (rename_list) + { + bool rename_col_seen = false; + + foreach(lc, rename_list) + { + StarRenameItem *item = (StarRenameItem *) lfirst(lc); + ColumnRef *cref = (ColumnRef *) item->name; + + if (starOptionMatchesColumn(pstate, cref, varnode, label)) + { + if (rename_col_seen) + ereport(ERROR, + (errcode(ERRCODE_DUPLICATE_COLUMN), + errmsg("duplicate column \"%s\" in RENAME list", + NameListToString(cref->fields)))); + + *output_name = item->aliasname; + cref->staroptcol_match = true; + rename_col_seen = true; + } + } + } + + return false; +} + /* * expandNSItemAttrs - * Workhorse for "*" expansion: produce a list of targetentries @@ -3310,7 +3545,8 @@ expandNSItemVars(ParseState *pstate, ParseNamespaceItem *nsitem, */ List * expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, - int sublevels_up, bool require_col_privs, int location) + int sublevels_up, bool require_col_privs, int location, + StarOptions *staroptions) { RangeTblEntry *rte = nsitem->p_rte; RTEPermissionInfo *perminfo = nsitem->p_perminfo; @@ -3340,11 +3576,22 @@ expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, { char *label = strVal(lfirst(name)); Var *varnode = (Var *) lfirst(var); + char *output_name = label; + Node *output_expr = (Node *) varnode; TargetEntry *te; - te = makeTargetEntry((Expr *) varnode, + /* Handle EXCLUDE, REPLACE, and RENAME lists */ + if (staroptions && applyStarOptions(pstate, varnode, label, + &output_expr, &output_name, + staroptions)) + { + /* column is excluded, so skip it */ + continue; + } + + te = makeTargetEntry((Expr *) output_expr, (AttrNumber) pstate->p_next_resno++, - label, + output_name, false); te_list = lappend(te_list, te); diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c index 728936b502e..01cbf261f98 100644 --- a/src/backend/parser/parse_target.c +++ b/src/backend/parser/parse_target.c @@ -44,14 +44,14 @@ static Node *transformAssignmentSubscripts(ParseState *pstate, Node *rhs, CoercionContext ccontext, int location); -static List *ExpandColumnRefStar(ParseState *pstate, ColumnRef *cref, +static List *ExpandColumnRefStar(ParseState *pstate, ColumnRef *cref, StarOptions *staroptions, bool make_target_entry); -static List *ExpandAllTables(ParseState *pstate, int location); +static List *ExpandAllTables(ParseState *pstate, int location, StarOptions *staroptions); static List *ExpandIndirectionStar(ParseState *pstate, A_Indirection *ind, bool make_target_entry, ParseExprKind exprKind); static List *ExpandSingleTable(ParseState *pstate, ParseNamespaceItem *nsitem, int sublevels_up, int location, - bool make_target_entry); + bool make_target_entry, StarOptions *staroptions); static List *ExpandRowReference(ParseState *pstate, Node *expr, bool make_target_entry); static int FigureColnameInternal(Node *node, char **name); @@ -107,6 +107,80 @@ transformTargetEntry(ParseState *pstate, resjunk); } +/* + * check_star_options() + * Check that all columns mentioned in the star options actually exist. + * + * Extract the relation and column names from the ColumnRef. + * We do not care about other qualification errors here, + * since they were already checked during column matching. + */ +static void +check_star_options(ParseState *pstate, StarOptions *staroptions) +{ + List *exclude_list = staroptions->exclude_list; + List *replace_list = staroptions->replace_list; + List *rename_list = staroptions->rename_list; + + if (exclude_list) + { + ListCell *elc; + + foreach(elc, exclude_list) + { + StarExcludeItem *item = (StarExcludeItem *) lfirst(elc); + ColumnRef *cref = item->name; + char *relname = NULL; + char *colname = NULL; + + colname = strVal(llast(cref->fields)); + if (list_length(cref->fields) > 1) + relname = strVal(list_nth(cref->fields, list_length(cref->fields) - 2)); + + if (!cref->staroptcol_match) + errorMissingColumn(pstate, relname, colname, cref->location); + } + } + if (replace_list) + { + ListCell *rlc; + + foreach(rlc, replace_list) + { + StarReplaceItem *item = (StarReplaceItem *) lfirst(rlc); + ColumnRef *cref = item->name; + char *relname = NULL; + char *colname = NULL; + + colname = strVal(llast(cref->fields)); + if (list_length(cref->fields) > 1) + relname = strVal(list_nth(cref->fields, list_length(cref->fields) - 2)); + + if (!cref->staroptcol_match) + errorMissingColumn(pstate, relname, colname, cref->location); + } + } + if (rename_list) + { + ListCell *rlc; + + foreach(rlc, rename_list) + { + StarRenameItem *item = (StarRenameItem *) lfirst(rlc); + ColumnRef *cref = item->name; + char *relname = NULL; + char *colname = NULL; + + colname = strVal(llast(cref->fields)); + if (list_length(cref->fields) > 1) + relname = strVal(list_nth(cref->fields, list_length(cref->fields) - 2)); + + if (!cref->staroptcol_match) + errorMissingColumn(pstate, relname, colname, cref->location); + } + } +} + /* * transformTargetList() @@ -147,11 +221,16 @@ transformTargetList(ParseState *pstate, List *targetlist, if (IsA(llast(cref->fields), A_Star)) { + A_Star *star = (A_Star *) llast(cref->fields); + /* It is something.*, expand into multiple items */ p_target = list_concat(p_target, ExpandColumnRefStar(pstate, cref, + star->staroptions, true)); + if (star->staroptions) + check_star_options(pstate, star->staroptions); continue; } } @@ -239,7 +318,7 @@ transformExpressionList(ParseState *pstate, List *exprlist, { /* It is something.*, expand into multiple items */ result = list_concat(result, - ExpandColumnRefStar(pstate, cref, + ExpandColumnRefStar(pstate, cref, NULL, false)); continue; } @@ -1121,7 +1200,7 @@ checkInsertTargets(ParseState *pstate, List *cols, List **attrnos) * The referenced columns are marked as requiring SELECT access. */ static List * -ExpandColumnRefStar(ParseState *pstate, ColumnRef *cref, +ExpandColumnRefStar(ParseState *pstate, ColumnRef *cref, StarOptions *staroptions, bool make_target_entry) { List *fields = cref->fields; @@ -1138,7 +1217,7 @@ ExpandColumnRefStar(ParseState *pstate, ColumnRef *cref, * need not handle the make_target_entry==false case here. */ Assert(make_target_entry); - return ExpandAllTables(pstate, cref->location); + return ExpandAllTables(pstate, cref->location, staroptions); } else { @@ -1278,7 +1357,7 @@ ExpandColumnRefStar(ParseState *pstate, ColumnRef *cref, * OK, expand the nsitem into fields. */ return ExpandSingleTable(pstate, nsitem, levels_up, cref->location, - make_target_entry); + make_target_entry, staroptions); } } @@ -1294,7 +1373,7 @@ ExpandColumnRefStar(ParseState *pstate, ColumnRef *cref, * The referenced relations/columns are marked as requiring SELECT access. */ static List * -ExpandAllTables(ParseState *pstate, int location) +ExpandAllTables(ParseState *pstate, int location, StarOptions *staroptions) { List *target = NIL; bool found_table = false; @@ -1317,7 +1396,8 @@ ExpandAllTables(ParseState *pstate, int location) nsitem, 0, true, - location)); + location, + staroptions)); } /* @@ -1374,12 +1454,12 @@ ExpandIndirectionStar(ParseState *pstate, A_Indirection *ind, */ static List * ExpandSingleTable(ParseState *pstate, ParseNamespaceItem *nsitem, - int sublevels_up, int location, bool make_target_entry) + int sublevels_up, int location, bool make_target_entry, StarOptions *staroptions) { if (make_target_entry) { /* expandNSItemAttrs handles permissions marking */ - return expandNSItemAttrs(pstate, nsitem, sublevels_up, true, location); + return expandNSItemAttrs(pstate, nsitem, sublevels_up, true, location, staroptions); } else { @@ -1448,7 +1528,7 @@ ExpandRowReference(ParseState *pstate, Node *expr, ParseNamespaceItem *nsitem; nsitem = GetNSItemByVar(pstate, var); - return ExpandSingleTable(pstate, nsitem, var->varlevelsup, var->location, make_target_entry); + return ExpandSingleTable(pstate, nsitem, var->varlevelsup, var->location, make_target_entry, NULL); } /* diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 0debcd193ab..44e8e285e33 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -310,9 +310,48 @@ typedef struct ColumnRef { NodeTag type; List *fields; /* field names (String nodes) or A_Star */ + + /* + * These fields are only used with star option columns. It would be + * redundant to store them in each Star*Item, so we store them here + * instead. + */ + bool staroptcol_match; /* true if this star option column is + * matched; for reporting errors */ ParseLoc location; /* token location, or -1 if unknown */ } ColumnRef; +typedef struct StarExcludeItem +{ + NodeTag type; + ColumnRef *name; /* possibly qualified column name */ + ParseLoc location; /* token location, or -1 if unknown */ +} StarExcludeItem; + +typedef struct StarReplaceItem +{ + NodeTag type; + ColumnRef *name; /* possibly qualified column name */ + Node *expr; /* expression to replace the column with */ + ParseLoc location; /* token location, or -1 if unknown */ +} StarReplaceItem; + +typedef struct StarRenameItem +{ + NodeTag type; + ColumnRef *name; /* possibly qualified column name */ + char *aliasname; /* new output name for the column */ + ParseLoc location; /* token location, or -1 if unknown */ +} StarRenameItem; + +typedef struct StarOptions +{ + NodeTag type; + List *exclude_list; /* List of StarExcludeItem */ + List *replace_list; /* List of StarReplaceItem */ + List *rename_list; /* List of StarRenameItem */ +} StarOptions; + /* * ParamRef - specifies a $n parameter reference */ @@ -472,6 +511,7 @@ typedef struct FuncCall typedef struct A_Star { NodeTag type; + StarOptions *staroptions; /* options for '*' in fields, or NULL if none */ } A_Star; /* diff --git a/src/include/parser/parse_relation.h b/src/include/parser/parse_relation.h index ecfb4c047c0..96803aed764 100644 --- a/src/include/parser/parse_relation.h +++ b/src/include/parser/parse_relation.h @@ -124,7 +124,7 @@ extern List *expandNSItemVars(ParseState *pstate, ParseNamespaceItem *nsitem, List **colnames); extern List *expandNSItemAttrs(ParseState *pstate, ParseNamespaceItem *nsitem, int sublevels_up, bool require_col_privs, - int location); + int location, StarOptions *staroptions); extern int attnameAttNum(Relation rd, const char *attname, bool sysColOK); extern const NameData *attnumAttName(Relation rd, int attid); extern Oid attnumTypeId(Relation rd, int attid); diff --git a/src/test/regress/expected/select_staroptions.out b/src/test/regress/expected/select_staroptions.out new file mode 100644 index 00000000000..6617d144002 --- /dev/null +++ b/src/test/regress/expected/select_staroptions.out @@ -0,0 +1,965 @@ +-- +-- SELECT EXCLUDE/REPLACE/RENAME +-- +SET client_min_messages TO 'warning'; +DROP TABLE IF EXISTS users; +DROP TABLE IF EXISTS orders; +RESET client_min_messages; +CREATE TABLE users ( + id INT PRIMARY KEY, + name TEXT, + email TEXT, + created_at TIMESTAMP +); +CREATE TABLE orders ( + id INT PRIMARY KEY, + user_id INT REFERENCES users(id), + amount NUMERIC(10,2), + status TEXT +); +CREATE SCHEMA s1; +CREATE SCHEMA s2; +CREATE TABLE s1.products ( + id INT PRIMARY KEY, + name TEXT, + price NUMERIC(10,2) +); +CREATE TABLE s2.products ( + id INT PRIMARY KEY, + name TEXT, + price NUMERIC(10,2), + description TEXT +); +-- Insert sample data +INSERT INTO users (id, name, email, created_at) VALUES +(1, 'Alice', 'alice@example.com', '2026-01-01 10:00:00'), +(2, 'Bob', 'bob@example.com', '2026-01-02 11:00:00'), +(3, 'Carol', NULL, '2026-01-03 12:00:00'); +INSERT INTO orders (id, user_id, amount, status) VALUES +(101, 1, 50.00, 'paid'), +(102, 1, 75.50, 'shipped'), +(103, 2, 20.00, 'cancelled'); +INSERT INTO s1.products (id, name, price) VALUES +(1, 'Product A', 10.00), +(2, 'Product B', 15.50); +INSERT INTO s2.products (id, name, price, description) VALUES +(1, 'Product X', 20.00, 'Description for X'), +(2, 'Product Y', 25.75, 'Description for Y'); +-- +-- EXCLUDE clause +-- +-- Simple EXCLUDE +SELECT * (EXCLUDE (email, created_at)) +FROM users +ORDER BY id; + id | name +----+------- + 1 | Alice + 2 | Bob + 3 | Carol +(3 rows) + +-- Exclude all but one column +SELECT * (EXCLUDE (name, email, created_at)) +FROM users +ORDER BY id; + id +---- + 1 + 2 + 3 +(3 rows) + +-- Exclude all columns +SELECT * (EXCLUDE (id, name, email, created_at)) +FROM users; +-- +(3 rows) + +-- EXCLUDE all using exclude list but overall SELECT list is not empty +SELECT id, users.* (EXCLUDE (id, name, email, created_at)) +FROM users +ORDER BY id; + id +---- + 1 + 2 + 3 +(3 rows) + +-- Aliasing with EXCLUDE +SELECT * (EXCLUDE (u.email)) +FROM users AS u +ORDER BY u.id; + id | name | created_at +----+-------+-------------------------- + 1 | Alice | Thu Jan 01 10:00:00 2026 + 2 | Bob | Fri Jan 02 11:00:00 2026 + 3 | Carol | Sat Jan 03 12:00:00 2026 +(3 rows) + +-- Expressions with EXCLUDE +SELECT * (EXCLUDE (name)), 1 + 1 AS two +FROM users +ORDER BY id; + id | email | created_at | two +----+-------------------+--------------------------+----- + 1 | alice@example.com | Thu Jan 01 10:00:00 2026 | 2 + 2 | bob@example.com | Fri Jan 02 11:00:00 2026 | 2 + 3 | | Sat Jan 03 12:00:00 2026 | 2 +(3 rows) + +-- JOINs with EXCLUDE +-- Join, unqualified EXCLUDE +SELECT * (EXCLUDE (created_at)) +FROM users +JOIN orders ON orders.user_id = users.id +ORDER BY users.id, orders.id; + id | name | email | id | user_id | amount | status +----+-------+-------------------+-----+---------+--------+----------- + 1 | Alice | alice@example.com | 101 | 1 | 50.00 | paid + 1 | Alice | alice@example.com | 102 | 1 | 75.50 | shipped + 2 | Bob | bob@example.com | 103 | 2 | 20.00 | cancelled +(3 rows) + +-- Join, qualified EXCLUDE +SELECT * (EXCLUDE (users.created_at, orders.amount)) +FROM users +JOIN orders ON orders.user_id = users.id +ORDER BY users.id, orders.id; + id | name | email | id | user_id | status +----+-------+-------------------+-----+---------+----------- + 1 | Alice | alice@example.com | 101 | 1 | paid + 1 | Alice | alice@example.com | 102 | 1 | shipped + 2 | Bob | bob@example.com | 103 | 2 | cancelled +(3 rows) + +-- Join, aliased tables with EXCLUDE +SELECT * (EXCLUDE (u.created_at, o.amount)) +FROM users AS u +JOIN orders AS o ON o.user_id = u.id +ORDER BY u.id, o.id; + id | name | email | id | user_id | status +----+-------+-------------------+-----+---------+----------- + 1 | Alice | alice@example.com | 101 | 1 | paid + 1 | Alice | alice@example.com | 102 | 1 | shipped + 2 | Bob | bob@example.com | 103 | 2 | cancelled +(3 rows) + +-- Qualified stars; EXCLUDE is applied to each star separately +SELECT +users.* (EXCLUDE (id)), +orders.* (EXCLUDE (user_id)) +FROM users +LEFT JOIN orders ON orders.user_id = users.id +ORDER BY users.id, orders.id; + name | email | created_at | id | amount | status +-------+-------------------+--------------------------+-----+--------+----------- + Alice | alice@example.com | Thu Jan 01 10:00:00 2026 | 101 | 50.00 | paid + Alice | alice@example.com | Thu Jan 01 10:00:00 2026 | 102 | 75.50 | shipped + Bob | bob@example.com | Fri Jan 02 11:00:00 2026 | 103 | 20.00 | cancelled + Carol | | Sat Jan 03 12:00:00 2026 | | | +(4 rows) + +-- Schema-qualified star +SELECT s1.products.* (EXCLUDE (price)) +FROM s1.products +ORDER BY id; + id | name +----+----------- + 1 | Product A + 2 | Product B +(2 rows) + +-- Schema-qualified column +SELECT * (EXCLUDE (s1.products.price)) +FROM s1.products +ORDER BY id; + id | name +----+----------- + 1 | Product A + 2 | Product B +(2 rows) + +-- Name collision +SELECT * (EXCLUDE (id)) +FROM users +JOIN orders ON orders.user_id = users.id +ORDER BY users.id, orders.id; + name | email | created_at | user_id | amount | status +-------+-------------------+--------------------------+---------+--------+----------- + Alice | alice@example.com | Thu Jan 01 10:00:00 2026 | 1 | 50.00 | paid + Alice | alice@example.com | Thu Jan 01 10:00:00 2026 | 1 | 75.50 | shipped + Bob | bob@example.com | Fri Jan 02 11:00:00 2026 | 2 | 20.00 | cancelled +(3 rows) + +-- Subqueries with EXCLUDE +SELECT * (EXCLUDE (u.created_at)) +FROM ( + SELECT * FROM users +) u +ORDER BY id; + id | name | email +----+-------+------------------- + 1 | Alice | alice@example.com + 2 | Bob | bob@example.com + 3 | Carol | +(3 rows) + +SELECT * (EXCLUDE (j.created_at)) +FROM ( + users JOIN orders USING (id) +) AS j; + id | name | email | user_id | amount | status +----+------+-------+---------+--------+-------- +(0 rows) + +-- CTEs with EXCLUDE +WITH base_users AS ( + SELECT * FROM users +) +SELECT * (EXCLUDE (base_users.created_at)) +FROM base_users +ORDER BY id; + id | name | email +----+-------+------------------- + 1 | Alice | alice@example.com + 2 | Bob | bob@example.com + 3 | Carol | +(3 rows) + +-- Multiple FROM items with EXCLUDE +SELECT * (EXCLUDE (email, status)) FROM users, orders ORDER BY users.id, orders.id; + id | name | created_at | id | user_id | amount +----+-------+--------------------------+-----+---------+-------- + 1 | Alice | Thu Jan 01 10:00:00 2026 | 101 | 1 | 50.00 + 1 | Alice | Thu Jan 01 10:00:00 2026 | 102 | 1 | 75.50 + 1 | Alice | Thu Jan 01 10:00:00 2026 | 103 | 2 | 20.00 + 2 | Bob | Fri Jan 02 11:00:00 2026 | 101 | 1 | 50.00 + 2 | Bob | Fri Jan 02 11:00:00 2026 | 102 | 1 | 75.50 + 2 | Bob | Fri Jan 02 11:00:00 2026 | 103 | 2 | 20.00 + 3 | Carol | Sat Jan 03 12:00:00 2026 | 101 | 1 | 50.00 + 3 | Carol | Sat Jan 03 12:00:00 2026 | 102 | 1 | 75.50 + 3 | Carol | Sat Jan 03 12:00:00 2026 | 103 | 2 | 20.00 +(9 rows) + +-- CROSS JOIN with EXCLUDE +SELECT * (EXCLUDE (email, status)) FROM users CROSS JOIN orders ORDER BY users.id, orders.id; + id | name | created_at | id | user_id | amount +----+-------+--------------------------+-----+---------+-------- + 1 | Alice | Thu Jan 01 10:00:00 2026 | 101 | 1 | 50.00 + 1 | Alice | Thu Jan 01 10:00:00 2026 | 102 | 1 | 75.50 + 1 | Alice | Thu Jan 01 10:00:00 2026 | 103 | 2 | 20.00 + 2 | Bob | Fri Jan 02 11:00:00 2026 | 101 | 1 | 50.00 + 2 | Bob | Fri Jan 02 11:00:00 2026 | 102 | 1 | 75.50 + 2 | Bob | Fri Jan 02 11:00:00 2026 | 103 | 2 | 20.00 + 3 | Carol | Sat Jan 03 12:00:00 2026 | 101 | 1 | 50.00 + 3 | Carol | Sat Jan 03 12:00:00 2026 | 102 | 1 | 75.50 + 3 | Carol | Sat Jan 03 12:00:00 2026 | 103 | 2 | 20.00 +(9 rows) + +-- +-- REPLACE clause +-- +-- Simple REPLACE +SELECT * (REPLACE (email WITH 'user@example.com', created_at WITH NULL)) +FROM users +ORDER BY id; + id | name | email | created_at +----+-------+------------------+------------ + 1 | Alice | user@example.com | + 2 | Bob | user@example.com | + 3 | Carol | user@example.com | +(3 rows) + +-- Aliasing with REPLACE +SELECT * (REPLACE (u.email WITH 'user@example.com')) +FROM users AS u +ORDER BY u.id; + id | name | email | created_at +----+-------+------------------+-------------------------- + 1 | Alice | user@example.com | Thu Jan 01 10:00:00 2026 + 2 | Bob | user@example.com | Fri Jan 02 11:00:00 2026 + 3 | Carol | user@example.com | Sat Jan 03 12:00:00 2026 +(3 rows) + +-- Expressions with REPLACE +SELECT * (REPLACE (created_at WITH NULL)), 1 + 1 AS two +FROM users +ORDER BY id; + id | name | email | created_at | two +----+-------+-------------------+------------+----- + 1 | Alice | alice@example.com | | 2 + 2 | Bob | bob@example.com | | 2 + 3 | Carol | | | 2 +(3 rows) + +-- JOINs with REPLACE +-- Join, unqualified REPLACE +SELECT * (REPLACE (created_at WITH created_at + INTERVAL '1 day')) +FROM users +JOIN orders ON orders.user_id = users.id +ORDER BY users.id, orders.id; + id | name | email | created_at | id | user_id | amount | status +----+-------+-------------------+--------------------------+-----+---------+--------+----------- + 1 | Alice | alice@example.com | Fri Jan 02 10:00:00 2026 | 101 | 1 | 50.00 | paid + 1 | Alice | alice@example.com | Fri Jan 02 10:00:00 2026 | 102 | 1 | 75.50 | shipped + 2 | Bob | bob@example.com | Sat Jan 03 11:00:00 2026 | 103 | 2 | 20.00 | cancelled +(3 rows) + +-- Join, qualified REPLACE +SELECT * (REPLACE (users.created_at WITH NULL, orders.amount WITH orders.amount * 2)) +FROM users +JOIN orders ON orders.user_id = users.id +ORDER BY users.id, orders.id; + id | name | email | created_at | id | user_id | amount | status +----+-------+-------------------+------------+-----+---------+--------+----------- + 1 | Alice | alice@example.com | | 101 | 1 | 100.00 | paid + 1 | Alice | alice@example.com | | 102 | 1 | 151.00 | shipped + 2 | Bob | bob@example.com | | 103 | 2 | 40.00 | cancelled +(3 rows) + +-- Join, aliased tables with REPLACE +SELECT * (REPLACE (u.created_at WITH NULL, o.amount WITH o.amount * 2)) +FROM users AS u +JOIN orders AS o ON o.user_id = u.id +ORDER BY u.id, o.id; + id | name | email | created_at | id | user_id | amount | status +----+-------+-------------------+------------+-----+---------+--------+----------- + 1 | Alice | alice@example.com | | 101 | 1 | 100.00 | paid + 1 | Alice | alice@example.com | | 102 | 1 | 151.00 | shipped + 2 | Bob | bob@example.com | | 103 | 2 | 40.00 | cancelled +(3 rows) + +-- Qualified stars; REPLACE is applied to each star separately +SELECT +users.* (REPLACE (id WITH 999)), +orders.* (REPLACE (user_id WITH NULL)) +FROM users +LEFT JOIN orders ON orders.user_id = users.id +ORDER BY users.id, orders.id; + id | name | email | created_at | id | user_id | amount | status +-----+-------+-------------------+--------------------------+-----+---------+--------+----------- + 999 | Alice | alice@example.com | Thu Jan 01 10:00:00 2026 | 101 | | 50.00 | paid + 999 | Alice | alice@example.com | Thu Jan 01 10:00:00 2026 | 102 | | 75.50 | shipped + 999 | Bob | bob@example.com | Fri Jan 02 11:00:00 2026 | 103 | | 20.00 | cancelled + 999 | Carol | | Sat Jan 03 12:00:00 2026 | | | | +(4 rows) + +-- Schema-qualified star +SELECT s1.products.* (REPLACE (price WITH price * 1.1)) +FROM s1.products +ORDER BY id; + id | name | price +----+-----------+-------- + 1 | Product A | 11.000 + 2 | Product B | 17.050 +(2 rows) + +-- Schema-qualified column +SELECT * (REPLACE (s1.products.price WITH s1.products.price * 1.1)) +FROM s1.products +ORDER BY id; + id | name | price +----+-----------+-------- + 1 | Product A | 11.000 + 2 | Product B | 17.050 +(2 rows) + +-- Name collision +SELECT * (REPLACE (id WITH 999)) +FROM users +JOIN orders ON orders.user_id = users.id +ORDER BY users.id, orders.id; + id | name | email | created_at | id | user_id | amount | status +-----+-------+-------------------+--------------------------+-----+---------+--------+----------- + 999 | Alice | alice@example.com | Thu Jan 01 10:00:00 2026 | 999 | 1 | 50.00 | paid + 999 | Alice | alice@example.com | Thu Jan 01 10:00:00 2026 | 999 | 1 | 75.50 | shipped + 999 | Bob | bob@example.com | Fri Jan 02 11:00:00 2026 | 999 | 2 | 20.00 | cancelled +(3 rows) + +-- Subqueries with REPLACE +SELECT * (REPLACE (u.created_at WITH u.created_at + INTERVAL '1 day')) +FROM ( + SELECT * FROM users +) u +ORDER BY id; + id | name | email | created_at +----+-------+-------------------+-------------------------- + 1 | Alice | alice@example.com | Fri Jan 02 10:00:00 2026 + 2 | Bob | bob@example.com | Sat Jan 03 11:00:00 2026 + 3 | Carol | | Sun Jan 04 12:00:00 2026 +(3 rows) + +SELECT * (REPLACE (j.created_at WITH j.created_at + INTERVAL '1 day')) +FROM ( + users JOIN orders USING (id) +) AS j; + id | name | email | created_at | user_id | amount | status +----+------+-------+------------+---------+--------+-------- +(0 rows) + +-- CTEs with REPLACE +WITH base_users AS ( + SELECT * FROM users +) +SELECT * (REPLACE (base_users.created_at WITH NULL)) +FROM base_users +ORDER BY id; + id | name | email | created_at +----+-------+-------------------+------------ + 1 | Alice | alice@example.com | + 2 | Bob | bob@example.com | + 3 | Carol | | +(3 rows) + +-- Multiple FROM items with REPLACE +SELECT * (REPLACE (email WITH 'user@example.com', status WITH 'N/A')) FROM users, orders ORDER BY users.id, orders.id; + id | name | email | created_at | id | user_id | amount | status +----+-------+------------------+--------------------------+-----+---------+--------+-------- + 1 | Alice | user@example.com | Thu Jan 01 10:00:00 2026 | 101 | 1 | 50.00 | N/A + 1 | Alice | user@example.com | Thu Jan 01 10:00:00 2026 | 102 | 1 | 75.50 | N/A + 1 | Alice | user@example.com | Thu Jan 01 10:00:00 2026 | 103 | 2 | 20.00 | N/A + 2 | Bob | user@example.com | Fri Jan 02 11:00:00 2026 | 101 | 1 | 50.00 | N/A + 2 | Bob | user@example.com | Fri Jan 02 11:00:00 2026 | 102 | 1 | 75.50 | N/A + 2 | Bob | user@example.com | Fri Jan 02 11:00:00 2026 | 103 | 2 | 20.00 | N/A + 3 | Carol | user@example.com | Sat Jan 03 12:00:00 2026 | 101 | 1 | 50.00 | N/A + 3 | Carol | user@example.com | Sat Jan 03 12:00:00 2026 | 102 | 1 | 75.50 | N/A + 3 | Carol | user@example.com | Sat Jan 03 12:00:00 2026 | 103 | 2 | 20.00 | N/A +(9 rows) + +-- CROSS JOIN with REPLACE +SELECT * (REPLACE (email WITH 'user@example.com', status WITH 'N/A')) FROM users CROSS JOIN orders ORDER BY users.id, orders.id; + id | name | email | created_at | id | user_id | amount | status +----+-------+------------------+--------------------------+-----+---------+--------+-------- + 1 | Alice | user@example.com | Thu Jan 01 10:00:00 2026 | 101 | 1 | 50.00 | N/A + 1 | Alice | user@example.com | Thu Jan 01 10:00:00 2026 | 102 | 1 | 75.50 | N/A + 1 | Alice | user@example.com | Thu Jan 01 10:00:00 2026 | 103 | 2 | 20.00 | N/A + 2 | Bob | user@example.com | Fri Jan 02 11:00:00 2026 | 101 | 1 | 50.00 | N/A + 2 | Bob | user@example.com | Fri Jan 02 11:00:00 2026 | 102 | 1 | 75.50 | N/A + 2 | Bob | user@example.com | Fri Jan 02 11:00:00 2026 | 103 | 2 | 20.00 | N/A + 3 | Carol | user@example.com | Sat Jan 03 12:00:00 2026 | 101 | 1 | 50.00 | N/A + 3 | Carol | user@example.com | Sat Jan 03 12:00:00 2026 | 102 | 1 | 75.50 | N/A + 3 | Carol | user@example.com | Sat Jan 03 12:00:00 2026 | 103 | 2 | 20.00 | N/A +(9 rows) + +-- +-- RENAME clause +-- +-- Simple RENAME +SELECT * (RENAME (name AS user_name, email AS user_email)) +FROM users +ORDER BY id; + id | user_name | user_email | created_at +----+-----------+-------------------+-------------------------- + 1 | Alice | alice@example.com | Thu Jan 01 10:00:00 2026 + 2 | Bob | bob@example.com | Fri Jan 02 11:00:00 2026 + 3 | Carol | | Sat Jan 03 12:00:00 2026 +(3 rows) + +-- Aliasing with RENAME +SELECT * (RENAME (u.email AS user_email)) +FROM users AS u +ORDER BY u.id; + id | name | user_email | created_at +----+-------+-------------------+-------------------------- + 1 | Alice | alice@example.com | Thu Jan 01 10:00:00 2026 + 2 | Bob | bob@example.com | Fri Jan 02 11:00:00 2026 + 3 | Carol | | Sat Jan 03 12:00:00 2026 +(3 rows) + +-- Expressions with RENAME +SELECT * (RENAME (created_at AS date_created)), 1 + 1 AS two +FROM users +ORDER BY id; + id | name | email | date_created | two +----+-------+-------------------+--------------------------+----- + 1 | Alice | alice@example.com | Thu Jan 01 10:00:00 2026 | 2 + 2 | Bob | bob@example.com | Fri Jan 02 11:00:00 2026 | 2 + 3 | Carol | | Sat Jan 03 12:00:00 2026 | 2 +(3 rows) + +-- JOINs with RENAME +-- Join, unqualified RENAME +SELECT * (RENAME (created_at AS date_created)) +FROM users +JOIN orders ON orders.user_id = users.id +ORDER BY users.id, orders.id; + id | name | email | date_created | id | user_id | amount | status +----+-------+-------------------+--------------------------+-----+---------+--------+----------- + 1 | Alice | alice@example.com | Thu Jan 01 10:00:00 2026 | 101 | 1 | 50.00 | paid + 1 | Alice | alice@example.com | Thu Jan 01 10:00:00 2026 | 102 | 1 | 75.50 | shipped + 2 | Bob | bob@example.com | Fri Jan 02 11:00:00 2026 | 103 | 2 | 20.00 | cancelled +(3 rows) + +-- Join, qualified RENAME +SELECT * (RENAME (users.created_at AS date_created, orders.amount AS order_amount)) +FROM users +JOIN orders ON orders.user_id = users.id +ORDER BY users.id, orders.id; + id | name | email | date_created | id | user_id | order_amount | status +----+-------+-------------------+--------------------------+-----+---------+--------------+----------- + 1 | Alice | alice@example.com | Thu Jan 01 10:00:00 2026 | 101 | 1 | 50.00 | paid + 1 | Alice | alice@example.com | Thu Jan 01 10:00:00 2026 | 102 | 1 | 75.50 | shipped + 2 | Bob | bob@example.com | Fri Jan 02 11:00:00 2026 | 103 | 2 | 20.00 | cancelled +(3 rows) + +-- Join, aliased tables with RENAME +SELECT * (RENAME (u.created_at AS date_created, o.amount AS order_amount)) +FROM users AS u +JOIN orders AS o ON o.user_id = u.id +ORDER BY u.id, o.id; + id | name | email | date_created | id | user_id | order_amount | status +----+-------+-------------------+--------------------------+-----+---------+--------------+----------- + 1 | Alice | alice@example.com | Thu Jan 01 10:00:00 2026 | 101 | 1 | 50.00 | paid + 1 | Alice | alice@example.com | Thu Jan 01 10:00:00 2026 | 102 | 1 | 75.50 | shipped + 2 | Bob | bob@example.com | Fri Jan 02 11:00:00 2026 | 103 | 2 | 20.00 | cancelled +(3 rows) + +-- Qualified stars; RENAME is applied to each star separately +SELECT +users.* (RENAME (id AS uid)), +orders.* (RENAME (user_id AS customer_id)) +FROM users +LEFT JOIN orders ON orders.user_id = users.id +ORDER BY users.id, orders.id; + uid | name | email | created_at | id | customer_id | amount | status +-----+-------+-------------------+--------------------------+-----+-------------+--------+----------- + 1 | Alice | alice@example.com | Thu Jan 01 10:00:00 2026 | 101 | 1 | 50.00 | paid + 1 | Alice | alice@example.com | Thu Jan 01 10:00:00 2026 | 102 | 1 | 75.50 | shipped + 2 | Bob | bob@example.com | Fri Jan 02 11:00:00 2026 | 103 | 2 | 20.00 | cancelled + 3 | Carol | | Sat Jan 03 12:00:00 2026 | | | | +(4 rows) + +-- Schema-qualified star +SELECT s1.products.* (RENAME (price AS product_price)) +FROM s1.products +ORDER BY id; + id | name | product_price +----+-----------+--------------- + 1 | Product A | 10.00 + 2 | Product B | 15.50 +(2 rows) + +-- Schema-qualified column +SELECT * (RENAME (s1.products.price AS product_price)) +FROM s1.products +ORDER BY id; + id | name | product_price +----+-----------+--------------- + 1 | Product A | 10.00 + 2 | Product B | 15.50 +(2 rows) + +-- Name collision +SELECT * (RENAME (id AS _id)) +FROM users +JOIN orders ON orders.user_id = users.id +ORDER BY users.id, orders.id; + _id | name | email | created_at | _id | user_id | amount | status +-----+-------+-------------------+--------------------------+-----+---------+--------+----------- + 1 | Alice | alice@example.com | Thu Jan 01 10:00:00 2026 | 101 | 1 | 50.00 | paid + 1 | Alice | alice@example.com | Thu Jan 01 10:00:00 2026 | 102 | 1 | 75.50 | shipped + 2 | Bob | bob@example.com | Fri Jan 02 11:00:00 2026 | 103 | 2 | 20.00 | cancelled +(3 rows) + +-- Subqueries with RENAME +SELECT * (RENAME (u.created_at AS date_created)) +FROM ( + SELECT * FROM users +) u +ORDER BY id; + id | name | email | date_created +----+-------+-------------------+-------------------------- + 1 | Alice | alice@example.com | Thu Jan 01 10:00:00 2026 + 2 | Bob | bob@example.com | Fri Jan 02 11:00:00 2026 + 3 | Carol | | Sat Jan 03 12:00:00 2026 +(3 rows) + +SELECT * (RENAME (j.created_at AS date_created)) +FROM ( + users JOIN orders USING (id) +) AS j; + id | name | email | date_created | user_id | amount | status +----+------+-------+--------------+---------+--------+-------- +(0 rows) + +-- CTEs with RENAME +WITH base_users AS ( + SELECT * FROM users +) +SELECT * (RENAME (base_users.created_at AS date_created)) +FROM base_users +ORDER BY id; + id | name | email | date_created +----+-------+-------------------+-------------------------- + 1 | Alice | alice@example.com | Thu Jan 01 10:00:00 2026 + 2 | Bob | bob@example.com | Fri Jan 02 11:00:00 2026 + 3 | Carol | | Sat Jan 03 12:00:00 2026 +(3 rows) + +-- Multiple FROM items with RENAME +SELECT * (RENAME (email AS user_email, status AS order_status)) FROM users, orders ORDER BY users.id, orders.id; + id | name | user_email | created_at | id | user_id | amount | order_status +----+-------+-------------------+--------------------------+-----+---------+--------+-------------- + 1 | Alice | alice@example.com | Thu Jan 01 10:00:00 2026 | 101 | 1 | 50.00 | paid + 1 | Alice | alice@example.com | Thu Jan 01 10:00:00 2026 | 102 | 1 | 75.50 | shipped + 1 | Alice | alice@example.com | Thu Jan 01 10:00:00 2026 | 103 | 2 | 20.00 | cancelled + 2 | Bob | bob@example.com | Fri Jan 02 11:00:00 2026 | 101 | 1 | 50.00 | paid + 2 | Bob | bob@example.com | Fri Jan 02 11:00:00 2026 | 102 | 1 | 75.50 | shipped + 2 | Bob | bob@example.com | Fri Jan 02 11:00:00 2026 | 103 | 2 | 20.00 | cancelled + 3 | Carol | | Sat Jan 03 12:00:00 2026 | 101 | 1 | 50.00 | paid + 3 | Carol | | Sat Jan 03 12:00:00 2026 | 102 | 1 | 75.50 | shipped + 3 | Carol | | Sat Jan 03 12:00:00 2026 | 103 | 2 | 20.00 | cancelled +(9 rows) + +-- CROSS JOIN with RENAME +SELECT * (RENAME (email AS user_email, status AS order_status)) FROM users CROSS JOIN orders ORDER BY users.id, orders.id; + id | name | user_email | created_at | id | user_id | amount | order_status +----+-------+-------------------+--------------------------+-----+---------+--------+-------------- + 1 | Alice | alice@example.com | Thu Jan 01 10:00:00 2026 | 101 | 1 | 50.00 | paid + 1 | Alice | alice@example.com | Thu Jan 01 10:00:00 2026 | 102 | 1 | 75.50 | shipped + 1 | Alice | alice@example.com | Thu Jan 01 10:00:00 2026 | 103 | 2 | 20.00 | cancelled + 2 | Bob | bob@example.com | Fri Jan 02 11:00:00 2026 | 101 | 1 | 50.00 | paid + 2 | Bob | bob@example.com | Fri Jan 02 11:00:00 2026 | 102 | 1 | 75.50 | shipped + 2 | Bob | bob@example.com | Fri Jan 02 11:00:00 2026 | 103 | 2 | 20.00 | cancelled + 3 | Carol | | Sat Jan 03 12:00:00 2026 | 101 | 1 | 50.00 | paid + 3 | Carol | | Sat Jan 03 12:00:00 2026 | 102 | 1 | 75.50 | shipped + 3 | Carol | | Sat Jan 03 12:00:00 2026 | 103 | 2 | 20.00 | cancelled +(9 rows) + +-- +-- EXCLUDE/REPLACE/RENAME combinations +-- +-- EXCLUDE + REPLACE different columns +SELECT * (EXCLUDE (created_at) REPLACE (email WITH 'user@example.com')) +FROM users; + id | name | email +----+-------+------------------ + 1 | Alice | user@example.com + 2 | Bob | user@example.com + 3 | Carol | user@example.com +(3 rows) + +-- EXCLUDE + RENAME different columns +SELECT * (EXCLUDE (created_at) RENAME (email AS user_email)) +FROM users; + id | name | user_email +----+-------+------------------- + 1 | Alice | alice@example.com + 2 | Bob | bob@example.com + 3 | Carol | +(3 rows) + +-- REPLACE + RENAME on same column +SELECT * (REPLACE (email WITH 'user@example.com', name WITH 'Unknown') RENAME (email AS user_email, name AS user_name)) +FROM users +ORDER BY id; + id | user_name | user_email | created_at +----+-----------+------------------+-------------------------- + 1 | Unknown | user@example.com | Thu Jan 01 10:00:00 2026 + 2 | Unknown | user@example.com | Fri Jan 02 11:00:00 2026 + 3 | Unknown | user@example.com | Sat Jan 03 12:00:00 2026 +(3 rows) + +-- REPLACE + RENAME on same column with EXCLUDE +SELECT * (EXCLUDE (created_at) REPLACE (email WITH 'user@example.com') RENAME (email AS user_email)) +FROM users; + id | name | user_email +----+-------+------------------ + 1 | Alice | user@example.com + 2 | Bob | user@example.com + 3 | Carol | user@example.com +(3 rows) + +-- All three clauses on different columns +SELECT * (EXCLUDE (created_at) REPLACE (email WITH 'user@example.com') RENAME (name AS user_name)) +FROM users +ORDER BY id; + id | user_name | email +----+-----------+------------------ + 1 | Alice | user@example.com + 2 | Bob | user@example.com + 3 | Carol | user@example.com +(3 rows) + +-- JOIN, unqualified +SELECT * (EXCLUDE (created_at) REPLACE (email WITH 'user@example.com') RENAME (amount AS order_amount)) +FROM users +JOIN orders ON orders.user_id = users.id +ORDER BY users.id, orders.id; + id | name | email | id | user_id | order_amount | status +----+-------+------------------+-----+---------+--------------+----------- + 1 | Alice | user@example.com | 101 | 1 | 50.00 | paid + 1 | Alice | user@example.com | 102 | 1 | 75.50 | shipped + 2 | Bob | user@example.com | 103 | 2 | 20.00 | cancelled +(3 rows) + +-- JOIN, qualified + aliased tables +SELECT * (EXCLUDE (u.created_at) REPLACE (u.email WITH 'user@example.com') RENAME (o.amount AS order_amount)) +FROM users AS u +JOIN orders AS o ON o.user_id = u.id +ORDER BY u.id, o.id; + id | name | email | id | user_id | order_amount | status +----+-------+------------------+-----+---------+--------------+----------- + 1 | Alice | user@example.com | 101 | 1 | 50.00 | paid + 1 | Alice | user@example.com | 102 | 1 | 75.50 | shipped + 2 | Bob | user@example.com | 103 | 2 | 20.00 | cancelled +(3 rows) + +-- Qualified stars with all three +SELECT +users.* (EXCLUDE (email) REPLACE (name WITH 'Unknown') RENAME (id AS uid)), +orders.* (EXCLUDE (status) REPLACE (amount WITH amount * 2) RENAME (id AS oid)) +FROM users +JOIN orders ON orders.user_id = users.id +ORDER BY users.id, orders.id; + uid | name | created_at | oid | user_id | amount +-----+---------+--------------------------+-----+---------+-------- + 1 | Unknown | Thu Jan 01 10:00:00 2026 | 101 | 1 | 100.00 + 1 | Unknown | Thu Jan 01 10:00:00 2026 | 102 | 1 | 151.00 + 2 | Unknown | Fri Jan 02 11:00:00 2026 | 103 | 2 | 40.00 +(3 rows) + +-- Schema-qualified star +SELECT s1.products.* (EXCLUDE (price) REPLACE (name WITH 'Product X') RENAME (id AS product_id)) +FROM s1.products +ORDER BY id; + product_id | name +------------+----------- + 1 | Product X + 2 | Product X +(2 rows) + +-- Schema-qualified column +SELECT * (EXCLUDE (s1.products.price) REPLACE (s1.products.name WITH 'Product X') RENAME (s1.products.id AS product_id)) +FROM s1.products +ORDER BY id; + product_id | name +------------+----------- + 1 | Product X + 2 | Product X +(2 rows) + +-- Subquery with all three +SELECT * (EXCLUDE (u.created_at) REPLACE (u.email WITH 'user@example.com') RENAME (u.name AS user_name)) +FROM ( + SELECT * FROM users +) AS u +ORDER BY id; + id | user_name | email +----+-----------+------------------ + 1 | Alice | user@example.com + 2 | Bob | user@example.com + 3 | Carol | user@example.com +(3 rows) + +-- CTE with all three +WITH base_users AS ( + SELECT * FROM users +) +SELECT * (EXCLUDE (base_users.created_at) REPLACE (base_users.email WITH 'user@example.com') RENAME (base_users.name AS user_name)) +FROM base_users +ORDER BY id; + id | user_name | email +----+-----------+------------------ + 1 | Alice | user@example.com + 2 | Bob | user@example.com + 3 | Carol | user@example.com +(3 rows) + +-- CROSS JOIN with all three +SELECT * (EXCLUDE (status) REPLACE (email WITH 'user@example.com') RENAME (users.id AS uid, orders.id AS oid)) FROM users, orders ORDER BY users.id, orders.id; + uid | name | email | created_at | oid | user_id | amount +-----+-------+------------------+--------------------------+-----+---------+-------- + 1 | Alice | user@example.com | Thu Jan 01 10:00:00 2026 | 101 | 1 | 50.00 + 1 | Alice | user@example.com | Thu Jan 01 10:00:00 2026 | 102 | 1 | 75.50 + 1 | Alice | user@example.com | Thu Jan 01 10:00:00 2026 | 103 | 2 | 20.00 + 2 | Bob | user@example.com | Fri Jan 02 11:00:00 2026 | 101 | 1 | 50.00 + 2 | Bob | user@example.com | Fri Jan 02 11:00:00 2026 | 102 | 1 | 75.50 + 2 | Bob | user@example.com | Fri Jan 02 11:00:00 2026 | 103 | 2 | 20.00 + 3 | Carol | user@example.com | Sat Jan 03 12:00:00 2026 | 101 | 1 | 50.00 + 3 | Carol | user@example.com | Sat Jan 03 12:00:00 2026 | 102 | 1 | 75.50 + 3 | Carol | user@example.com | Sat Jan 03 12:00:00 2026 | 103 | 2 | 20.00 +(9 rows) + +-- Test 3-part qualification with JOIN on different schema tables +SELECT * (EXCLUDE (s1.products.name) REPLACE (s2.products.name WITH 'Z') RENAME (s1.products.id AS p1_id, s2.products.id AS p2_id)) +FROM s1.products +JOIN s2.products ON s1.products.id = s2.products.id +ORDER BY s1.products.id; + p1_id | price | p2_id | name | price | description +-------+-------+-------+------+-------+------------------- + 1 | 10.00 | 1 | Z | 20.00 | Description for X + 2 | 15.50 | 2 | Z | 25.75 | Description for Y +(2 rows) + +-- Test 4-part qualification +SELECT * (EXCLUDE (regression.public.users.email)) +FROM users +ORDER BY id; + id | name | created_at +----+-------+-------------------------- + 1 | Alice | Thu Jan 01 10:00:00 2026 + 2 | Bob | Fri Jan 02 11:00:00 2026 + 3 | Carol | Sat Jan 03 12:00:00 2026 +(3 rows) + +-- SELECT INTO with star options +CREATE TABLE tbl_a (a int, b int, c int); +CREATE TABLE tbl_b (c int, d int, e int); +INSERT INTO tbl_a VALUES (1, 2, 3), (4, 5, 6); +INSERT INTO tbl_b VALUES (6, 7, 8), (9, 10, 11); +SELECT * (EXCLUDE (b, e)) INTO tbl_copy +FROM tbl_a +JOIN tbl_b ON tbl_a.c = tbl_b.c +ORDER BY tbl_a.a, tbl_b.c; -- error, as c is in both tables +ERROR: column "c" specified more than once +SELECT * (EXCLUDE (ta.c, tb.d)) INTO tbl_copy +FROM tbl_a AS ta +JOIN tbl_b AS tb ON ta.c = tb.c +ORDER BY ta.a, tb.c; +SELECT * FROM tbl_copy; + a | b | c | e +---+---+---+--- + 4 | 5 | 6 | 8 +(1 row) + +-- INSERT INTO ... SELECT with star options +INSERT INTO tbl_copy SELECT * (EXCLUDE (ta.c, tb.d)) +FROM tbl_a AS ta +JOIN tbl_b AS tb ON ta.c = tb.c +ORDER BY ta.a, tb.c; +SELECT * FROM tbl_copy; + a | b | c | e +---+---+---+--- + 4 | 5 | 6 | 8 + 4 | 5 | 6 | 8 +(2 rows) + +-- RETURNING with star options +INSERT INTO tbl_copy SELECT * (EXCLUDE (b)) +FROM tbl_a +RETURNING * (EXCLUDE (c)); -- error, cannot use star options with RETURNING clause +ERROR: RETURNING with star options is not supported +LINE 3: RETURNING * (EXCLUDE (c)); + ^ +INSERT INTO tbl_copy SELECT * (EXCLUDE (ta.c, tb.d)) +FROM tbl_a AS ta +JOIN tbl_b AS tb ON ta.c = tb.c +RETURNING *; + a | b | c | e +---+---+---+--- + 4 | 5 | 6 | 8 +(1 row) + +-- Error cases +-- Non-existent columns (error case) +SELECT * (EXCLUDE (does_not_exist)) FROM users; +ERROR: column "does_not_exist" does not exist +LINE 1: SELECT * (EXCLUDE (does_not_exist)) FROM users; + ^ +SELECT * (REPLACE (does_not_exist WITH 'user@example.com')) FROM users; +ERROR: column "does_not_exist" does not exist +LINE 1: SELECT * (REPLACE (does_not_exist WITH 'user@example.com')) ... + ^ +SELECT * (RENAME (does_not_exist AS user_email)) FROM users; +ERROR: column "does_not_exist" does not exist +LINE 1: SELECT * (RENAME (does_not_exist AS user_email)) FROM users; + ^ +-- Empty lists (error case) +SELECT * (EXCLUDE ()) FROM users; +ERROR: syntax error at or near ")" +LINE 1: SELECT * (EXCLUDE ()) FROM users; + ^ +SELECT * (REPLACE ()) FROM users; +ERROR: syntax error at or near ")" +LINE 1: SELECT * (REPLACE ()) FROM users; + ^ +SELECT * (RENAME ()) FROM users; +ERROR: syntax error at or near ")" +LINE 1: SELECT * (RENAME ()) FROM users; + ^ +-- duplicate column names (error case) +SELECT * (EXCLUDE (id, id)) FROM users; +ERROR: duplicate column "id" in EXCLUDE list +SELECT * (REPLACE (email WITH 'user@example.com', email WITH NULL)) FROM users; +ERROR: duplicate column "email" in REPLACE list +SELECT * (RENAME (email AS user_email, email AS user_name)) FROM users; +ERROR: duplicate column "email" in RENAME list +SELECT * () +FROM users; +ERROR: syntax error at or near ")" +LINE 1: SELECT * () + ^ +-- EXCLUDE + REPLACE same column (error case) +SELECT * (EXCLUDE (id) REPLACE (id WITH 999)) +FROM users; +ERROR: column "id" cannot occur in both EXCLUDE and REPLACE list +-- EXCLUDE + RENAME same column (error case) +SELECT * (EXCLUDE (id) RENAME (id AS user_id)) +FROM users; +ERROR: column "id" cannot occur in both EXCLUDE and RENAME list +-- Cannot use star inside options list (error case) +SELECT * (EXCLUDE (users.*)) +FROM users +ORDER BY id; +ERROR: syntax error at or near ")" +LINE 1: SELECT * (EXCLUDE (users.*)) + ^ +-- Incorrect use of table alias (error case) +SELECT u.* (EXCLUDE (users.email)) FROM users u; +ERROR: invalid reference to FROM-clause entry for table "users" +LINE 1: SELECT u.* (EXCLUDE (users.email)) FROM users u; + ^ +HINT: Perhaps you meant to reference the table alias "u". +-- Test dropping a column +ALTER TABLE users DROP COLUMN email; +SELECT * (EXCLUDE (users.email)) FROM users; -- error, as email column no longer exists +ERROR: column users.email does not exist +LINE 1: SELECT * (EXCLUDE (users.email)) FROM users; + ^ +SELECT * (EXCLUDE (name)) FROM users; + id | created_at +----+-------------------------- + 1 | Thu Jan 01 10:00:00 2026 + 2 | Fri Jan 02 11:00:00 2026 + 3 | Sat Jan 03 12:00:00 2026 +(3 rows) + +-- Apply star options to a column from a different table in a multi-table query (error case) +SELECT +users.* (EXCLUDE (orders.amount)) +FROM users, orders; +ERROR: column orders.amount does not exist +LINE 2: users.* (EXCLUDE (orders.amount)) + ^ +DETAIL: There is a column named "amount" in table "orders", but it cannot be referenced from this part of the query. +-- Check privilege handling with star options +CREATE USER tuser; +GRANT SELECT (name) ON TABLE users TO tuser; +SET ROLE tuser; +SELECT * (EXCLUDE (id, created_at)) FROM users; -- should succeed, as id and created_at are excluded + name +------- + Alice + Bob + Carol +(3 rows) + +SELECT * (REPLACE (created_at WITH NULL)) FROM users; -- should fail, as created_at is not accessible +ERROR: permission denied for table users +SELECT * (RENAME (created_at AS date_created)) FROM users; -- should fail, as created_at is not accessible +ERROR: permission denied for table users +RESET ROLE; +-- clean up +DROP TABLE orders; +DROP TABLE users; +DROP TABLE tbl_a; +DROP TABLE tbl_b; +DROP TABLE tbl_copy; +DROP TABLE s1.products; +DROP TABLE s2.products; +DROP SCHEMA s1; +DROP SCHEMA s2; +DROP user tuser; diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index 75063f87a4a..5fde04091f0 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -102,7 +102,7 @@ test: publication subscription # Another group of parallel tests # select_views depends on create_view # ---------- -test: select_views portals_p2 foreign_key dependency guc bitmapops combocid tsearch tsdicts foreign_data window xmlmap functional_deps advisory_lock indirect_toast equivclass stats_rewrite +test: select_views select_staroptions portals_p2 foreign_key dependency guc bitmapops combocid tsearch tsdicts foreign_data window xmlmap functional_deps advisory_lock indirect_toast equivclass stats_rewrite # ---------- # Another group of parallel tests (JSON related) diff --git a/src/test/regress/sql/select_staroptions.sql b/src/test/regress/sql/select_staroptions.sql new file mode 100644 index 00000000000..d24f91ad4c0 --- /dev/null +++ b/src/test/regress/sql/select_staroptions.sql @@ -0,0 +1,519 @@ +-- +-- SELECT EXCLUDE/REPLACE/RENAME +-- + +SET client_min_messages TO 'warning'; + +DROP TABLE IF EXISTS users; +DROP TABLE IF EXISTS orders; + +RESET client_min_messages; + +CREATE TABLE users ( + id INT PRIMARY KEY, + name TEXT, + email TEXT, + created_at TIMESTAMP +); + +CREATE TABLE orders ( + id INT PRIMARY KEY, + user_id INT REFERENCES users(id), + amount NUMERIC(10,2), + status TEXT +); + +CREATE SCHEMA s1; +CREATE SCHEMA s2; + +CREATE TABLE s1.products ( + id INT PRIMARY KEY, + name TEXT, + price NUMERIC(10,2) +); + +CREATE TABLE s2.products ( + id INT PRIMARY KEY, + name TEXT, + price NUMERIC(10,2), + description TEXT +); + +-- Insert sample data +INSERT INTO users (id, name, email, created_at) VALUES +(1, 'Alice', 'alice@example.com', '2026-01-01 10:00:00'), +(2, 'Bob', 'bob@example.com', '2026-01-02 11:00:00'), +(3, 'Carol', NULL, '2026-01-03 12:00:00'); + +INSERT INTO orders (id, user_id, amount, status) VALUES +(101, 1, 50.00, 'paid'), +(102, 1, 75.50, 'shipped'), +(103, 2, 20.00, 'cancelled'); + +INSERT INTO s1.products (id, name, price) VALUES +(1, 'Product A', 10.00), +(2, 'Product B', 15.50); + +INSERT INTO s2.products (id, name, price, description) VALUES +(1, 'Product X', 20.00, 'Description for X'), +(2, 'Product Y', 25.75, 'Description for Y'); + +-- +-- EXCLUDE clause +-- +-- Simple EXCLUDE +SELECT * (EXCLUDE (email, created_at)) +FROM users +ORDER BY id; + +-- Exclude all but one column +SELECT * (EXCLUDE (name, email, created_at)) +FROM users +ORDER BY id; + +-- Exclude all columns +SELECT * (EXCLUDE (id, name, email, created_at)) +FROM users; + +-- EXCLUDE all using exclude list but overall SELECT list is not empty +SELECT id, users.* (EXCLUDE (id, name, email, created_at)) +FROM users +ORDER BY id; + +-- Aliasing with EXCLUDE +SELECT * (EXCLUDE (u.email)) +FROM users AS u +ORDER BY u.id; + +-- Expressions with EXCLUDE +SELECT * (EXCLUDE (name)), 1 + 1 AS two +FROM users +ORDER BY id; + +-- JOINs with EXCLUDE +-- Join, unqualified EXCLUDE +SELECT * (EXCLUDE (created_at)) +FROM users +JOIN orders ON orders.user_id = users.id +ORDER BY users.id, orders.id; + +-- Join, qualified EXCLUDE +SELECT * (EXCLUDE (users.created_at, orders.amount)) +FROM users +JOIN orders ON orders.user_id = users.id +ORDER BY users.id, orders.id; + +-- Join, aliased tables with EXCLUDE +SELECT * (EXCLUDE (u.created_at, o.amount)) +FROM users AS u +JOIN orders AS o ON o.user_id = u.id +ORDER BY u.id, o.id; + +-- Qualified stars; EXCLUDE is applied to each star separately +SELECT +users.* (EXCLUDE (id)), +orders.* (EXCLUDE (user_id)) +FROM users +LEFT JOIN orders ON orders.user_id = users.id +ORDER BY users.id, orders.id; + +-- Schema-qualified star +SELECT s1.products.* (EXCLUDE (price)) +FROM s1.products +ORDER BY id; + +-- Schema-qualified column +SELECT * (EXCLUDE (s1.products.price)) +FROM s1.products +ORDER BY id; + +-- Name collision +SELECT * (EXCLUDE (id)) +FROM users +JOIN orders ON orders.user_id = users.id +ORDER BY users.id, orders.id; + +-- Subqueries with EXCLUDE +SELECT * (EXCLUDE (u.created_at)) +FROM ( + SELECT * FROM users +) u +ORDER BY id; + +SELECT * (EXCLUDE (j.created_at)) +FROM ( + users JOIN orders USING (id) +) AS j; + +-- CTEs with EXCLUDE +WITH base_users AS ( + SELECT * FROM users +) +SELECT * (EXCLUDE (base_users.created_at)) +FROM base_users +ORDER BY id; + +-- Multiple FROM items with EXCLUDE +SELECT * (EXCLUDE (email, status)) FROM users, orders ORDER BY users.id, orders.id; + +-- CROSS JOIN with EXCLUDE +SELECT * (EXCLUDE (email, status)) FROM users CROSS JOIN orders ORDER BY users.id, orders.id; + +-- +-- REPLACE clause +-- +-- Simple REPLACE +SELECT * (REPLACE (email WITH 'user@example.com', created_at WITH NULL)) +FROM users +ORDER BY id; + +-- Aliasing with REPLACE +SELECT * (REPLACE (u.email WITH 'user@example.com')) +FROM users AS u +ORDER BY u.id; + +-- Expressions with REPLACE +SELECT * (REPLACE (created_at WITH NULL)), 1 + 1 AS two +FROM users +ORDER BY id; + +-- JOINs with REPLACE +-- Join, unqualified REPLACE +SELECT * (REPLACE (created_at WITH created_at + INTERVAL '1 day')) +FROM users +JOIN orders ON orders.user_id = users.id +ORDER BY users.id, orders.id; + +-- Join, qualified REPLACE +SELECT * (REPLACE (users.created_at WITH NULL, orders.amount WITH orders.amount * 2)) +FROM users +JOIN orders ON orders.user_id = users.id +ORDER BY users.id, orders.id; + +-- Join, aliased tables with REPLACE +SELECT * (REPLACE (u.created_at WITH NULL, o.amount WITH o.amount * 2)) +FROM users AS u +JOIN orders AS o ON o.user_id = u.id +ORDER BY u.id, o.id; + +-- Qualified stars; REPLACE is applied to each star separately +SELECT +users.* (REPLACE (id WITH 999)), +orders.* (REPLACE (user_id WITH NULL)) +FROM users +LEFT JOIN orders ON orders.user_id = users.id +ORDER BY users.id, orders.id; + +-- Schema-qualified star +SELECT s1.products.* (REPLACE (price WITH price * 1.1)) +FROM s1.products +ORDER BY id; + +-- Schema-qualified column +SELECT * (REPLACE (s1.products.price WITH s1.products.price * 1.1)) +FROM s1.products +ORDER BY id; + +-- Name collision +SELECT * (REPLACE (id WITH 999)) +FROM users +JOIN orders ON orders.user_id = users.id +ORDER BY users.id, orders.id; + +-- Subqueries with REPLACE +SELECT * (REPLACE (u.created_at WITH u.created_at + INTERVAL '1 day')) +FROM ( + SELECT * FROM users +) u +ORDER BY id; + +SELECT * (REPLACE (j.created_at WITH j.created_at + INTERVAL '1 day')) +FROM ( + users JOIN orders USING (id) +) AS j; + +-- CTEs with REPLACE +WITH base_users AS ( + SELECT * FROM users +) +SELECT * (REPLACE (base_users.created_at WITH NULL)) +FROM base_users +ORDER BY id; + +-- Multiple FROM items with REPLACE +SELECT * (REPLACE (email WITH 'user@example.com', status WITH 'N/A')) FROM users, orders ORDER BY users.id, orders.id; + +-- CROSS JOIN with REPLACE +SELECT * (REPLACE (email WITH 'user@example.com', status WITH 'N/A')) FROM users CROSS JOIN orders ORDER BY users.id, orders.id; + +-- +-- RENAME clause +-- +-- Simple RENAME +SELECT * (RENAME (name AS user_name, email AS user_email)) +FROM users +ORDER BY id; + +-- Aliasing with RENAME +SELECT * (RENAME (u.email AS user_email)) +FROM users AS u +ORDER BY u.id; + +-- Expressions with RENAME +SELECT * (RENAME (created_at AS date_created)), 1 + 1 AS two +FROM users +ORDER BY id; + +-- JOINs with RENAME +-- Join, unqualified RENAME +SELECT * (RENAME (created_at AS date_created)) +FROM users +JOIN orders ON orders.user_id = users.id +ORDER BY users.id, orders.id; + +-- Join, qualified RENAME +SELECT * (RENAME (users.created_at AS date_created, orders.amount AS order_amount)) +FROM users +JOIN orders ON orders.user_id = users.id +ORDER BY users.id, orders.id; + +-- Join, aliased tables with RENAME +SELECT * (RENAME (u.created_at AS date_created, o.amount AS order_amount)) +FROM users AS u +JOIN orders AS o ON o.user_id = u.id +ORDER BY u.id, o.id; + +-- Qualified stars; RENAME is applied to each star separately +SELECT +users.* (RENAME (id AS uid)), +orders.* (RENAME (user_id AS customer_id)) +FROM users +LEFT JOIN orders ON orders.user_id = users.id +ORDER BY users.id, orders.id; + +-- Schema-qualified star +SELECT s1.products.* (RENAME (price AS product_price)) +FROM s1.products +ORDER BY id; + +-- Schema-qualified column +SELECT * (RENAME (s1.products.price AS product_price)) +FROM s1.products +ORDER BY id; + +-- Name collision +SELECT * (RENAME (id AS _id)) +FROM users +JOIN orders ON orders.user_id = users.id +ORDER BY users.id, orders.id; + +-- Subqueries with RENAME +SELECT * (RENAME (u.created_at AS date_created)) +FROM ( + SELECT * FROM users +) u +ORDER BY id; + +SELECT * (RENAME (j.created_at AS date_created)) +FROM ( + users JOIN orders USING (id) +) AS j; + +-- CTEs with RENAME +WITH base_users AS ( + SELECT * FROM users +) +SELECT * (RENAME (base_users.created_at AS date_created)) +FROM base_users +ORDER BY id; + +-- Multiple FROM items with RENAME +SELECT * (RENAME (email AS user_email, status AS order_status)) FROM users, orders ORDER BY users.id, orders.id; + +-- CROSS JOIN with RENAME +SELECT * (RENAME (email AS user_email, status AS order_status)) FROM users CROSS JOIN orders ORDER BY users.id, orders.id; + +-- +-- EXCLUDE/REPLACE/RENAME combinations +-- +-- EXCLUDE + REPLACE different columns +SELECT * (EXCLUDE (created_at) REPLACE (email WITH 'user@example.com')) +FROM users; + +-- EXCLUDE + RENAME different columns +SELECT * (EXCLUDE (created_at) RENAME (email AS user_email)) +FROM users; + +-- REPLACE + RENAME on same column +SELECT * (REPLACE (email WITH 'user@example.com', name WITH 'Unknown') RENAME (email AS user_email, name AS user_name)) +FROM users +ORDER BY id; + +-- REPLACE + RENAME on same column with EXCLUDE +SELECT * (EXCLUDE (created_at) REPLACE (email WITH 'user@example.com') RENAME (email AS user_email)) +FROM users; + +-- All three clauses on different columns +SELECT * (EXCLUDE (created_at) REPLACE (email WITH 'user@example.com') RENAME (name AS user_name)) +FROM users +ORDER BY id; + +-- JOIN, unqualified +SELECT * (EXCLUDE (created_at) REPLACE (email WITH 'user@example.com') RENAME (amount AS order_amount)) +FROM users +JOIN orders ON orders.user_id = users.id +ORDER BY users.id, orders.id; + +-- JOIN, qualified + aliased tables +SELECT * (EXCLUDE (u.created_at) REPLACE (u.email WITH 'user@example.com') RENAME (o.amount AS order_amount)) +FROM users AS u +JOIN orders AS o ON o.user_id = u.id +ORDER BY u.id, o.id; + +-- Qualified stars with all three +SELECT +users.* (EXCLUDE (email) REPLACE (name WITH 'Unknown') RENAME (id AS uid)), +orders.* (EXCLUDE (status) REPLACE (amount WITH amount * 2) RENAME (id AS oid)) +FROM users +JOIN orders ON orders.user_id = users.id +ORDER BY users.id, orders.id; + +-- Schema-qualified star +SELECT s1.products.* (EXCLUDE (price) REPLACE (name WITH 'Product X') RENAME (id AS product_id)) +FROM s1.products +ORDER BY id; + +-- Schema-qualified column +SELECT * (EXCLUDE (s1.products.price) REPLACE (s1.products.name WITH 'Product X') RENAME (s1.products.id AS product_id)) +FROM s1.products +ORDER BY id; + +-- Subquery with all three +SELECT * (EXCLUDE (u.created_at) REPLACE (u.email WITH 'user@example.com') RENAME (u.name AS user_name)) +FROM ( + SELECT * FROM users +) AS u +ORDER BY id; + +-- CTE with all three +WITH base_users AS ( + SELECT * FROM users +) +SELECT * (EXCLUDE (base_users.created_at) REPLACE (base_users.email WITH 'user@example.com') RENAME (base_users.name AS user_name)) +FROM base_users +ORDER BY id; + +-- CROSS JOIN with all three +SELECT * (EXCLUDE (status) REPLACE (email WITH 'user@example.com') RENAME (users.id AS uid, orders.id AS oid)) FROM users, orders ORDER BY users.id, orders.id; + +-- Test 3-part qualification with JOIN on different schema tables +SELECT * (EXCLUDE (s1.products.name) REPLACE (s2.products.name WITH 'Z') RENAME (s1.products.id AS p1_id, s2.products.id AS p2_id)) +FROM s1.products +JOIN s2.products ON s1.products.id = s2.products.id +ORDER BY s1.products.id; + +-- Test 4-part qualification +SELECT * (EXCLUDE (regression.public.users.email)) +FROM users +ORDER BY id; + +-- SELECT INTO with star options +CREATE TABLE tbl_a (a int, b int, c int); +CREATE TABLE tbl_b (c int, d int, e int); +INSERT INTO tbl_a VALUES (1, 2, 3), (4, 5, 6); +INSERT INTO tbl_b VALUES (6, 7, 8), (9, 10, 11); + +SELECT * (EXCLUDE (b, e)) INTO tbl_copy +FROM tbl_a +JOIN tbl_b ON tbl_a.c = tbl_b.c +ORDER BY tbl_a.a, tbl_b.c; -- error, as c is in both tables + +SELECT * (EXCLUDE (ta.c, tb.d)) INTO tbl_copy +FROM tbl_a AS ta +JOIN tbl_b AS tb ON ta.c = tb.c +ORDER BY ta.a, tb.c; + +SELECT * FROM tbl_copy; + +-- INSERT INTO ... SELECT with star options +INSERT INTO tbl_copy SELECT * (EXCLUDE (ta.c, tb.d)) +FROM tbl_a AS ta +JOIN tbl_b AS tb ON ta.c = tb.c +ORDER BY ta.a, tb.c; + +SELECT * FROM tbl_copy; + +-- RETURNING with star options +INSERT INTO tbl_copy SELECT * (EXCLUDE (b)) +FROM tbl_a +RETURNING * (EXCLUDE (c)); -- error, cannot use star options with RETURNING clause + +INSERT INTO tbl_copy SELECT * (EXCLUDE (ta.c, tb.d)) +FROM tbl_a AS ta +JOIN tbl_b AS tb ON ta.c = tb.c +RETURNING *; + +-- Error cases +-- Non-existent columns (error case) +SELECT * (EXCLUDE (does_not_exist)) FROM users; +SELECT * (REPLACE (does_not_exist WITH 'user@example.com')) FROM users; +SELECT * (RENAME (does_not_exist AS user_email)) FROM users; + +-- Empty lists (error case) +SELECT * (EXCLUDE ()) FROM users; +SELECT * (REPLACE ()) FROM users; +SELECT * (RENAME ()) FROM users; + +-- duplicate column names (error case) +SELECT * (EXCLUDE (id, id)) FROM users; +SELECT * (REPLACE (email WITH 'user@example.com', email WITH NULL)) FROM users; +SELECT * (RENAME (email AS user_email, email AS user_name)) FROM users; +SELECT * () +FROM users; + +-- EXCLUDE + REPLACE same column (error case) +SELECT * (EXCLUDE (id) REPLACE (id WITH 999)) +FROM users; + +-- EXCLUDE + RENAME same column (error case) +SELECT * (EXCLUDE (id) RENAME (id AS user_id)) +FROM users; + +-- Cannot use star inside options list (error case) +SELECT * (EXCLUDE (users.*)) +FROM users +ORDER BY id; + +-- Incorrect use of table alias (error case) +SELECT u.* (EXCLUDE (users.email)) FROM users u; + +-- Test dropping a column +ALTER TABLE users DROP COLUMN email; +SELECT * (EXCLUDE (users.email)) FROM users; -- error, as email column no longer exists +SELECT * (EXCLUDE (name)) FROM users; + +-- Apply star options to a column from a different table in a multi-table query (error case) +SELECT +users.* (EXCLUDE (orders.amount)) +FROM users, orders; + +-- Check privilege handling with star options +CREATE USER tuser; +GRANT SELECT (name) ON TABLE users TO tuser; +SET ROLE tuser; +SELECT * (EXCLUDE (id, created_at)) FROM users; -- should succeed, as id and created_at are excluded +SELECT * (REPLACE (created_at WITH NULL)) FROM users; -- should fail, as created_at is not accessible +SELECT * (RENAME (created_at AS date_created)) FROM users; -- should fail, as created_at is not accessible +RESET ROLE; + +-- clean up +DROP TABLE orders; +DROP TABLE users; +DROP TABLE tbl_a; +DROP TABLE tbl_b; +DROP TABLE tbl_copy; +DROP TABLE s1.products; +DROP TABLE s2.products; +DROP SCHEMA s1; +DROP SCHEMA s2; +DROP user tuser; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 3bed561f7f2..26db272a181 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -2978,6 +2978,10 @@ SplitTextOutputData SplitVar StackElem StakindFlags +StarExcludeItem +StarOptions +StarRenameItem +StarReplaceItem StartBufferIOResult StartDataPtrType StartLOPtrType -- 2.43.0