Repository: citusdata/citus
Pull request #7920: Issue 7887 Enhance AddInsertSelectCasts for Identity Columns
Author: login: m3hm3t
Labels: name: cherry-pick-13.0
Created: 2025-03-07T14:45:31Z | Merged: 2025-03-10T10:54:30Z

## Enhance `AddInsertSelectCasts` for Identity Columns


This PR fixes #7887  and improves the behavior of partial inserts into **identity columns** by modifying the **`AddInsertSelectCasts`** function. Specifically, we introduce **special-case handling** for `nextval(...)` calls (represented in the parse tree as `NextValueExpr`) to ensure that if the identity column’s declared type differs from `nextval`’s default return type (`int8`), we **cast** the expression properly. This prevents mismatches like `int8` → `int4` from causing “invalid string enlargement” errors or other type-related failures.

When `INSERT ... SELECT` is processed, `AddInsertSelectCasts` reconciles each target column’s type with the corresponding SELECT expression’s type. Historically, for identity columns that rely on `nextval(...)`, we can end up with a mismatch:
- `nextval` returns **`int8`**,
- The identity column might be **`int4`**, **`bigint`**, or another integer type.

Without a correct cast, Postgres or Citus can produce plan-time or runtime errors. By **detecting** `NextValueExpr` and applying a cast to the column’s type, the final plan ensures consistent insertion without errors.

## What Changed

1. **Check for `NextValueExpr`**:  
   In `AddInsertSelectCasts`, we now have a code block:
   ```c
   if (IsA(selectEntry->expr, NextValueExpr))
   {
       Oid nextvalType = GetNextvalReturnTypeCatalog();
       ...
       // If (targetType != nextvalType), build a cast from int8 -> targetType
   }
   else
   {
       // fallback to generic mismatch logic
   }
   ```
   This short-circuits any expression that’s a `nextval(...)` call, letting us explicitly cast to the correct type.

2. **Fallback Generic Logic**:  
   If it isn’t a `NextValueExpr` (i.e. a normal column or expression mismatch), we still rely on the existing path that compares `sourceType` vs. `targetType` and calls `CastExpr(...)` if they differ.

3. **`GetNextvalReturnTypeCatalog`**:  
   We added or refined a helper function to confirm that `nextval` returns `int8`, or do a `LookupFuncName("nextval", ...)` to discover the function’s return type from `pg_proc`—making it robust if future changes happen.

## Benefits

- **Partial inserts** into identity columns no longer fail with type mismatches.  
- When `nextval` yields `int8` but the identity column is `int4` (or another type), we properly cast to the column’s type in the plan.  
- Preserves the **existing** approach for other columns—only identity calls get the specialized `NextValueExpr` logic.

## Testing

   - Extended `generatedidentity.sql` test scenario to cover partial inserts into both `GENERATED ALWAYS` and `GENERATED BY DEFAULT` identity columns, including tests for the `OVERRIDING SYSTEM VALUE` clause and partial inserts referencing foreign-key columns.
   
   

Linked issue #7887: Cannot bulk insert from one local table to another local table if one has an identity
Hello!  While implementing Citus distributed and reference tables, we encountered an issue inserting data from one local table to another when the target local table has an identity.  Repro steps below:

Citus version: Citus 12.1.6 on x86_64-pc-linux-gnu, compiled by gcc (Debian 12.2.0-14) 12.2.0, 64-bit

```
--create a test schema
create schema test;

-- add our first local table
create table test.local1(
    id text not null primary key
);

-- add our reference table
create table test.reference1(
    id int not null primary key,
    reference_col1 text not null
);

-- apply the reference table type
select create_reference_table('test.reference1');

-- add our second local table
create table test.local2(
    id int not null generated always as identity,
    local1fk text not null,
    reference1fk int not null,
    constraint loc1fk foreign key (local1fk) references test.local1(id),
    constraint reference1fk foreign key (reference1fk) references test.reference1(id),
    constraint testlocpk primary key (id)
);

-- create a distributed table
create table test.distributed1(
    id int not null generated always as identity,
    distro_key int not null,
    constraint testdistro1pk primary key (id, distro_key)
);

-- apply distribution on the distro key
select create_distributed_table('test.distributed1', 'distro_key');

-- add our first local table values
insert into test.local1(id) values ('aaaaa'), ('bbbbb'), ('ccccc');

-- add our reference data
insert into test.reference1(id, reference_col1) values (1, 'test'), (2, 'test2'), (3, 'test3');

-- add data to our distributed table
insert into test.distributed1(distro_key) values (1), (1), (1), (2), (2), (3);

-- we confirm that we have two local tables and one reference table
select
    a.table_name,
    b.citus_table_type
from information_schema.tables a
    left join citus_tables b on b.table_name::text = concat(a.table_schema::text, '.', a.table_name::text)
where a.table_schema = 'test'
	and a.table_type = 'BASE TABLE'
order by a.table_name

-- bulk insert from our first local table into our second
insert into test.local2(local1fk, reference1fk)
select id, 1 from test.local1

-- why does this fail?  invalid string enlargement request size: -4
```

At first I assumed this might be an unsupported operation, but I tried it again in the exact same scenario in which the local table has a serial PK rather than an identity, and did not receive the error.  As a result, I suspect this is a bug related to support of identities.

Successful test with serial instead:

```
--create a test schema
create schema testserial;

-- add our first local table
create table testserial.local1(
    id text not null primary key
);

-- add our reference table
create table testserial.reference1(
    id int not null primary key,
    reference_col1 text not null
);

-- apply the reference table type
select create_reference_table('testserial.reference1');

-- add our second local table
create table testserial.local2(
    id bigserial,
    local1fk text not null,
    reference1fk int not null,
    constraint loc1fk foreign key (local1fk) references testserial.local1(id),
    constraint reference1fk foreign key (reference1fk) references testserial.reference1(id),
    constraint testlocpk primary key (id)
);

-- create a distributed table
create table testserial.distributed1(
    id bigserial,
    distro_key int not null,
    constraint testdistro1pk primary key (id, distro_key)
);

-- apply distribution on the distro key
select create_distributed_table('testserial.distributed1', 'distro_key');

-- add our first local table values
insert into testserial.local1(id) values ('aaaaa'), ('bbbbb'), ('ccccc');

-- add our reference data
insert into testserial.reference1(id, reference_col1) values (1, 'test'), (2, 'test2'), (3, 'test3');

-- add data to our distributed table
insert into testserial.distributed1(distro_key) values (1), (1), (1), (2), (2), (3);

-- we confirm that we have two local tables and one reference table
select
    a.table_name,
    b.citus_table_type
from information_schema.tables a
    left join citus_tables b on b.table_name::text = concat(a.table_schema::text, '.', a.table_name::text)
where a.table_schema = 'testserial'
	and a.table_type = 'BASE TABLE'
order by a.table_name

-- bulk insert from our first local table into our second
insert into testserial.local2(local1fk, reference1fk)
select id, 1 from test.local1

-- why did this work, but an identity did not?
```
Linked issue comments: user: login: colm-mchugh
created_at: 2025-02-07T13:53:10Z
body: @nick-oehmen we need to investigate and fix the error you are hitting in your first test, but as a workaround could you omit the `reference1fk` constraint from the `CREATE TABLE test.local2` statement and add it after `test.local2` has been populated with the INSERT from `test.local1` ? 
author_association: CONTRIBUTOR
user: login: kalderos-noehmen
created_at: 2025-02-07T16:37:20Z
body: Thank you for the response @colm-mchugh!  The example I provided is a pretty simple one just to demonstrate the issue, the actual issue is occurring in a much larger DB as we're testing implementing distribution prior to pushing to the live production DB.  The bulk insert operations occur throughout the DB in stored procedures and there's no way to prevent them from running after distribution is implemented, if anything this may just delay our implementation of table distribution until this issue is fixed.  Do you happen to know a timeline for resolution?
author_association: NONE
user: login: colm-mchugh
created_at: 2025-02-07T17:14:55Z
body: _Do you happen to know a timeline for resolution?_

We will aim to fix this in Citus 13.0.1, so approx. 6-8 weeks from now. 
author_association: CONTRIBUTOR
user: login: m3hm3t
created_at: 2025-02-19T11:00:33Z
body: Analysis suggests that in Citus’s **multi-copy** logic, the row descriptor becomes mismatched for the identity column. Instead of allowing Postgres to handle the default identity value, Citus incorrectly maps the returning `SELECT` columns onto the table columns. This leads to corrupted data in the slot, resulting in the meltdown within `textsend/enlargeStringInfo()`.

**Actual Behavior**

Citus triggers a corruption in the row descriptor for the local table’s identity column. The code attempts to call `textsend `on a non-text datum (or interprets memory incorrectly).


**Technical Analysis**

- Stack Trace Excerpts show `multi_copy.c -> AppendCopyRowData -> textsend -> enlargeStringInfo(...)`.
- Debugger Logs indicate that for the identity column, the code sets up `int4send `or tries to skip it, but the actual `valueArray[]` gets mismatched with the text column.
- This mismatch typically arises in partial inserts where only 2 columns are provided for a table of 3 columns, and the identity is not explicitly listed.
- Using a `SERIAL `column instead of `IDENTITY `avoids the bug, implying that Citus’s logic for partial inserts has been well-tested for serial-like defaults but not for identity definitions.

**Why SERIAL Works but IDENTITY Fails**

- **SERIAL** is simply` INT + DEFAULT nextval(...)`. Citus historically handles partial inserts that omit `SERIAL` columns without trouble.
- **IDENTITY** (especially `GENERATED ALWAYS`) uses a different internal catalog mechanism (`attidentity`), so the Citus code path that aligns columns may not treat it as a default properly.

**Potential Fix Outline**

- In Citus’s `multi_copy.c,` ensure that identity columns omitted from the `INSERT` target list are treated the same as columns with a default.
- Confirm that **column → slot** assignment in partial inserts does not shift or misalign the rest of the columns.

References

- **Postgres** error message chain: `textsend() -> pq_sendtext() -> enlargeStringInfo()` -> “invalid string enlargement request size: -4”.
- **Citus** code paths: `CitusSendTupleToPlacements, AppendCopyRowData, AddSlotToBuffer in multi_copy.c`.



author_association: CONTRIBUTOR

Diff:
diff --git a/src/backend/distributed/planner/insert_select_planner.c b/src/backend/distributed/planner/insert_select_planner.c
index ca0c74f8fb0..ce61bd0ae51 100644
--- a/src/backend/distributed/planner/insert_select_planner.c
+++ b/src/backend/distributed/planner/insert_select_planner.c
@@ -96,6 +96,17 @@ static List * AddInsertSelectCasts(List *insertTargetList, List *selectTargetLis
 								   Oid targetRelationId);
 static Expr * CastExpr(Expr *expr, Oid sourceType, Oid targetType, Oid targetCollation,
 					   int targetTypeMod);
+static Oid GetNextvalReturnTypeCatalog(void);
+static void AppendCastedEntry(TargetEntry *insertEntry, TargetEntry *selectEntry,
+							  Oid castFromType, Oid targetType, Oid collation, int32
+							  typmod,
+							  int targetEntryIndex,
+							  List **projectedEntries, List **nonProjectedEntries);
+static void SetTargetEntryName(TargetEntry *tle, const char *format, int index);
+static void ResetTargetEntryResno(List *targetList);
+static void ProcessEntryPair(TargetEntry *insertEntry, TargetEntry *selectEntry,
+							 Form_pg_attribute attr, int targetEntryIndex,
+							 List **projectedEntries, List **nonProjectedEntries);
 
 
 /* depth of current insert/select planner. */
@@ -1617,11 +1628,11 @@ RelabelTargetEntryList(List *selectTargetList, List *insertTargetList)
 
 
 /*
- * AddInsertSelectCasts makes sure that the types in columns in the given
- * target lists have the same type as the columns of the given relation.
- * It might add casts to ensure that.
+ * AddInsertSelectCasts ensures that the columns in the given target lists
+ * have the same type as the corresponding columns of the target relation.
+ * It adds casts when necessary.
  *
- * It returns the updated selectTargetList.
+ * Returns the updated selectTargetList.
  */
 static List *
 AddInsertSelectCasts(List *insertTargetList, List *selectTargetList,
@@ -1631,9 +1642,9 @@ AddInsertSelectCasts(List *insertTargetList, List *selectTargetList,
 	List *nonProjectedEntries = NIL;
 
 	/*
-	 * ReorderInsertSelectTargetLists() makes sure that first few columns of
-	 * the SELECT query match the insert targets. It might contain additional
-	 * items for GROUP BY, etc.
+	 * ReorderInsertSelectTargetLists() ensures that the first few columns of the
+	 * SELECT query match the insert targets. It might also include additional
+	 * items (for GROUP BY, etc.), so the insertTargetList is shorter.
 	 */
 	Assert(list_length(insertTargetList) <= list_length(selectTargetList));
 
@@ -1646,71 +1657,20 @@ AddInsertSelectCasts(List *insertTargetList, List *selectTargetList,
 
 	forboth_ptr(insertEntry, insertTargetList, selectEntry, selectTargetList)
 	{
+		/*
+		 * Retrieve the target attribute corresponding to the insert entry.
+		 * The attribute is located at (resno - 1) in the tuple descriptor.
+		 */
 		Form_pg_attribute attr = TupleDescAttr(destTupleDescriptor,
 											   insertEntry->resno - 1);
 
-		Oid sourceType = exprType((Node *) selectEntry->expr);
-		Oid targetType = attr->atttypid;
-		if (sourceType != targetType)
-		{
-			/* ReorderInsertSelectTargetLists ensures we only have Vars */
-			Assert(IsA(insertEntry->expr, Var));
-
-			/* we will cast the SELECT expression, so the type changes */
-			Var *insertVar = (Var *) insertEntry->expr;
-			insertVar->vartype = targetType;
-			insertVar->vartypmod = attr->atttypmod;
-			insertVar->varcollid = attr->attcollation;
-
-			/*
-			 * We cannot modify the selectEntry in-place, because ORDER BY or
-			 * GROUP BY clauses might be pointing to it with comparison types
-			 * of the source type. So instead we keep the original one as a
-			 * non-projected entry, so GROUP BY and ORDER BY are happy, and
-			 * create a duplicated projected entry with the coerced expression.
-			 */
-			TargetEntry *coercedEntry = copyObject(selectEntry);
-			coercedEntry->expr = CastExpr((Expr *) selectEntry->expr, sourceType,
-										  targetType, attr->attcollation,
-										  attr->atttypmod);
-			coercedEntry->ressortgroupref = 0;
-
-			/*
-			 * The only requirement is that users don't use this name in ORDER BY
-			 * or GROUP BY, and it should be unique across the same query.
-			 */
-			StringInfo resnameString = makeStringInfo();
-			appendStringInfo(resnameString, "auto_coerced_by_citus_%d", targetEntryIndex);
-			coercedEntry->resname = resnameString->data;
-
-			projectedEntries = lappend(projectedEntries, coercedEntry);
-
-			if (selectEntry->ressortgroupref != 0)
-			{
-				selectEntry->resjunk = true;
-
-				/*
-				 * This entry might still end up in the SELECT output list, so
-				 * rename it to avoid ambiguity.
-				 *
-				 * See https://github.com/citusdata/citus/pull/3470.
-				 */
-				resnameString = makeStringInfo();
-				appendStringInfo(resnameString, "discarded_target_item_%d",
-								 targetEntryIndex);
-				selectEntry->resname = resnameString->data;
-
-				nonProjectedEntries = lappend(nonProjectedEntries, selectEntry);
-			}
-		}
-		else
-		{
-			projectedEntries = lappend(projectedEntries, selectEntry);
-		}
+		ProcessEntryPair(insertEntry, selectEntry, attr, targetEntryIndex,
+						 &projectedEntries, &nonProjectedEntries);
 
 		targetEntryIndex++;
 	}
 
+	/* Append any additional non-projected entries from selectTargetList */
 	for (int entryIndex = list_length(insertTargetList);
 		 entryIndex < list_length(selectTargetList);
 		 entryIndex++)
@@ -1719,18 +1679,154 @@ AddInsertSelectCasts(List *insertTargetList, List *selectTargetList,
 																	entryIndex));
 	}
 
-	/* selectEntry->resno must be the ordinal number of the entry */
+	/* Concatenate projected and non-projected entries and reset resno numbering */
 	selectTargetList = list_concat(projectedEntries, nonProjectedEntries);
+	ResetTargetEntryResno(selectTargetList);
+
+	table_close(distributedRelation, NoLock);
+
+	return selectTargetList;
+}
+
+
+/*
+ * Processes a single pair of insert and select target entries.
+ * It compares the source and target types and appends either the
+ * original select entry or a casted version to the appropriate list.
+ */
+static void
+ProcessEntryPair(TargetEntry *insertEntry, TargetEntry *selectEntry,
+				 Form_pg_attribute attr, int targetEntryIndex,
+				 List **projectedEntries, List **nonProjectedEntries)
+{
+	Oid effectiveSourceType = exprType((Node *) selectEntry->expr);
+	Oid targetType = attr->atttypid;
+
+	/*
+	 * If the select expression is a NextValueExpr, use its actual return type.
+	 *
+	 * NextValueExpr represents a call to the nextval() function, which is used to
+	 * obtain the next value from a sequence—commonly for populating auto-increment
+	 * columns. In many cases, nextval() returns an INT8 (bigint), but the actual
+	 * return type may differ depending on database configuration or custom implementations.
+	 *
+	 * Since the target column might have a different type (e.g., INT4), we need to
+	 * obtain the real return type of nextval() to ensure that any type coercion is applied
+	 * correctly. This is done by calling GetNextvalReturnTypeCatalog(), which looks up the
+	 * function in the catalog and returns its return type. The effectiveSourceType is then
+	 * set to this value, ensuring that subsequent comparisons and casts use the correct type.
+	 */
+	if (IsA(selectEntry->expr, NextValueExpr))
+	{
+		effectiveSourceType = GetNextvalReturnTypeCatalog();
+	}
+
+	if (effectiveSourceType != targetType)
+	{
+		AppendCastedEntry(insertEntry, selectEntry,
+						  effectiveSourceType, targetType,
+						  attr->attcollation, attr->atttypmod,
+						  targetEntryIndex,
+						  projectedEntries, nonProjectedEntries);
+	}
+	else
+	{
+		/* Types match, no cast needed */
+		*projectedEntries = lappend(*projectedEntries, selectEntry);
+	}
+}
+
+
+/*
+ * Resets the resno field for each target entry in the list so that
+ * they are numbered sequentially.
+ */
+static void
+ResetTargetEntryResno(List *targetList)
+{
 	int entryResNo = 1;
-	TargetEntry *selectTargetEntry = NULL;
-	foreach_declared_ptr(selectTargetEntry, selectTargetList)
+	ListCell *lc = NULL;
+	foreach(lc, targetList)
 	{
-		selectTargetEntry->resno = entryResNo++;
+		TargetEntry *tle = (TargetEntry *) lfirst(lc);
+		tle->resno = entryResNo++;
 	}
+}
 
-	table_close(distributedRelation, NoLock);
 
-	return selectTargetList;
+/*
+ * Looks up the nextval(regclass) function in pg_proc, returning its actual
+ * rettype. In a standard build, that will be INT8OID, but this is more robust.
+ */
+static Oid
+GetNextvalReturnTypeCatalog(void)
+{
+	Oid argTypes[1] = { REGCLASSOID };
+	List *nameList = list_make1(makeString("nextval"));
+
+	/* Look up the nextval(regclass) function */
+	Oid nextvalFuncOid = LookupFuncName(nameList, 1, argTypes, false);
+	if (!OidIsValid(nextvalFuncOid))
+	{
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_FUNCTION),
+				 errmsg("could not find function nextval(regclass)")));
+	}
+
+	/* Retrieve and validate the return type of the nextval function */
+	Oid nextvalReturnType = get_func_rettype(nextvalFuncOid);
+	if (!OidIsValid(nextvalReturnType))
+	{
+		elog(ERROR, "could not determine return type of nextval(regclass)");
+	}
+
+	return nextvalReturnType;
+}
+
+
+/**
+ * Modifies the given insert entry to match the target column's type and typmod,
+ * then creates and appends a new target entry containing a casted expression
+ * to the projected list. If the original select entry is used by ORDER BY or GROUP BY,
+ * it is marked as junk to avoid ambiguity.
+ */
+static void
+AppendCastedEntry(TargetEntry *insertEntry, TargetEntry *selectEntry,
+				  Oid castFromType, Oid targetType, Oid collation, int32 typmod,
+				  int targetEntryIndex,
+				  List **projectedEntries, List **nonProjectedEntries)
+{
+	/* Update the insert entry's Var to match the target column's type, typmod, and collation */
+	Assert(IsA(insertEntry->expr, Var));
+	{
+		Var *insertVar = (Var *) insertEntry->expr;
+		insertVar->vartype = targetType;
+		insertVar->vartypmod = typmod;
+		insertVar->varcollid = collation;
+	}
+
+	/* Create a new TargetEntry with the casted expression */
+	TargetEntry *coercedEntry = copyObject(selectEntry);
+	coercedEntry->expr = CastExpr((Expr *) selectEntry->expr,
+								  castFromType,
+								  targetType,
+								  collation,
+								  typmod);
+	coercedEntry->ressortgroupref = 0;
+
+	/* Assign a unique name to the coerced entry */
+	SetTargetEntryName(coercedEntry, "auto_coerced_by_citus_%d", targetEntryIndex);
+	*projectedEntries = lappend(*projectedEntries, coercedEntry);
+
+	/* If the original select entry is referenced in ORDER BY or GROUP BY,
+	 * mark it as junk and rename it to avoid ambiguity.
+	 */
+	if (selectEntry->ressortgroupref != 0)
+	{
+		selectEntry->resjunk = true;
+		SetTargetEntryName(selectEntry, "discarded_target_item_%d", targetEntryIndex);
+		*nonProjectedEntries = lappend(*nonProjectedEntries, selectEntry);
+	}
 }
 
 
@@ -1815,6 +1911,16 @@ CastExpr(Expr *expr, Oid sourceType, Oid targetType, Oid targetCollation,
 }
 
 
+/* Helper function to set the target entry name using a formatted string */
+static void
+SetTargetEntryName(TargetEntry *tle, const char *format, int index)
+{
+	StringInfo resnameString = makeStringInfo();
+	appendStringInfo(resnameString, format, index);
+	tle->resname = resnameString->data;
+}
+
+
 /* PlanningInsertSelect returns true if we are planning an INSERT ...SELECT query */
 bool
 PlanningInsertSelect(void)
diff --git a/src/test/regress/expected/generated_identity.out b/src/test/regress/expected/generated_identity.out
index 8fe7a0dc64d..b1102b7814f 100644
--- a/src/test/regress/expected/generated_identity.out
+++ b/src/test/regress/expected/generated_identity.out
@@ -560,5 +560,116 @@ SELECT * FROM test;
  1 | 2 | 2
 (2 rows)
 
+-- Test for issue #7887 Fix insert select planner to exclude identity columns from target list on partial inserts
+-- https://github.com/citusdata/citus/pull/7911
+CREATE TABLE local1 (
+    id text not null primary key
+);
+CREATE TABLE reference1 (
+    id int not null primary key,
+    reference_col1 text not null
+);
+SELECT create_reference_table('reference1');
+ create_reference_table
+---------------------------------------------------------------------
+
+(1 row)
+
+CREATE TABLE local2 (
+    id int not null generated always as identity,
+    local1fk text not null,
+    reference1fk int not null,
+    constraint loc1fk foreign key (local1fk) references local1(id),
+    constraint reference1fk foreign key (reference1fk) references reference1(id),
+    constraint testlocpk primary key (id)
+);
+INSERT INTO local1(id) VALUES ('aaaaa'), ('bbbbb'), ('ccccc');
+INSERT INTO reference1(id, reference_col1) VALUES (1, 'test'), (2, 'test2'), (3, 'test3');
+--
+-- Partial insert: omit the identity column
+-- This triggers the known bug in older code paths if not fixed.
+--
+INSERT INTO local2(local1fk, reference1fk)
+    SELECT id, 1
+    FROM local1;
+-- Check inserted rows in local2
+SELECT * FROM local2;
+ id | local1fk | reference1fk
+---------------------------------------------------------------------
+  1 | aaaaa    |            1
+  2 | bbbbb    |            1
+  3 | ccccc    |            1
+(3 rows)
+
+-- We do a "INSERT INTO local2(id, local1fk, reference1fk) SELECT 9999, id, 2" which
+-- should fail under normal PG rules if no OVERRIDING clause is used.
+INSERT INTO local2(id, local1fk, reference1fk)
+    SELECT 9999, id, 2 FROM local1 LIMIT 1;
+ERROR:  cannot insert a non-DEFAULT value into column "id"
+DETAIL:  Column "id" is an identity column defined as GENERATED ALWAYS.
+HINT:  Use OVERRIDING SYSTEM VALUE to override.
+-- Using OVERRIDING SYSTEM VALUE to override ALWAYS identity
+INSERT INTO local2(id, local1fk, reference1fk)
+    OVERRIDING SYSTEM VALUE
+    SELECT 9999, id, 2 FROM local1 LIMIT 1;
+-- Create a second table with BY DEFAULT identity to test different identity mode
+CREATE TABLE local2_bydefault (
+    id int NOT NULL GENERATED BY DEFAULT AS IDENTITY,
+    local1fk text NOT NULL,
+    reference1fk int NOT NULL,
+    CONSTRAINT loc1fk_bd FOREIGN KEY (local1fk) REFERENCES local1(id),
+    CONSTRAINT reference1fk_bd FOREIGN KEY (reference1fk) REFERENCES reference1(id),
+    CONSTRAINT testlocpk_bd PRIMARY KEY (id)
+);
+INSERT INTO local1(id) VALUES ('xxxxx'), ('yyyyy'), ('ddddd'), ('zzzzz');
+INSERT INTO local2_bydefault(local1fk, reference1fk)
+    SELECT 'xxxxx', 1;
+-- Show inserted row in local2_bydefault
+SELECT * FROM local2_bydefault;
+ id | local1fk | reference1fk
+---------------------------------------------------------------------
+  1 | xxxxx    |            1
+(1 row)
+
+--
+-- Overriding a BY DEFAULT identity with user value
+-- (which is allowed even without OVERRIDING clause).
+--
+-- Provide explicit id for BY DEFAULT identity => no special OVERRIDING needed
+INSERT INTO local2_bydefault(id, local1fk, reference1fk)
+    VALUES (5000, 'yyyyy', 2);
+-- Show rows (we expect id=5000 and one with auto-generated ID)
+SELECT * FROM local2_bydefault ORDER BY id;
+  id  | local1fk | reference1fk
+---------------------------------------------------------------------
+    1 | xxxxx    |            1
+ 5000 | yyyyy    |            2
+(2 rows)
+
+-- Insert referencing reference1fk=3 => partial insert on both tables
+INSERT INTO local2(local1fk, reference1fk)
+    VALUES ('ddddd', 3);
+INSERT INTO local2_bydefault(local1fk, reference1fk)
+    SELECT 'zzzzz', 3;
+-- Show final state of local2 and local2_bydefault
+SELECT 'local2' as table_name, * FROM local2
+UNION ALL
+SELECT 'local2_bydefault', * FROM local2_bydefault
+ORDER BY table_name, id;
+    table_name    |  id  | local1fk | reference1fk
+---------------------------------------------------------------------
+ local2           |    1 | aaaaa    |            1
+ local2           |    2 | bbbbb    |            1
+ local2           |    3 | ccccc    |            1
+ local2           |    4 | ddddd    |            3
+ local2           | 9999 | aaaaa    |            2
+ local2_bydefault |    1 | xxxxx    |            1
+ local2_bydefault |    2 | zzzzz    |            3
+ local2_bydefault | 5000 | yyyyy    |            2
+(8 rows)
+
+-- End of test for issue #7887
+-- Cleanup
+SET client_min_messages TO WARNING;
 DROP SCHEMA generated_identities CASCADE;
 DROP USER identity_test_user;
diff --git a/src/test/regress/sql/generated_identity.sql b/src/test/regress/sql/generated_identity.sql
index df967ddd0fa..5de9ea692f9 100644
--- a/src/test/regress/sql/generated_identity.sql
+++ b/src/test/regress/sql/generated_identity.sql
@@ -279,5 +279,99 @@ INSERT INTO test VALUES (1,2);
 INSERT INTO test SELECT x, y FROM test WHERE x = 1;
 SELECT * FROM test;
 
+
+-- Test for issue #7887 Fix insert select planner to exclude identity columns from target list on partial inserts
+-- https://github.com/citusdata/citus/pull/7911
+CREATE TABLE local1 (
+    id text not null primary key
+);
+
+CREATE TABLE reference1 (
+    id int not null primary key,
+    reference_col1 text not null
+);
+SELECT create_reference_table('reference1');
+
+CREATE TABLE local2 (
+    id int not null generated always as identity,
+    local1fk text not null,
+    reference1fk int not null,
+    constraint loc1fk foreign key (local1fk) references local1(id),
+    constraint reference1fk foreign key (reference1fk) references reference1(id),
+    constraint testlocpk primary key (id)
+);
+
+INSERT INTO local1(id) VALUES ('aaaaa'), ('bbbbb'), ('ccccc');
+INSERT INTO reference1(id, reference_col1) VALUES (1, 'test'), (2, 'test2'), (3, 'test3');
+
+--
+-- Partial insert: omit the identity column
+-- This triggers the known bug in older code paths if not fixed.
+--
+INSERT INTO local2(local1fk, reference1fk)
+    SELECT id, 1
+    FROM local1;
+
+-- Check inserted rows in local2
+SELECT * FROM local2;
+
+
+-- We do a "INSERT INTO local2(id, local1fk, reference1fk) SELECT 9999, id, 2" which
+-- should fail under normal PG rules if no OVERRIDING clause is used.
+
+INSERT INTO local2(id, local1fk, reference1fk)
+    SELECT 9999, id, 2 FROM local1 LIMIT 1;
+
+-- Using OVERRIDING SYSTEM VALUE to override ALWAYS identity
+INSERT INTO local2(id, local1fk, reference1fk)
+    OVERRIDING SYSTEM VALUE
+    SELECT 9999, id, 2 FROM local1 LIMIT 1;
+
+-- Create a second table with BY DEFAULT identity to test different identity mode
+CREATE TABLE local2_bydefault (
+    id int NOT NULL GENERATED BY DEFAULT AS IDENTITY,
+    local1fk text NOT NULL,
+    reference1fk int NOT NULL,
+    CONSTRAINT loc1fk_bd FOREIGN KEY (local1fk) REFERENCES local1(id),
+    CONSTRAINT reference1fk_bd FOREIGN KEY (reference1fk) REFERENCES reference1(id),
+    CONSTRAINT testlocpk_bd PRIMARY KEY (id)
+);
+
+INSERT INTO local1(id) VALUES ('xxxxx'), ('yyyyy'), ('ddddd'), ('zzzzz');
+
+INSERT INTO local2_bydefault(local1fk, reference1fk)
+    SELECT 'xxxxx', 1;
+
+-- Show inserted row in local2_bydefault
+SELECT * FROM local2_bydefault;
+
+--
+-- Overriding a BY DEFAULT identity with user value
+-- (which is allowed even without OVERRIDING clause).
+--
+-- Provide explicit id for BY DEFAULT identity => no special OVERRIDING needed
+INSERT INTO local2_bydefault(id, local1fk, reference1fk)
+    VALUES (5000, 'yyyyy', 2);
+
+-- Show rows (we expect id=5000 and one with auto-generated ID)
+SELECT * FROM local2_bydefault ORDER BY id;
+
+-- Insert referencing reference1fk=3 => partial insert on both tables
+INSERT INTO local2(local1fk, reference1fk)
+    VALUES ('ddddd', 3);
+
+INSERT INTO local2_bydefault(local1fk, reference1fk)
+    SELECT 'zzzzz', 3;
+
+-- Show final state of local2 and local2_bydefault
+SELECT 'local2' as table_name, * FROM local2
+UNION ALL
+SELECT 'local2_bydefault', * FROM local2_bydefault
+ORDER BY table_name, id;
+
+-- End of test for issue #7887
+
+-- Cleanup
+SET client_min_messages TO WARNING;
 DROP SCHEMA generated_identities CASCADE;
 DROP USER identity_test_user;


Reviews:
user: login: colm-mchugh
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T17:59:59Z
user: login: colm-mchugh
body: LGTM, thanks for accommodating review comments.  Can we label with `cherry-pick-13.0` ? 
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T08:36:11Z
user: login: m3hm3t
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T08:47:11Z
user: login: onurctirtir
body: Let's please add such a line into PR description so that this fix appears in changelog since it's a user-facing change.

>DESCRIPTION: Fixes a bug ... /

You can check CHANGELOG.md to see the example formats that we use in release changelog.

Also, let's close the other PR since we don't want to merge it anymore?

Finally, having such a line in PR description lets Github automaticall close the issue once this PR is merged:

> Fixes #7887.
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-10T08:56:38Z
user: login: m3hm3t
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T10:12:45Z

Review comments:
diff_hunk: @@ -1617,11 +1628,87 @@ RelabelTargetEntryList(List *selectTargetList, List *insertTargetList)
 
 
 /*
- * AddInsertSelectCasts makes sure that the types in columns in the given
- * target lists have the same type as the columns of the given relation.
- * It might add casts to ensure that.
+ * Processes a single pair of insert and select target entries.
+ * It compares the source and target types and appends either the
+ * original select entry or a casted version to the appropriate list.
+ */
+static void
+process_entry_pair(TargetEntry *insertEntry, TargetEntry *selectEntry,
+				   Form_pg_attribute attr, int targetEntryIndex,
+				   List **projectedEntries, List **nonProjectedEntries)
+{
+	Oid sourceType = exprType((Node *) selectEntry->expr);
+	Oid targetType = attr->atttypid;
+
+	/*
+	* If the select expression is a NextValueExpr, use its actual return type.
+	*
+	* NextValueExpr represents a call to the nextval() function, which is used to
+	* obtain the next value from a sequence—commonly for populating auto-increment
+	* columns. In many cases, nextval() returns an INT8 (bigint), but the actual
+	* return type may differ depending on database configuration or custom implementations.
+	*
+	* Since the target column might have a different type (e.g., INT4), we need to
+	* obtain the real return type of nextval() to ensure that any type coercion is applied
+	* correctly. This is done by calling GetNextvalReturnTypeCatalog(), which looks up the
+	* function in the catalog and returns its return type. The effectiveCastFromType is then
+	* set to this value, ensuring that subsequent comparisons and casts use the correct type.
+	*/
+	if (IsA(selectEntry->expr, NextValueExpr))
+	{
+		Oid nextvalType = GetNextvalReturnTypeCatalog();
+		if (targetType != nextvalType)
+		{
+			append_casted_entry(insertEntry, selectEntry,
+								nextvalType, targetType,
+								attr->attcollation, attr->atttypmod,
+								targetEntryIndex,
+								projectedEntries, nonProjectedEntries);
+		}
+		else
+		{
+			*projectedEntries = lappend(*projectedEntries, selectEntry);
+		}
+	}
+	else if (sourceType != targetType)
+	{
+		append_casted_entry(insertEntry, selectEntry,
+							sourceType, targetType,
+							attr->attcollation, attr->atttypmod,
+							targetEntryIndex,
+							projectedEntries, nonProjectedEntries);
+	}
+	else
+	{
+		/* Types match, no cast needed */
+		*projectedEntries = lappend(*projectedEntries, selectEntry);
+	}
path: src/backend/distributed/planner/insert_select_planner.c
user: login: colm-mchugh
body: Can we change the if statement here so that it leads with `if (source != targetType)` instead of the check for NextValueExpr ? Given that it is the more likely branch to be taken in practice. So something like:
```
if (sourceType != targetType)
{
    /* deal with cast */
}
else
{
   if (IsA(selectEntry->expr, NextValueExpr))
   {
     /* Deal with NextValueExpr, apply cast if needed */
   }
   
   /* no cast needed */ 
   *projectedEntries = lappend(*projectedEntries, selectEntry);
}
```
Looks like a nice refactoring in general, but this is the one suggestion I have.
created_at: 2025-03-07T17:59:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1617,11 +1628,87 @@ RelabelTargetEntryList(List *selectTargetList, List *insertTargetList)
 
 
 /*
- * AddInsertSelectCasts makes sure that the types in columns in the given
- * target lists have the same type as the columns of the given relation.
- * It might add casts to ensure that.
+ * Processes a single pair of insert and select target entries.
+ * It compares the source and target types and appends either the
+ * original select entry or a casted version to the appropriate list.
+ */
+static void
+process_entry_pair(TargetEntry *insertEntry, TargetEntry *selectEntry,
+				   Form_pg_attribute attr, int targetEntryIndex,
+				   List **projectedEntries, List **nonProjectedEntries)
+{
+	Oid sourceType = exprType((Node *) selectEntry->expr);
+	Oid targetType = attr->atttypid;
+
+	/*
+	* If the select expression is a NextValueExpr, use its actual return type.
+	*
+	* NextValueExpr represents a call to the nextval() function, which is used to
+	* obtain the next value from a sequence—commonly for populating auto-increment
+	* columns. In many cases, nextval() returns an INT8 (bigint), but the actual
+	* return type may differ depending on database configuration or custom implementations.
+	*
+	* Since the target column might have a different type (e.g., INT4), we need to
+	* obtain the real return type of nextval() to ensure that any type coercion is applied
+	* correctly. This is done by calling GetNextvalReturnTypeCatalog(), which looks up the
+	* function in the catalog and returns its return type. The effectiveCastFromType is then
+	* set to this value, ensuring that subsequent comparisons and casts use the correct type.
+	*/
+	if (IsA(selectEntry->expr, NextValueExpr))
+	{
+		Oid nextvalType = GetNextvalReturnTypeCatalog();
+		if (targetType != nextvalType)
+		{
+			append_casted_entry(insertEntry, selectEntry,
+								nextvalType, targetType,
+								attr->attcollation, attr->atttypmod,
+								targetEntryIndex,
+								projectedEntries, nonProjectedEntries);
+		}
+		else
+		{
+			*projectedEntries = lappend(*projectedEntries, selectEntry);
+		}
+	}
+	else if (sourceType != targetType)
+	{
+		append_casted_entry(insertEntry, selectEntry,
+							sourceType, targetType,
+							attr->attcollation, attr->atttypmod,
+							targetEntryIndex,
+							projectedEntries, nonProjectedEntries);
+	}
+	else
+	{
+		/* Types match, no cast needed */
+		*projectedEntries = lappend(*projectedEntries, selectEntry);
+	}
path: src/backend/distributed/planner/insert_select_planner.c
user: login: m3hm3t
body: I attempted to restructure it without introducing complexity.
created_at: 2025-03-10T08:47:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -96,6 +96,17 @@ static List * AddInsertSelectCasts(List *insertTargetList, List *selectTargetLis
 								   Oid targetRelationId);
 static Expr * CastExpr(Expr *expr, Oid sourceType, Oid targetType, Oid targetCollation,
 					   int targetTypeMod);
+static Oid GetNextvalReturnTypeCatalog(void);
+static void append_casted_entry(TargetEntry *insertEntry, TargetEntry *selectEntry,
+								Oid castFromType, Oid targetType, Oid collation, int32
+								typmod,
+								int targetEntryIndex,
+								List **projectedEntries, List **nonProjectedEntries);
+static void set_target_entry_name(TargetEntry *tle, const char *format, int index);
+static void reset_target_entry_resno(List *targetList);
+static void process_entry_pair(TargetEntry *insertEntry, TargetEntry *selectEntry,
+							   Form_pg_attribute attr, int targetEntryIndex,
+							   List **projectedEntries, List **nonProjectedEntries);
path: src/backend/distributed/planner/insert_select_planner.c
user: login: onurctirtir
body: As in `GetNextvalReturnTypeCatalog()`, we prefer CamelCase in code-base over snake-case, except the functions that we copy from Postgres, so it would be better if we rename these functions before merging.
created_at: 2025-03-10T08:54:09Z
author_association: MEMBER
diff_hunk: @@ -96,6 +96,17 @@ static List * AddInsertSelectCasts(List *insertTargetList, List *selectTargetLis
 								   Oid targetRelationId);
 static Expr * CastExpr(Expr *expr, Oid sourceType, Oid targetType, Oid targetCollation,
 					   int targetTypeMod);
+static Oid GetNextvalReturnTypeCatalog(void);
+static void append_casted_entry(TargetEntry *insertEntry, TargetEntry *selectEntry,
+								Oid castFromType, Oid targetType, Oid collation, int32
+								typmod,
+								int targetEntryIndex,
+								List **projectedEntries, List **nonProjectedEntries);
+static void set_target_entry_name(TargetEntry *tle, const char *format, int index);
+static void reset_target_entry_resno(List *targetList);
+static void process_entry_pair(TargetEntry *insertEntry, TargetEntry *selectEntry,
+							   Form_pg_attribute attr, int targetEntryIndex,
+							   List **projectedEntries, List **nonProjectedEntries);
path: src/backend/distributed/planner/insert_select_planner.c
user: login: m3hm3t
body: converted
created_at: 2025-03-10T10:12:45Z
author_association: CONTRIBUTOR

PR comments:
user: login: codecov[bot]
created_at: 2025-03-07T14:48:52Z
body: ## [Codecov](https://app.codecov.io/gh/citusdata/citus/pull/7920?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=citusdata) Report
Attention: Patch coverage is `91.30435%` with `4 lines` in your changes missing coverage. Please review.
> Project coverage is 89.46%. Comparing base [(`c55bc8c`)](https://app.codecov.io/gh/citusdata/citus/commit/c55bc8c669071b73af69e1136e183b640d84b23b?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=citusdata) to head [(`1ee6927`)](https://app.codecov.io/gh/citusdata/citus/commit/1ee69271682299825cd772aed9f6da8e267b4244?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=citusdata).
> Report is 26 commits behind head on release-13.0.

<details><summary>Additional details and impacted files</summary>


```diff
@@               Coverage Diff                @@
##           release-13.0    #7920      +/-   ##
================================================
- Coverage         89.48%   89.46%   -0.03%     
================================================
  Files               276      276              
  Lines             60063    60052      -11     
  Branches           7524     7525       +1     
================================================
- Hits              53747    53724      -23     
- Misses             4166     4180      +14     
+ Partials           2150     2148       -2     
```

</details>
<details><summary>🚀 New features to boost your workflow: </summary>

- ❄ [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE
user: login: c2main
created_at: 2025-03-07T18:43:24Z
body: I wanted to review as I was working on similar topic, but the code move make it hard to use diff. Would it be possible to move the code then have the fix or the reverse but not mixing both ? 
author_association: CONTRIBUTOR
user: login: c2main
created_at: 2025-03-07T19:33:07Z
body: > I wanted to review as I was working on similar topic, but the code move make it hard to use diff. Would it be possible to move the code then have the fix or the reverse but not mixing both ?

I think you should be just replacing this:

```
+   :targetList (
+      {TARGETENTRY 
+      :expr 
+         {NEXTVALUEEXPR 
+         :seqid 17616 
+         :typeId 23
+         }
+      :resno 1 
+      :resname id 
+      :ressortgroupref 0 
+      :resorigtbl 0 
+      :resorigcol 0 
+      :resjunk false
+      }
```
with:
```
+   :targetList (
+      {TARGETENTRY 
+      :expr 
+         {FUNCEXPR 
+         :funcid 480 
+         :funcresulttype 23 
+         :funcretset false 
+         :funcvariadic false 
+         :funcformat 2 
+         :funccollid 0 
+         :inputcollid 0 
+         :args (
+            {FUNCEXPR 
+            :funcid 1574 
+            :funcresulttype 20 
+            :funcretset false 
+            :funcvariadic false 
+            :funcformat 0 
+            :funccollid 0 
+            :inputcollid 0 
+            :args (
+               {CONST 
+               :consttype 2205 
+               :consttypmod -1 
+               :constcollid 0 
+               :constlen 4 
+               :constbyval true 
+               :constisnull false 
+               :location -1 
+               :constvalue 4 [ -11 68 0 0 0 0 0 0 ]
+               }
+            )
+            :location -1
+            }
+         )
+         :location -1
+         }
+      :resno 1 
+      :resname id 
+      :ressortgroupref 0 
+      :resorigtbl 0 
+      :resorigcol 0 
+      :resjunk false
+      }
```

or similar... i.e. replace node with a `makeFuncExpr` or the like... then the citus executor will handle manage the call to the relevant function.

author_association: CONTRIBUTOR
user: login: c2main
created_at: 2025-03-07T19:33:57Z
body: (serial/nextval are replaced by parser, not identity columns, IIRCC)
author_association: CONTRIBUTOR
user: login: colm-mchugh
created_at: 2025-03-08T12:26:14Z
body: > > I wanted to review as I was working on similar topic, but the code move make it hard to use diff. Would it be possible to move the code then have the fix or the reverse but not mixing both ?
> 
> I think you should be just replacing this:
> 
> ```
> +   :targetList (
> +      {TARGETENTRY 
> +      :expr 
> +         {NEXTVALUEEXPR 
> +         :seqid 17616 
> +         :typeId 23
> +         }
> +      :resno 1 
> +      :resname id 
> +      :ressortgroupref 0 
> +      :resorigtbl 0 
> +      :resorigcol 0 
> +      :resjunk false
> +      }
> ```
> 
> with:
> 
> ```
> +   :targetList (
> +      {TARGETENTRY 
> +      :expr 
> +         {FUNCEXPR 
> +         :funcid 480 
> +         :funcresulttype 23 
> +         :funcretset false 
> +         :funcvariadic false 
> +         :funcformat 2 
> +         :funccollid 0 
> +         :inputcollid 0 
> +         :args (
> +            {FUNCEXPR 
> +            :funcid 1574 
> +            :funcresulttype 20 
> +            :funcretset false 
> +            :funcvariadic false 
> +            :funcformat 0 
> +            :funccollid 0 
> +            :inputcollid 0 
> +            :args (
> +               {CONST 
> +               :consttype 2205 
> +               :consttypmod -1 
> +               :constcollid 0 
> +               :constlen 4 
> +               :constbyval true 
> +               :constisnull false 
> +               :location -1 
> +               :constvalue 4 [ -11 68 0 0 0 0 0 0 ]
> +               }
> +            )
> +            :location -1
> +            }
> +         )
> +         :location -1
> +         }
> +      :resno 1 
> +      :resname id 
> +      :ressortgroupref 0 
> +      :resorigtbl 0 
> +      :resorigcol 0 
> +      :resjunk false
> +      }
> ```
> 
> or similar... i.e. replace node with a `makeFuncExpr` or the like... then the citus executor will handle manage the call to the relevant function.

This approach was considered in #7911 but we decided instead to favor the insert-a-cast approach here, for the following reasons:
- The Postgres executor handles `NextValueExpr` nodes, which are used only for identity columns. (see `execExpr.c`)
- Postgres ruleutils converts a `NextValueExpr` node to a `FuncExpr` for nextval() 
- The problem is not present if the column is `serial` - in that case, the expression given to the Citus planner hook is a `FuncExpr` for nextval()  casted to the type of the target column if necessary.

So we reckon that emulating the Postgres processing for `serial` columns is the most desirable way to go.
author_association: CONTRIBUTOR
user: login: colm-mchugh
created_at: 2025-03-08T12:31:06Z
body: > I wanted to review as I was working on similar topic, but the code move make it hard to use diff. Would it be possible to move the code then have the fix or the reverse but not mixing both ?

This was motivated by the two cases - (1) different target and source types, (2) source is a generated identity - both requiring a cast, so refactoring the code made sense. The flow is the same, but now there's a function to apply the cast and an extra branch for handling `NextValueExpr`  
author_association: CONTRIBUTOR
user: login: m3hm3t
created_at: 2025-03-10T07:26:45Z
body: > I wanted to review as I was working on similar topic, but the code move make it hard to use diff. Would it be possible to move the code then have the fix or the reverse but not mixing both ?

@c2main 
I apologize for mixing the fix with the refactoring. The fix introduced a lot of duplicate code, so I had to clean it up with some refactoring.
author_association: CONTRIBUTOR
user: login: m3hm3t
created_at: 2025-03-10T07:46:19Z
body: > > I wanted to review as I was working on similar topic, but the code move make it hard to use diff. Would it be possible to move the code then have the fix or the reverse but not mixing both ?
> 
> I think you should be just replacing this:
> 


```
selectEntry:
  Address: 0x55eadc13d898

xpr:
  Type: T_TargetEntry
  Expression:
    Address: 0x55eadc13d608
    Type: T_NextValueExpr

resno: 1
resname:
  Address: 0x55eadc13d8e8
  Value: "id"
  First Character: 'i' (ASCII: 105)

ressortgroupref: 0
resorigtbl: 0
resorigcol: 0
resjunk: false
```
=>

```
coercedEntry:
  Address: 0x55eadc13daf8

xpr:
  Type: T_TargetEntry
  Expression:
    Address: 0x55eadc13db80
    Type: T_FuncExpr

resno: 1
resname:
  Address: 0x55eadc13dc50
  Value: "auto_coerced_by_citus_0"
  First Character: 'a' (ASCII: 97)

ressortgroupref: 0
resorigtbl: 0
resorigcol: 0
resjunk: false
```


As @colm-mchugh mentioned earlier, the type is still being replaced in the same way.
author_association: CONTRIBUTOR