3838#include "pg_lake/extensions/postgis.h"
3939#include "pg_lake/pgduck/numeric.h"
4040#include "pg_lake/pgduck/serialize.h"
41+ #include "pg_lake/pgduck/struct_conversion.h"
4142#include "pg_lake/util/numeric.h"
4243#include "executor/executor.h"
4344#include "mb/pg_wchar.h"
@@ -106,11 +107,12 @@ typedef struct CopyToStateData
106107 FmgrInfo * out_functions ; /* lookup info for output functions */
107108
108109 /*
109- * Per-column TupleDesc cache for composite-type columns. NULL means the
110- * column is not composite (or is an anonymous RECORD column). A non-NULL
111- * value is a pinned TupleDesc looked up once during StartCopyTo.
110+ * Oid -> TupleDesc hash table for composite-type columns. Pre-populated
111+ * during StartCopyTo with the top-level composite column types; lazily
112+ * extended with nested composite types encountered during serialization.
113+ * Each entry holds a pinned TupleDesc released in EndCopy.
112114 */
113- TupleDesc * composite_tupdescs ;
115+ HTAB * tupdesc_cache ;
114116 MemoryContext rowcontext ; /* per-row evaluation context */
115117 uint64 bytes_processed ; /* number of bytes processed so far */
116118
@@ -297,18 +299,15 @@ EndCopy(CopyToState cstate)
297299 }
298300 }
299301
300- /* Release pinned TupleDescs from the composite type cache */
302+ /* Release all pinned TupleDescs from the cache */
301303 {
302- ListCell * cur ;
304+ HASH_SEQ_STATUS seq ;
305+ TupleDescCacheEntry * entry ;
303306
304- foreach (cur , cstate -> attnumlist )
305- {
306- int attnum = lfirst_int (cur );
307- TupleDesc cached = cstate -> composite_tupdescs [attnum - 1 ];
308-
309- if (cached != NULL )
310- ReleaseTupleDesc (cached );
311- }
307+ hash_seq_init (& seq , cstate -> tupdesc_cache );
308+ while ((entry = (TupleDescCacheEntry * ) hash_seq_search (& seq )) != NULL )
309+ ReleaseTupleDesc (entry -> tupdesc );
310+ hash_destroy (cstate -> tupdesc_cache );
312311 }
313312
314313 MemoryContextDelete (cstate -> rowcontext );
@@ -477,7 +476,19 @@ StartCopyTo(CopyToState cstate, TupleDesc tupDesc)
477476
478477 /* Get info about the columns we need to process. */
479478 cstate -> out_functions = (FmgrInfo * ) palloc (num_phys_attrs * sizeof (FmgrInfo ));
480- cstate -> composite_tupdescs = (TupleDesc * ) palloc0 (num_phys_attrs * sizeof (TupleDesc ));
479+
480+ /* Create the Oid -> TupleDesc cache for composite-type columns. */
481+ {
482+ HASHCTL hctl ;
483+
484+ memset (& hctl , 0 , sizeof (hctl ));
485+ hctl .keysize = sizeof (Oid );
486+ hctl .entrysize = sizeof (TupleDescCacheEntry );
487+ hctl .hcxt = cstate -> copycontext ;
488+ cstate -> tupdesc_cache = hash_create ("TupleDesc cache" , 16 , & hctl ,
489+ HASH_ELEM | HASH_BLOBS | HASH_CONTEXT );
490+ }
491+
481492 foreach (cur , cstate -> attnumlist )
482493 {
483494 int attnum = lfirst_int (cur );
@@ -496,9 +507,9 @@ StartCopyTo(CopyToState cstate, TupleDesc tupDesc)
496507 fmgr_info (out_func_oid , & cstate -> out_functions [attnum - 1 ]);
497508
498509 /*
499- * Pre-populate the TupleDesc cache for composite columns and
500- * arrays of composite elements. Anonymous RECORD types are skipped
501- * because their structure is not known until row data is inspected.
510+ * Pre-populate the TupleDesc cache for composite columns and arrays
511+ * of composite elements. Anonymous RECORD types are skipped because
512+ * their structure is not known until row data is inspected.
502513 */
503514 {
504515 Oid elemTypeOid = get_element_type (attr -> atttypid );
@@ -507,8 +518,15 @@ StartCopyTo(CopyToState cstate, TupleDesc tupDesc)
507518
508519 if (get_typtype (baseTypeOid ) == TYPTYPE_COMPOSITE &&
509520 baseTypeOid != RECORDOID )
510- cstate -> composite_tupdescs [attnum - 1 ] =
511- lookup_rowtype_tupdesc (baseTypeOid , baseTypmod );
521+ {
522+ bool found ;
523+ TupleDescCacheEntry * entry = (TupleDescCacheEntry * )
524+ hash_search (cstate -> tupdesc_cache , & baseTypeOid ,
525+ HASH_ENTER , & found );
526+
527+ if (!found )
528+ entry -> tupdesc = lookup_rowtype_tupdesc (baseTypeOid , baseTypmod );
529+ }
512530 }
513531 }
514532
@@ -812,29 +830,11 @@ CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot)
812830
813831 if (ShouldUseDuckSerialization (cstate -> targetFormat , MakePGType (attr -> atttypid , attr -> atttypmod )))
814832 {
815- /*
816- * Since we are at the top-level when emitting an
817- * attribute in CopyOneRowTo(), we are not inside a
818- * composite type.
819- *
820- * For composite-type columns, cache the TupleDesc so
821- * we avoid repeating the expensive lookup_rowtype_tupdesc
822- * call on every row.
823- */
824- /*
825- * Pass the pre-populated TupleDesc (non-NULL for
826- * composite columns and arrays of composite elements)
827- * so PGDuckSerialize can skip the per-row
828- * lookup_rowtype_tupdesc call.
829- */
830- TupleDesc cachedTupleDesc =
831- cstate -> composite_tupdescs [attnum - 1 ];
832-
833833 string = PGDuckSerialize (& out_functions [attnum - 1 ],
834834 attr -> atttypid ,
835835 value ,
836836 cstate -> targetFormat ,
837- cachedTupleDesc );
837+ cstate -> tupdesc_cache );
838838 }
839839 else
840840 string = OutputFunctionCall (& out_functions [attnum - 1 ],
0 commit comments