@@ -323,104 +323,87 @@ pltsql_insert_exec_open_target_table(const char *target_table,
323323 * Validate column count from query string BEFORE plan preparation
324324 */
325325void
326- pltsql_insert_exec_validate_column_count_from_query ( const char * query_string )
326+ pltsql_insert_exec_validate_column_count ( PLtsql_execstate * estate , PLtsql_stmt_execsql * stmt )
327327{
328- SPIPlanPtr plan = NULL ;
328+ PLtsql_expr * expr = stmt -> sqlstmt ;
329+ SPIPlanPtr plan ;
330+ List * plansources ;
329331 CachedPlanSource * plansource ;
330332 TupleDesc result_desc ;
331333 int query_natts ;
332334 Oid temp_table_oid ;
333335 Relation temp_rel ;
334336 TupleDesc temp_tupdesc ;
335337 int temp_natts ;
336- MemoryContext oldcontext ;
337338
338339 /* Caller must ensure INSERT EXEC is active before calling */
339340 Assert (pltsql_insert_exec_active ());
340341
341342 /*
342343 * Temp table must exist. It is created during INSERT EXEC setup before any
343- * procedure-body statement runs, so it is normally valid here
344+ * procedure-body statement runs, so it is normally valid here.
344345 */
345346 temp_table_oid = insert_exec_ctx -> temp_table_oid ;
346347 if (!OidIsValid (temp_table_oid ))
347348 return ;
348349
350+ if (expr == NULL || expr -> query == NULL )
351+ return ;
352+
349353 /*
350- * Parse-analyze the query to obtain its result descriptor. SPI_prepare
351- * stops before planning, so constant expressions are not evaluated and a
352- * runtime error like division by zero will not fire here. On any error,
353- * defer to the normal execution path.
354+ * Reuse the cached plan if the statement was already prepared (earlier
355+ * execution). Otherwise prepare it once, through the normal parser setup.
354356 */
355- oldcontext = CurrentMemoryContext ;
356-
357- PG_TRY ();
358- {
359- plan = SPI_prepare (query_string , 0 , NULL );
360- }
361- PG_CATCH ();
357+ if (expr -> plan != NULL )
358+ plan = expr -> plan ;
359+ else
362360 {
363- /*
364- * Probe-only failure: defer to real execution, which re-raises it.
365- * Re-throw cancellation/OOM immediately - those must be honored now
366- */
367- ErrorData * edata ;
368- MemoryContextSwitchTo (oldcontext );
369- edata = CopyErrorData ();
370- FlushErrorState ();
371-
372- if (edata -> sqlerrcode == ERRCODE_QUERY_CANCELED ||
373- edata -> sqlerrcode == ERRCODE_ADMIN_SHUTDOWN ||
374- edata -> sqlerrcode == ERRCODE_OUT_OF_MEMORY )
375- {
376- ReThrowError (edata );
377- }
378- FreeErrorData (edata );
379- return ; /* Parse/analyze error - normal path will report it */
361+ expr -> func = estate -> func ;
362+ plan = SPI_prepare_params (expr -> query ,
363+ (ParserSetupHook ) pltsql_parser_setup ,
364+ (void * ) expr ,
365+ CURSOR_OPT_PARALLEL_OK );
366+ if (plan == NULL )
367+ return ;
380368 }
381- PG_END_TRY ();
369+
370+ plansources = SPI_plan_get_plan_sources (plan );
382371
383372 /* Expect exactly one analyzed statement with a known result shape */
384- if (plan == NULL || list_length (plan -> plancache_list ) != 1 )
385- {
386- if (plan != NULL )
387- SPI_freeplan (plan );
388- return ; /* Multiple statements or unusable plan, defer to runtime */
389- }
373+ if (list_length (plansources ) != 1 )
374+ return ;
390375
391- plansource = (CachedPlanSource * ) linitial (plan -> plancache_list );
376+ plansource = (CachedPlanSource * ) linitial (plansources );
392377 result_desc = plansource -> resultDesc ;
393378
394- /*
395- * resultDesc is NULL for statements that do not return tuples (e.g. EXEC).
396- * In that case there is nothing to validate statically; defer to runtime.
397- */
398379 if (result_desc == NULL )
399- {
400- SPI_freeplan (plan );
401380 return ;
402- }
403381
404382 query_natts = result_desc -> natts ;
405- SPI_freeplan (plan );
406383
407384 /* Get temp table column count */
408385 temp_rel = table_open (temp_table_oid , AccessShareLock );
409386 temp_tupdesc = RelationGetDescr (temp_rel );
410387 temp_natts = temp_tupdesc -> natts ;
411388 table_close (temp_rel , AccessShareLock );
412389
413- /* Check for column count mismatch */
390+ /* Column count mismatch: raise before execution so it wins over 1/0 etc. */
414391 if (query_natts != temp_natts )
415- {
416- /*
417- * A column mismatch must roll back all rows even when caught by
418- * TRY-CATCH, unlike data-level errors (e.g. division by zero) which
419- * only drop the current row.
420- */
421392 ereport (ERROR ,
422393 (errcode (ERRCODE_DATATYPE_MISMATCH ),
423394 errmsg ("structure of query does not match function result type" )));
395+
396+ /*
397+ * If we prepared the plan here (expr->plan was NULL), keep it and
398+ * cache it so exec_stmt_execsql reuses it instead of preparing again. This
399+ * is a result-returning SELECT, so it is not a modification statement.
400+ */
401+ if (expr -> plan == NULL )
402+ {
403+ SPI_keepplan (plan );
404+ expr -> plan = plan ;
405+ stmt -> mod_stmt = false;
406+ stmt -> mod_stmt_tablevar = false;
424407 }
425408}
426409
0 commit comments