From f2a38d918a0cb3f4279c75f399c82eedb37c909d Mon Sep 17 00:00:00 2001 From: Haibo Yan Date: Sun, 26 Apr 2026 22:24:20 -0700 Subject: [PATCH v1 5/5] executor: adopt buffered-insert API for restricted INSERT ... SELECT Add a restricted first-step buffered-insert adoption for `INSERT INTO ... SELECT` in the executor. This patch integrates the existing buffered-insert lifecycle API into the plain non-partitioned heap `CMD_INSERT` path in `ExecInsert()` / `ExecModifyTable()`, using a local flush callback for post-insert work. The buffered path is intentionally narrow. It falls back to the existing single-row path for cases outside the restricted scope, including: - ON CONFLICT - RETURNING - partitioned targets - BEFORE ROW / INSTEAD OF triggers - FDW targets - MERGE / cross-partition UPDATE insert side - volatile target-side default expressions The flush callback performs the minimal post-insert executor work needed for this first step: - index maintenance via `ExecInsertIndexTuples()` - AFTER ROW trigger firing via `ExecARInsertTriggers()` - `es_processed` accounting Add focused regression coverage for: - basic bulk INSERT ... SELECT - indexed targets - AFTER ROW trigger behavior - index + trigger combination - fallback cases (ON CONFLICT, RETURNING, BEFORE ROW trigger, partitioned target, volatile target defaults) - zero-row insert --- src/backend/executor/nodeModifyTable.c | 164 +++++++++++ src/include/nodes/execnodes.h | 5 + src/test/regress/expected/insert_buffered.out | 271 ++++++++++++++++++ src/test/regress/parallel_schedule | 2 +- src/test/regress/sql/insert_buffered.sql | 209 ++++++++++++++ 5 files changed, 650 insertions(+), 1 deletion(-) create mode 100644 src/test/regress/expected/insert_buffered.out create mode 100644 src/test/regress/sql/insert_buffered.sql diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c index 4cb057ca4f9..06af0637407 100644 --- a/src/backend/executor/nodeModifyTable.c +++ b/src/backend/executor/nodeModifyTable.c @@ -83,6 +83,85 @@ typedef struct MTTargetRelLookup int relationIndex; /* rel's index in resultRelInfo[] array */ } MTTargetRelLookup; +/* + * Flush callback context for buffered INSERT INTO ... SELECT. + * + * These are the parameters required by the three callback operations + * (ExecInsertIndexTuples, ExecARInsertTriggers, es_processed++): + * + * - estate, resultRelInfo: required by ExecInsertIndexTuples/ExecARInsertTriggers. + * - transition_capture: 5th parameter of ExecARInsertTriggers; the non-buffered + * path passes mtstate->mt_transition_capture for CMD_INSERT. NULL would + * silently break statement-level triggers with REFERENCING NEW TABLE. + * - canSetTag: the non-buffered path gates es_processed++ on this after the + * insert; the buffered return-NULL bypasses that gate, so the callback + * replicates it. + */ +typedef struct ExecBufferedInsertFlushState +{ + EState *estate; + ResultRelInfo *resultRelInfo; + TransitionCaptureState *transition_capture; + bool canSetTag; +} ExecBufferedInsertFlushState; + +/* + * Flush callback for buffered INSERT INTO ... SELECT. + * + * Called once per flushed tuple after heap_multi_insert() completes a batch. + * Performs index maintenance, AFTER ROW trigger firing, and tuple counting. + */ +static void +ExecBufferedInsertFlushCb(void *context, TupleTableSlot *slot) +{ + ExecBufferedInsertFlushState *ctx = (ExecBufferedInsertFlushState *) context; + ResultRelInfo *resultRelInfo = ctx->resultRelInfo; + EState *estate = ctx->estate; + List *recheckIndexes = NIL; + + if (resultRelInfo->ri_NumIndices > 0) + recheckIndexes = ExecInsertIndexTuples(resultRelInfo, estate, 0, + slot, NIL, NULL); + + ExecARInsertTriggers(estate, resultRelInfo, slot, recheckIndexes, + ctx->transition_capture); + + list_free(recheckIndexes); + + if (ctx->canSetTag) + (estate->es_processed)++; +} + +/* + * Check whether a relation has volatile default expressions. + * + * Conservative target-side restriction: if any column default contains a + * volatile function (excluding nextval), the buffered-insert path is not used. + * This mirrors COPY FROM's volatile_defexprs check. + */ +static bool +ExecRelHasVolatileDefaults(Relation rel) +{ + TupleConstr *constr = RelationGetDescr(rel)->constr; + + if (constr == NULL || constr->num_defval == 0) + return false; + + for (int i = 0; i < constr->num_defval; i++) + { + Node *expr; + + if (constr->defval[i].adbin == NULL) + continue; + + expr = stringToNode(constr->defval[i].adbin); + + if (contain_volatile_functions_not_nextval(expr)) + return true; + } + return false; +} + /* * Context struct for a ModifyTable operation, containing basic execution * state and some output variables populated by ExecUpdateAct() and @@ -1269,6 +1348,49 @@ ExecInsert(ModifyTableContext *context, } else { + /* + * Buffered-insert path: lazily open the session on first call, + * then submit tuples via put() instead of single-row insert. + * Post-insert work (indexes, triggers) fires in the flush callback. + */ + if (mtstate->mt_buffered_insert_eligible && + mtstate->mt_bi_state == NULL) + { + ExecBufferedInsertFlushState *flush_ctx; + + flush_ctx = palloc(sizeof(ExecBufferedInsertFlushState)); + flush_ctx->estate = estate; + flush_ctx->resultRelInfo = resultRelInfo; + flush_ctx->transition_capture = mtstate->mt_transition_capture; + flush_ctx->canSetTag = canSetTag; + mtstate->mt_bi_flush_ctx = flush_ctx; + + mtstate->mt_bi_state = + table_buffered_insert_begin(resultRelationDesc, + estate->es_output_cid, + TABLE_INSERT_BAS_BULKWRITE, + ExecBufferedInsertFlushCb, + flush_ctx); + if (mtstate->mt_bi_state == NULL) + mtstate->mt_buffered_insert_eligible = false; + } + + if (mtstate->mt_bi_state != NULL) + { + /* + * Pre-insert validation already ran in this else-branch + * above the ON CONFLICT test — specifically: + * tts_tableOid init, ExecComputeStoredGenerated, + * ExecWithCheckOptions (RLS), ExecConstraints, + * ExecPartitionCheck. + * This inner else-branch (no ON CONFLICT) is reached only + * after all of those. Submit the validated tuple to the + * AM buffer; post-insert work fires in the flush callback. + */ + table_buffered_insert_put(mtstate->mt_bi_state, slot); + return NULL; + } + /* insert the tuple normally */ table_tuple_insert(resultRelationDesc, slot, estate->es_output_cid, @@ -5027,6 +5149,15 @@ ExecModifyTable(PlanState *pstate) if (estate->es_insert_pending_result_relations != NIL) ExecPendingInserts(estate); + /* + * Flush and clean up buffered-insert session if active. + */ + if (node->mt_bi_state != NULL) + { + table_buffered_insert_end(node->mt_bi_state); + node->mt_bi_state = NULL; + } + /* * We're done, but fire AFTER STATEMENT triggers before exiting. */ @@ -5773,6 +5904,30 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags) resultRelInfo->ri_BatchSize = 1; } + /* + * For CMD_INSERT without ON CONFLICT/RETURNING/partitioning/BEFORE ROW + * triggers, determine if the restricted buffered-insert path is eligible. + * AM support is resolved lazily at first ExecInsert() call. + */ + if (operation == CMD_INSERT) + { + ModifyTable *mtnode = (ModifyTable *) mtstate->ps.plan; + + resultRelInfo = mtstate->resultRelInfo; + mtstate->mt_buffered_insert_eligible = + (mtnode->onConflictAction == ONCONFLICT_NONE && + resultRelInfo->ri_projectReturning == NULL && + resultRelInfo->ri_RelationDesc->rd_rel->relkind != + RELKIND_PARTITIONED_TABLE && + !(resultRelInfo->ri_TrigDesc && + resultRelInfo->ri_TrigDesc->trig_insert_before_row) && + !(resultRelInfo->ri_TrigDesc && + resultRelInfo->ri_TrigDesc->trig_insert_instead_row) && + resultRelInfo->ri_FdwRoutine == NULL && + mtstate->operation == CMD_INSERT && + !ExecRelHasVolatileDefaults(resultRelInfo->ri_RelationDesc)); + } + /* * Lastly, if this is not the primary (canSetTag) ModifyTable node, add it * to estate->es_auxmodifytables so that it will be run to completion by @@ -5802,6 +5957,15 @@ ExecEndModifyTable(ModifyTableState *node) { int i; + /* + * Defensive: clean up buffered-insert if end() was not reached above. + */ + if (node->mt_bi_state != NULL) + { + table_buffered_insert_end(node->mt_bi_state); + node->mt_bi_state = NULL; + } + /* * Allow any FDWs to shut down */ diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index 13359180d25..7790bb0ba38 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -1508,6 +1508,11 @@ typedef struct ModifyTableState List *mt_updateColnosLists; List *mt_mergeActionLists; List *mt_mergeJoinConditions; + + /* Buffered-insert state for restricted INSERT INTO ... SELECT */ + bool mt_buffered_insert_eligible; + struct TableBufferedInsertStateData *mt_bi_state; + void *mt_bi_flush_ctx; /* private to nodeModifyTable.c */ } ModifyTableState; /* ---------------- diff --git a/src/test/regress/expected/insert_buffered.out b/src/test/regress/expected/insert_buffered.out new file mode 100644 index 00000000000..3ee43ae8b04 --- /dev/null +++ b/src/test/regress/expected/insert_buffered.out @@ -0,0 +1,271 @@ +-- +-- Tests for buffered-insert adoption in INSERT INTO ... SELECT (Patch 0005). +-- Restricted first step: non-partitioned heap target, no ON CONFLICT, +-- no RETURNING, no BEFORE ROW triggers. +-- +-- ============================================================ +-- T1: Basic bulk insert (exercises multiple auto-flush cycles) +-- ============================================================ +CREATE TABLE bi_target_basic (id int, val text); +INSERT INTO bi_target_basic +SELECT g, 'row-' || g FROM generate_series(1, 2000) g; +SELECT count(*) FROM bi_target_basic; + count +------- + 2000 +(1 row) + +SELECT min(id), max(id) FROM bi_target_basic; + min | max +-----+------ + 1 | 2000 +(1 row) + +DROP TABLE bi_target_basic; +-- ============================================================ +-- T2: Indexed target +-- ============================================================ +CREATE TABLE bi_target_idx (id int, val text); +CREATE INDEX bi_target_idx_id ON bi_target_idx (id); +INSERT INTO bi_target_idx +SELECT g, 'row-' || g FROM generate_series(1, 500) g; +SELECT count(*) FROM bi_target_idx; + count +------- + 500 +(1 row) + +-- Verify index is usable and correct +SET enable_seqscan = off; +SELECT count(*) FROM bi_target_idx WHERE id BETWEEN 1 AND 500; + count +------- + 500 +(1 row) + +RESET enable_seqscan; +DROP TABLE bi_target_idx; +-- ============================================================ +-- T3: AFTER ROW trigger +-- ============================================================ +CREATE TABLE bi_target_trig (id int, val text); +CREATE TABLE bi_audit (id int, val text, logged_at timestamp DEFAULT now()); +CREATE FUNCTION bi_audit_fn() RETURNS trigger +LANGUAGE plpgsql AS $$ +BEGIN + INSERT INTO bi_audit (id, val) VALUES (NEW.id, NEW.val); + RETURN NEW; +END; +$$; +CREATE TRIGGER bi_target_trig_after + AFTER INSERT ON bi_target_trig + FOR EACH ROW EXECUTE FUNCTION bi_audit_fn(); +INSERT INTO bi_target_trig +SELECT g, 'row-' || g FROM generate_series(1, 50) g; +SELECT count(*) FROM bi_target_trig; + count +------- + 50 +(1 row) + +SELECT count(*) FROM bi_audit; + count +------- + 50 +(1 row) + +-- Verify insertion order is preserved +SELECT bool_and(t.id = a.id) AS order_preserved +FROM (SELECT id, row_number() OVER (ORDER BY ctid) AS rn FROM bi_target_trig) t +JOIN (SELECT id, row_number() OVER (ORDER BY ctid) AS rn FROM bi_audit) a +ON t.rn = a.rn; + order_preserved +----------------- + t +(1 row) + +DROP TABLE bi_target_trig CASCADE; +DROP TABLE bi_audit; +DROP FUNCTION bi_audit_fn; +-- ============================================================ +-- T4: Index + AFTER ROW trigger combined +-- ============================================================ +CREATE TABLE bi_target_combo (id int, val text); +CREATE INDEX bi_target_combo_id ON bi_target_combo (id); +CREATE TABLE bi_audit_combo (id int, val text); +CREATE FUNCTION bi_audit_combo_fn() RETURNS trigger +LANGUAGE plpgsql AS $$ +BEGIN + INSERT INTO bi_audit_combo (id, val) VALUES (NEW.id, NEW.val); + RETURN NEW; +END; +$$; +CREATE TRIGGER bi_target_combo_after + AFTER INSERT ON bi_target_combo + FOR EACH ROW EXECUTE FUNCTION bi_audit_combo_fn(); +INSERT INTO bi_target_combo +SELECT g, 'row-' || g FROM generate_series(1, 100) g; +SELECT count(*) FROM bi_target_combo; + count +------- + 100 +(1 row) + +SELECT count(*) FROM bi_audit_combo; + count +------- + 100 +(1 row) + +-- Verify index correctness +SET enable_seqscan = off; +SELECT count(*) FROM bi_target_combo WHERE id BETWEEN 1 AND 100; + count +------- + 100 +(1 row) + +RESET enable_seqscan; +DROP TABLE bi_target_combo CASCADE; +DROP TABLE bi_audit_combo; +DROP FUNCTION bi_audit_combo_fn; +-- ============================================================ +-- T5: ON CONFLICT fallback (uses non-buffered path) +-- ============================================================ +CREATE TABLE bi_target_conflict (id int PRIMARY KEY, val text); +INSERT INTO bi_target_conflict VALUES (1, 'existing'); +INSERT INTO bi_target_conflict +SELECT g, 'row-' || g FROM generate_series(1, 10) g +ON CONFLICT (id) DO NOTHING; +SELECT count(*) FROM bi_target_conflict; + count +------- + 10 +(1 row) + +SELECT val FROM bi_target_conflict WHERE id = 1; + val +---------- + existing +(1 row) + +DROP TABLE bi_target_conflict; +-- ============================================================ +-- T6: RETURNING fallback (uses non-buffered path) +-- ============================================================ +CREATE TABLE bi_target_ret (id int, val text); +INSERT INTO bi_target_ret +SELECT g, 'row-' || g FROM generate_series(1, 3) g +RETURNING id, val; + id | val +----+------- + 1 | row-1 + 2 | row-2 + 3 | row-3 +(3 rows) + +SELECT count(*) FROM bi_target_ret; + count +------- + 3 +(1 row) + +DROP TABLE bi_target_ret; +-- ============================================================ +-- T7: BEFORE ROW trigger fallback (uses non-buffered path) +-- ============================================================ +CREATE TABLE bi_target_br (id int, val text); +CREATE FUNCTION bi_br_fn() RETURNS trigger +LANGUAGE plpgsql AS $$ +BEGIN + NEW.val := NEW.val || '-modified'; + RETURN NEW; +END; +$$; +CREATE TRIGGER bi_target_br_before + BEFORE INSERT ON bi_target_br + FOR EACH ROW EXECUTE FUNCTION bi_br_fn(); +INSERT INTO bi_target_br +SELECT g, 'row-' || g FROM generate_series(1, 5) g; +SELECT count(*) FROM bi_target_br; + count +------- + 5 +(1 row) + +SELECT val FROM bi_target_br WHERE id = 1; + val +---------------- + row-1-modified +(1 row) + +DROP TABLE bi_target_br; +DROP FUNCTION bi_br_fn; +-- ============================================================ +-- T8: Partitioned target fallback (uses non-buffered path) +-- ============================================================ +CREATE TABLE bi_target_part (id int, val text) PARTITION BY RANGE (id); +CREATE TABLE bi_target_part_1 PARTITION OF bi_target_part FOR VALUES FROM (1) TO (501); +CREATE TABLE bi_target_part_2 PARTITION OF bi_target_part FOR VALUES FROM (501) TO (1001); +INSERT INTO bi_target_part +SELECT g, 'row-' || g FROM generate_series(1, 1000) g; +SELECT count(*) FROM bi_target_part; + count +------- + 1000 +(1 row) + +SELECT count(*) FROM bi_target_part_1; + count +------- + 500 +(1 row) + +SELECT count(*) FROM bi_target_part_2; + count +------- + 500 +(1 row) + +DROP TABLE bi_target_part; +-- ============================================================ +-- T9: Volatile target-default fallback +-- Expected to fall back to non-buffered path under E8. +-- Test validates correctness; path selection is not observable +-- from SQL output. +-- ============================================================ +CREATE TABLE bi_target_volatile ( + id int, + val text, + rand_val double precision DEFAULT random() +); +INSERT INTO bi_target_volatile (id, val) +SELECT g, 'row-' || g FROM generate_series(1, 5) g; +SELECT count(*) FROM bi_target_volatile; + count +------- + 5 +(1 row) + +-- Verify the volatile default was evaluated (all values should be distinct) +SELECT count(DISTINCT rand_val) = count(*) AS all_distinct +FROM bi_target_volatile; + all_distinct +-------------- + t +(1 row) + +DROP TABLE bi_target_volatile; +-- ============================================================ +-- T10: Zero-row insert +-- ============================================================ +CREATE TABLE bi_target_zero (id int, val text); +INSERT INTO bi_target_zero +SELECT g, 'row-' || g FROM generate_series(1, 100) g WHERE false; +SELECT count(*) FROM bi_target_zero; + count +------- + 0 +(1 row) + +DROP TABLE bi_target_zero; diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index 5d4f910155e..ae63c3dd73c 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -36,7 +36,7 @@ test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comment # execute two copy tests in parallel, to check that copy itself # is concurrent safe. # ---------- -test: copy copyselect copydml copyencoding insert insert_conflict +test: copy copyselect copydml copyencoding insert insert_conflict insert_buffered # ---------- # More groups of parallel tests diff --git a/src/test/regress/sql/insert_buffered.sql b/src/test/regress/sql/insert_buffered.sql new file mode 100644 index 00000000000..2ca6f3525f4 --- /dev/null +++ b/src/test/regress/sql/insert_buffered.sql @@ -0,0 +1,209 @@ +-- +-- Tests for buffered-insert adoption in INSERT INTO ... SELECT (Patch 0005). +-- Restricted first step: non-partitioned heap target, no ON CONFLICT, +-- no RETURNING, no BEFORE ROW triggers. +-- + +-- ============================================================ +-- T1: Basic bulk insert (exercises multiple auto-flush cycles) +-- ============================================================ +CREATE TABLE bi_target_basic (id int, val text); + +INSERT INTO bi_target_basic +SELECT g, 'row-' || g FROM generate_series(1, 2000) g; + +SELECT count(*) FROM bi_target_basic; +SELECT min(id), max(id) FROM bi_target_basic; + +DROP TABLE bi_target_basic; + +-- ============================================================ +-- T2: Indexed target +-- ============================================================ +CREATE TABLE bi_target_idx (id int, val text); +CREATE INDEX bi_target_idx_id ON bi_target_idx (id); + +INSERT INTO bi_target_idx +SELECT g, 'row-' || g FROM generate_series(1, 500) g; + +SELECT count(*) FROM bi_target_idx; + +-- Verify index is usable and correct +SET enable_seqscan = off; +SELECT count(*) FROM bi_target_idx WHERE id BETWEEN 1 AND 500; +RESET enable_seqscan; + +DROP TABLE bi_target_idx; + +-- ============================================================ +-- T3: AFTER ROW trigger +-- ============================================================ +CREATE TABLE bi_target_trig (id int, val text); +CREATE TABLE bi_audit (id int, val text, logged_at timestamp DEFAULT now()); + +CREATE FUNCTION bi_audit_fn() RETURNS trigger +LANGUAGE plpgsql AS $$ +BEGIN + INSERT INTO bi_audit (id, val) VALUES (NEW.id, NEW.val); + RETURN NEW; +END; +$$; + +CREATE TRIGGER bi_target_trig_after + AFTER INSERT ON bi_target_trig + FOR EACH ROW EXECUTE FUNCTION bi_audit_fn(); + +INSERT INTO bi_target_trig +SELECT g, 'row-' || g FROM generate_series(1, 50) g; + +SELECT count(*) FROM bi_target_trig; +SELECT count(*) FROM bi_audit; + +-- Verify insertion order is preserved +SELECT bool_and(t.id = a.id) AS order_preserved +FROM (SELECT id, row_number() OVER (ORDER BY ctid) AS rn FROM bi_target_trig) t +JOIN (SELECT id, row_number() OVER (ORDER BY ctid) AS rn FROM bi_audit) a +ON t.rn = a.rn; + +DROP TABLE bi_target_trig CASCADE; +DROP TABLE bi_audit; +DROP FUNCTION bi_audit_fn; + +-- ============================================================ +-- T4: Index + AFTER ROW trigger combined +-- ============================================================ +CREATE TABLE bi_target_combo (id int, val text); +CREATE INDEX bi_target_combo_id ON bi_target_combo (id); +CREATE TABLE bi_audit_combo (id int, val text); + +CREATE FUNCTION bi_audit_combo_fn() RETURNS trigger +LANGUAGE plpgsql AS $$ +BEGIN + INSERT INTO bi_audit_combo (id, val) VALUES (NEW.id, NEW.val); + RETURN NEW; +END; +$$; + +CREATE TRIGGER bi_target_combo_after + AFTER INSERT ON bi_target_combo + FOR EACH ROW EXECUTE FUNCTION bi_audit_combo_fn(); + +INSERT INTO bi_target_combo +SELECT g, 'row-' || g FROM generate_series(1, 100) g; + +SELECT count(*) FROM bi_target_combo; +SELECT count(*) FROM bi_audit_combo; + +-- Verify index correctness +SET enable_seqscan = off; +SELECT count(*) FROM bi_target_combo WHERE id BETWEEN 1 AND 100; +RESET enable_seqscan; + +DROP TABLE bi_target_combo CASCADE; +DROP TABLE bi_audit_combo; +DROP FUNCTION bi_audit_combo_fn; + +-- ============================================================ +-- T5: ON CONFLICT fallback (uses non-buffered path) +-- ============================================================ +CREATE TABLE bi_target_conflict (id int PRIMARY KEY, val text); + +INSERT INTO bi_target_conflict VALUES (1, 'existing'); + +INSERT INTO bi_target_conflict +SELECT g, 'row-' || g FROM generate_series(1, 10) g +ON CONFLICT (id) DO NOTHING; + +SELECT count(*) FROM bi_target_conflict; +SELECT val FROM bi_target_conflict WHERE id = 1; + +DROP TABLE bi_target_conflict; + +-- ============================================================ +-- T6: RETURNING fallback (uses non-buffered path) +-- ============================================================ +CREATE TABLE bi_target_ret (id int, val text); + +INSERT INTO bi_target_ret +SELECT g, 'row-' || g FROM generate_series(1, 3) g +RETURNING id, val; + +SELECT count(*) FROM bi_target_ret; + +DROP TABLE bi_target_ret; + +-- ============================================================ +-- T7: BEFORE ROW trigger fallback (uses non-buffered path) +-- ============================================================ +CREATE TABLE bi_target_br (id int, val text); + +CREATE FUNCTION bi_br_fn() RETURNS trigger +LANGUAGE plpgsql AS $$ +BEGIN + NEW.val := NEW.val || '-modified'; + RETURN NEW; +END; +$$; + +CREATE TRIGGER bi_target_br_before + BEFORE INSERT ON bi_target_br + FOR EACH ROW EXECUTE FUNCTION bi_br_fn(); + +INSERT INTO bi_target_br +SELECT g, 'row-' || g FROM generate_series(1, 5) g; + +SELECT count(*) FROM bi_target_br; +SELECT val FROM bi_target_br WHERE id = 1; + +DROP TABLE bi_target_br; +DROP FUNCTION bi_br_fn; + +-- ============================================================ +-- T8: Partitioned target fallback (uses non-buffered path) +-- ============================================================ +CREATE TABLE bi_target_part (id int, val text) PARTITION BY RANGE (id); +CREATE TABLE bi_target_part_1 PARTITION OF bi_target_part FOR VALUES FROM (1) TO (501); +CREATE TABLE bi_target_part_2 PARTITION OF bi_target_part FOR VALUES FROM (501) TO (1001); + +INSERT INTO bi_target_part +SELECT g, 'row-' || g FROM generate_series(1, 1000) g; + +SELECT count(*) FROM bi_target_part; +SELECT count(*) FROM bi_target_part_1; +SELECT count(*) FROM bi_target_part_2; + +DROP TABLE bi_target_part; + +-- ============================================================ +-- T9: Volatile target-default fallback +-- Expected to fall back to non-buffered path under E8. +-- Test validates correctness; path selection is not observable +-- from SQL output. +-- ============================================================ +CREATE TABLE bi_target_volatile ( + id int, + val text, + rand_val double precision DEFAULT random() +); + +INSERT INTO bi_target_volatile (id, val) +SELECT g, 'row-' || g FROM generate_series(1, 5) g; + +SELECT count(*) FROM bi_target_volatile; +-- Verify the volatile default was evaluated (all values should be distinct) +SELECT count(DISTINCT rand_val) = count(*) AS all_distinct +FROM bi_target_volatile; + +DROP TABLE bi_target_volatile; + +-- ============================================================ +-- T10: Zero-row insert +-- ============================================================ +CREATE TABLE bi_target_zero (id int, val text); + +INSERT INTO bi_target_zero +SELECT g, 'row-' || g FROM generate_series(1, 100) g WHERE false; + +SELECT count(*) FROM bi_target_zero; + +DROP TABLE bi_target_zero; -- 2.52.0