1010
1111#include "postgres.h"
1212
13+ #include "miscadmin.h"
14+
1315#include "access/genam.h"
1416#include "access/htup_details.h"
1517#include "access/xact.h"
1618#include "catalog/index.h"
1719#include "catalog/pg_attrdef.h"
1820#include "catalog/pg_class.h"
21+ #include "catalog/pg_collation.h"
1922#include "catalog/pg_constraint.h"
2023#include "catalog/pg_depend.h"
2124#include "catalog/pg_type.h"
25+ #include "commands/defrem.h"
2226#include "commands/tablecmds.h"
27+ #include "executor/spi.h"
2328#include "foreign/foreign.h"
2429#include "lib/stringinfo.h"
25- #include "miscadmin.h"
26- #include "commands/defrem.h"
27- #include "executor/spi.h"
2830#include "nodes/nodeFuncs.h"
2931#include "nodes/parsenodes.h"
3032#include "parser/parse_expr.h"
3537#include "utils/lsyscache.h"
3638#include "utils/ruleutils.h"
3739#include "utils/syscache.h"
40+ #include "utils/varlena.h"
3841
3942#include "pg_version_constants.h"
4043
@@ -78,7 +81,80 @@ bool AllowUnsafeConstraints = false;
7881 * Citus walks the list in order and distributes by the first column that exists
7982 * in the table. Applies to CREATE TABLE and CREATE TABLE AS SELECT.
8083 */
81- char * DistributionColumnsGUC = "" ;
84+ char * DistributionColumns = "" ;
85+
86+ /* Pre-parsed list of distribution column names (List of String nodes).
87+ * Updated by AssignDistributionColumns whenever the GUC changes. */
88+ List * ParsedDistributionColumns = NIL ;
89+
90+
91+ /*
92+ * CheckDistributionColumns is the GUC check hook for
93+ * citus.distribution_columns. It validates the comma-separated identifier
94+ * list using SplitIdentifierString and rejects invalid input (e.g. empty
95+ * tokens from double commas) before the assign hook runs.
96+ */
97+ bool
98+ CheckDistributionColumns (char * * newval , void * * extra , GucSource source )
99+ {
100+ if (* newval == NULL || (* newval )[0 ] == '\0' )
101+ {
102+ return true;
103+ }
104+
105+ char * rawCopy = pstrdup (* newval );
106+ List * parsed = NIL ;
107+ bool valid = SplitIdentifierString (rawCopy , ',' , & parsed );
108+
109+ list_free (parsed );
110+ pfree (rawCopy );
111+
112+ return valid ;
113+ }
114+
115+
116+ /*
117+ * AssignDistributionColumns is the GUC assign hook for
118+ * citus.distribution_columns. It parses the comma-separated string into
119+ * a list of char* pointers (via SplitIdentifierString) so that callers
120+ * don't re-tokenize on every use. The check hook has already validated
121+ * the input, so SplitIdentifierString will always succeed here.
122+ */
123+ void
124+ AssignDistributionColumns (const char * newval , void * extra )
125+ {
126+ /*
127+ * SplitIdentifierString modifies the input string in-place and returns
128+ * a list of char* pointers into that string. We must keep the backing
129+ * string alive as long as ParsedDistributionColumns references it, and
130+ * free it on the next call. Everything must live in TopMemoryContext
131+ * so it survives transaction boundaries.
132+ */
133+ static char * previousRawList = NULL ;
134+
135+ list_free (ParsedDistributionColumns );
136+ ParsedDistributionColumns = NIL ;
137+
138+ if (previousRawList != NULL )
139+ {
140+ pfree (previousRawList );
141+ previousRawList = NULL ;
142+ }
143+
144+ if (!newval || newval [0 ] == '\0' )
145+ {
146+ return ;
147+ }
148+
149+ MemoryContext oldContext = MemoryContextSwitchTo (TopMemoryContext );
150+
151+ previousRawList = pstrdup (newval );
152+
153+ SplitIdentifierString (previousRawList , ',' , & ParsedDistributionColumns );
154+
155+ MemoryContextSwitchTo (oldContext );
156+ }
157+
82158
83159/* Local functions forward declarations for unsupported command checks */
84160static void PostprocessCreateTableStmtForeignKeys (CreateStmt * createStatement );
@@ -4251,7 +4327,6 @@ ErrorIfTableHasIdentityColumn(Oid relationId)
42514327}
42524328
42534329
4254-
42554330/*
42564331 * FindMatchingDistributionColumnFromTargetList walks the GUC priority list
42574332 * and returns the first column name that appears in the given Query's
@@ -4262,78 +4337,68 @@ ErrorIfTableHasIdentityColumn(Oid relationId)
42624337static char *
42634338FindMatchingDistributionColumnFromTargetList (List * targetList , List * colNames )
42644339{
4265- if (DistributionColumnsGUC == NULL || DistributionColumnsGUC [ 0 ] == '\0' )
4340+ if (ParsedDistributionColumns == NIL )
42664341 {
42674342 return NULL ;
42684343 }
42694344
4270- /* work on a copy so we don't modify the GUC value */
4271- char * rawList = pstrdup (DistributionColumnsGUC );
4272- char * token = NULL ;
4273- char * savePtr = NULL ;
4345+ /* Build list of available column names from colNames or targetList */
4346+ List * availableCols = NIL ;
4347+ bool freeAvailableCols = false;
42744348
4275- for (token = strtok_r (rawList , "," , & savePtr );
4276- token != NULL ;
4277- token = strtok_r (NULL , "," , & savePtr ))
4349+ if (colNames != NIL )
42784350 {
4279- /* trim leading whitespace */
4280- while (* token == ' ' || * token == '\t' )
4281- {
4282- token ++ ;
4283- }
4284-
4285- /* trim trailing whitespace */
4286- char * end = token + strlen (token ) - 1 ;
4287- while (end > token && (* end == ' ' || * end == '\t' ))
4351+ /* colNames is a List of String nodes; extract raw char* */
4352+ ListCell * cnCell = NULL ;
4353+ foreach (cnCell , colNames )
42884354 {
4289- * end = '\0' ;
4290- end -- ;
4355+ availableCols = lappend (availableCols , strVal (lfirst (cnCell )));
42914356 }
4292-
4293- /* skip empty tokens */
4294- if (* token == '\0' )
4295- {
4296- continue ;
4297- }
4298-
4299- /*
4300- * Walk the target list to check if any output column matches.
4301- * If colNames is provided, use those names instead.
4302- */
4303- ListCell * colCell = list_head (colNames );
4304- TargetEntry * tle = NULL ;
4305- foreach_declared_ptr (tle , targetList )
4357+ freeAvailableCols = true;
4358+ }
4359+ else
4360+ {
4361+ ListCell * tlCell = NULL ;
4362+ foreach (tlCell , targetList )
43064363 {
4307- if (tle -> resjunk )
4364+ TargetEntry * tle = lfirst_node (TargetEntry , tlCell );
4365+ if (!tle -> resjunk && tle -> resname )
43084366 {
4309- continue ;
4367+ availableCols = lappend ( availableCols , tle -> resname ) ;
43104368 }
4369+ }
4370+ freeAvailableCols = true;
4371+ }
43114372
4312- const char * colName = NULL ;
4313- if (colCell != NULL )
4314- {
4315- colName = strVal (lfirst (colCell ));
4316- colCell = lnext (colNames , colCell );
4317- }
4318- else
4319- {
4320- colName = tle -> resname ;
4321- }
4373+ /* Walk priority list, return the first match */
4374+ ListCell * tokenCell = NULL ;
4375+ foreach (tokenCell , ParsedDistributionColumns )
4376+ {
4377+ const char * candidate = (const char * ) lfirst (tokenCell );
43224378
4323- if (colName != NULL && strcmp (colName , token ) == 0 )
4379+ ListCell * colCell = NULL ;
4380+ foreach (colCell , availableCols )
4381+ {
4382+ const char * colName = (const char * ) lfirst (colCell );
4383+ if (pg_strcasecmp (candidate , colName ) == 0 )
43244384 {
4325- char * result = pstrdup (token );
4326- pfree (rawList );
4327- return result ;
4385+ if (freeAvailableCols )
4386+ {
4387+ list_free (availableCols );
4388+ }
4389+ return pstrdup (candidate );
43284390 }
43294391 }
43304392 }
43314393
4332- pfree (rawList );
4394+ if (freeAvailableCols )
4395+ {
4396+ list_free (availableCols );
4397+ }
4398+
43334399 return NULL ;
43344400}
43354401
4336-
43374402/*
43384403 * FindMatchingDistributionColumn walks the comma-separated priority list in
43394404 * citus.distribution_columns and returns a palloc'd copy of the first column
@@ -4343,50 +4408,22 @@ FindMatchingDistributionColumnFromTargetList(List *targetList, List *colNames)
43434408static char *
43444409FindMatchingDistributionColumn (Oid relationId )
43454410{
4346- if (DistributionColumnsGUC == NULL || DistributionColumnsGUC [ 0 ] == '\0' )
4411+ if (ParsedDistributionColumns == NIL )
43474412 {
43484413 return NULL ;
43494414 }
43504415
4351- /* work on a copy so we don't modify the GUC value */
4352- char * rawList = pstrdup (DistributionColumnsGUC );
4353- char * token = NULL ;
4354- char * savePtr = NULL ;
4355-
4356- for (token = strtok_r (rawList , "," , & savePtr );
4357- token != NULL ;
4358- token = strtok_r (NULL , "," , & savePtr ))
4416+ ListCell * cell = NULL ;
4417+ foreach (cell , ParsedDistributionColumns )
43594418 {
4360- /* trim leading whitespace */
4361- while (* token == ' ' || * token == '\t' )
4362- {
4363- token ++ ;
4364- }
4365-
4366- /* trim trailing whitespace */
4367- char * end = token + strlen (token ) - 1 ;
4368- while (end > token && (* end == ' ' || * end == '\t' ))
4369- {
4370- * end = '\0' ;
4371- end -- ;
4372- }
4373-
4374- /* skip empty tokens (e.g. "col1,,col2") */
4375- if (* token == '\0' )
4376- {
4377- continue ;
4378- }
4379-
4380- AttrNumber attNum = get_attnum (relationId , token );
4419+ const char * colName = (const char * ) lfirst (cell );
4420+ AttrNumber attNum = get_attnum (relationId , colName );
43814421 if (attNum != InvalidAttrNumber )
43824422 {
4383- char * result = pstrdup (token );
4384- pfree (rawList );
4385- return result ;
4423+ return pstrdup (colName );
43864424 }
43874425 }
43884426
4389- pfree (rawList );
43904427 return NULL ;
43914428}
43924429
@@ -4399,7 +4436,7 @@ FindMatchingDistributionColumn(Oid relationId)
43994436static bool
44004437ShouldAutoDistributeNewTable (Oid relationId )
44014438{
4402- if (DistributionColumnsGUC == NULL || DistributionColumnsGUC [ 0 ] == '\0' )
4439+ if (ParsedDistributionColumns == NIL )
44034440 {
44044441 return false;
44054442 }
@@ -4513,7 +4550,7 @@ TryOptimizeCTASForAutoDistribution(CreateTableAsStmt *ctasStmt,
45134550 const char * queryString )
45144551{
45154552 /* Quick bail-out if the GUC is not set */
4516- if (DistributionColumnsGUC == NULL || DistributionColumnsGUC [ 0 ] == '\0' )
4553+ if (ParsedDistributionColumns == NIL )
45174554 {
45184555 return false;
45194556 }
@@ -4608,8 +4645,8 @@ TryOptimizeCTASForAutoDistribution(CreateTableAsStmt *ctasStmt,
46084645 const char * schemaName = into -> rel -> schemaname ;
46094646 const char * tableName = into -> rel -> relname ;
46104647 const char * qualifiedName = schemaName ?
4611- quote_qualified_identifier (schemaName , tableName ) :
4612- quote_identifier (tableName );
4648+ quote_qualified_identifier (schemaName , tableName ) :
4649+ quote_identifier (tableName );
46134650
46144651 /*
46154652 * If IF NOT EXISTS is set and the table already exists, skip.
0 commit comments