diff --git a/README b/README index 78ba5ac..408be25 100644 --- a/README +++ b/README @@ -121,44 +121,35 @@ your Client SDK installation (e.g. $INFORMIXDIR/etc/sqlhosts and the Informix FDW can be used as follows, assumed you have an Informix connection named 'centosifx_tcp': +```sql CREATE EXTENSION informix_fdw; -CREATE SERVER centosifx_tcp +CREATE SERVER test_server FOREIGN DATA WRAPPER informix_fdw -OPTIONS (informixserver 'centosifx_tcp'); +OPTIONS (informixserver 'informix', informixdir '/opt/informix/csdk'); CREATE USER MAPPING FOR CURRENT_USER -SERVER centosifx_tcp +SERVER test_server OPTIONS (username 'informix', password 'informix'); -CREATE FOREIGN TABLE foo ( - id integer, - value integer - ) -SERVER centosifx_tcp -OPTIONS ( query 'SELECT * FROM foo', - database 'test', - informixdir '/Applications/IBM/informix'); - -CREATE SERVER sles11_tcp -FOREIGN DATA WRAPPER informix_fdw -OPTIONS (informixserver 'ol_informix1170'); - -CREATE USER MAPPING FOR CURRENT_USER -SERVER sles11_tcp -OPTIONS (username 'informix', password 'informix'); - -CREATE FOREIGN TABLE foo ( - id integer, - value integer - ) -SERVER sles11_tcp -OPTIONS ( query 'SELECT * FROM foo', - database 'test', - db_locale 'en_us.819', - client_locale 'en_US.utf8', - informixserver 'ol_informix1170', - informixdir '/Applications/IBM/informix'); +CREATE FOREIGN TABLE inttest(f1 bigint not null, + f2 integer, + f3 smallint) +SERVER test_server +OPTIONS(table 'inttest', + client_locale 'en_US.utf8' + db_locale 'en_US.819', + database 'regression_dml'); +```sql + +The settings `informixdir` and `informixserver` are dependent on your +installation and configuration. `db_locale` and `client_locale` must be +specified to enable correct conversion between foreign Informix and local PostgreSQL server. + +`table` identifies the table on the remote Informix server you want to access. There +is also the `query` parameter in the example above, which can be used to access +the remote data in a view-like manner. For more detailed information about options +read the FDW Options section below. = Supported datatypes = diff --git a/expected/informix_fdw_tx_1.out b/expected/informix_fdw_tx_1.out index ddf99f9..72c5ac5 100644 --- a/expected/informix_fdw_tx_1.out +++ b/expected/informix_fdw_tx_1.out @@ -1219,6 +1219,188 @@ COMMIT; -- ALTER FOREIGN TABLE inttest OPTIONS(DROP disable_rowid); -------------------------------------------------------------------------------- +-- Tests for PREPARE +-- +-- See discussion in github issue +-- https://github.com/credativ/informix_fdw/issues/31 +-------------------------------------------------------------------------------- +-- +-- INSERT +-- +BEGIN; +PREPARE ins_inttest(bigint) AS INSERT INTO inttest VALUES($1); +EXECUTE ins_inttest (1); +EXECUTE ins_inttest (2); +EXECUTE ins_inttest (3); +EXECUTE ins_inttest (4); +EXECUTE ins_inttest (5); +EXECUTE ins_inttest (6); +EXECUTE ins_inttest (7); +COMMIT; +DEALLOCATE ins_inttest; +-- +-- UPDATE +-- +BEGIN; +PREPARE upd_inttest(bigint) AS UPDATE inttest SET f1 = f1 WHERE f1 = $1; +EXECUTE upd_inttest (1); +EXECUTE upd_inttest (2); +EXECUTE upd_inttest (3); +EXECUTE upd_inttest (4); +EXECUTE upd_inttest (5); +EXECUTE upd_inttest (6); +EXECUTE upd_inttest (7); +COMMIT; +DEALLOCATE upd_inttest; +-- +-- DELETE +-- +BEGIN; +PREPARE del_inttest(bigint) AS DELETE FROM inttest WHERE f1 = $1; +EXECUTE del_inttest (1); +EXECUTE del_inttest (2); +EXECUTE del_inttest (3); +EXECUTE del_inttest (4); +EXECUTE del_inttest (5); +EXECUTE del_inttest (6); +EXECUTE del_inttest (7); +COMMIT; +DEALLOCATE del_inttest; +-------------------------------------------------------------------------------- +-- Trigger Tests +-------------------------------------------------------------------------------- +BEGIN; +CREATE TABLE IF NOT EXISTS delete_fdw_trigger_test(id bigint primary key); +-- +-- A before trigger testing before actions on INSERT/UPDATE/DELETE +-- on a foreign table +-- +CREATE OR REPLACE FUNCTION f_tg_test() +RETURNS trigger +LANGUAGE plpgsql +AS +$$ +BEGIN + IF TG_OP = 'DELETE' THEN + DELETE FROM inttest WHERE f1 = OLD.id; + RETURN OLD; + ELSIF TG_OP = 'UPDATE' THEN + UPDATE inttest SET f1 = NEW.id WHERE f1 = OLD.id; + RETURN NEW; + ELSIF TG_OP = 'INSERT' THEN + INSERT INTO inttest VALUES(NEW.id); + RETURN NEW; + ELSE + RAISE EXCEPTION 'unhandled trigger action %', TG_OP; + END IF; +END; +$$; +-- +-- A broken trigger function referencing the wrong tuple identifiers +-- according to the trigger action (NEW vs. OLD) +-- +-- Basically the same as above. +-- +CREATE OR REPLACE FUNCTION f_tg_test_broken() +RETURNS trigger +LANGUAGE plpgsql +AS +$$ +BEGIN + IF TG_OP = 'DELETE' THEN + DELETE FROM inttest WHERE f1 = NEW.id; + RETURN OLD; + ELSIF TG_OP = 'UPDATE' THEN + UPDATE inttest SET f1 = NEW.id WHERE f1 = OLD.id; + RETURN NEW; + ELSIF TG_OP = 'INSERT' THEN + INSERT INTO inttest VALUES(OLD.id); + RETURN NEW; + ELSE + RAISE EXCEPTION 'unhandled trigger action %', TG_OP; + END IF; +END; +$$; +CREATE TRIGGER tg_inttest +BEFORE DELETE OR UPDATE OR INSERT ON delete_fdw_trigger_test +FOR EACH ROW EXECUTE PROCEDURE f_tg_test(); +TRUNCATE delete_fdw_trigger_test; +INSERT INTO delete_fdw_trigger_test VALUES(1), (2), (3); +SELECT * FROM inttest; + f1 | f2 | f3 +----+----+---- + 1 | | + 2 | | + 3 | | +(3 rows) + +DELETE FROM delete_fdw_trigger_test WHERE id = 2; +SELECT * FROM inttest; + f1 | f2 | f3 +----+----+---- + 1 | | + 3 | | +(2 rows) + +UPDATE delete_fdw_trigger_test SET id = 4 WHERE id = 3; +SELECT * FROM inttest; + f1 | f2 | f3 +----+----+---- + 1 | | + 4 | | +(2 rows) + +INSERT INTO delete_fdw_trigger_test VALUES(5); +SELECT * FROM inttest; + f1 | f2 | f3 +----+----+---- + 1 | | + 4 | | + 5 | | +(3 rows) + +DELETE FROM delete_fdw_trigger_test; +SELECT * FROM inttest; + f1 | f2 | f3 +----+----+---- +(0 rows) + +DROP TRIGGER tg_inttest ON delete_fdw_trigger_test; +CREATE TRIGGER tg_inttest +BEFORE DELETE OR UPDATE OR INSERT ON delete_fdw_trigger_test +FOR EACH ROW EXECUTE PROCEDURE f_tg_test_broken(); +-- should fail +SAVEPOINT broken; +INSERT INTO delete_fdw_trigger_test VALUES(1), (2), (3); +ROLLBACK TO broken; +SELECT * FROM inttest; + f1 | f2 | f3 +----+----+---- +(0 rows) + +-- should delete nothing +DELETE FROM delete_fdw_trigger_test WHERE id = 2; +SELECT * FROM inttest; + f1 | f2 | f3 +----+----+---- +(0 rows) + +-- should update nothing +UPDATE delete_fdw_trigger_test SET id = 4 WHERE id = 3; +SELECT * FROM inttest; + f1 | f2 | f3 +----+----+---- +(0 rows) + +DELETE FROM delete_fdw_trigger_test; +SELECT * FROM inttest; + f1 | f2 | f3 +----+----+---- +(0 rows) + +DROP TRIGGER tg_inttest ON delete_fdw_trigger_test; +COMMIT; +-------------------------------------------------------------------------------- -- Regression Tests End, Cleanup -------------------------------------------------------------------------------- DROP FOREIGN TABLE float_test; diff --git a/ifx_conncache.c b/ifx_conncache.c index 61e40d8..77c436d 100644 --- a/ifx_conncache.c +++ b/ifx_conncache.c @@ -25,6 +25,14 @@ */ #define IFX_CONNCACHE_HASHTABLE "IFX_CONN_CACHE" +/* + * Flags for dynahash hashtable, version dependent + */ +#if PG_VERSION_NUM >= 140000 +#define IFX_FDW_CONNCACHE_HASH_FLAGS HASH_ELEM | HASH_CONTEXT | HASH_STRINGS +#else +#define IFX_FDW_CONNCACHE_HASH_FLAGS HASH_ELEM | HASH_CONTEXT +#endif static void ifxConnCache_init(void); bool IfxCacheIsInitialized; @@ -64,7 +72,7 @@ static void ifxConnCache_init() ifxCache.connections = hash_create(IFX_CONNCACHE_HASHTABLE, IFX_CONNCACHE_SIZE, &hash_ctl, - HASH_ELEM | HASH_CONTEXT); + IFX_FDW_CONNCACHE_HASH_FLAGS); /* * Back to old context. diff --git a/ifx_fdw.c b/ifx_fdw.c index b549710..033db6f 100644 --- a/ifx_fdw.c +++ b/ifx_fdw.c @@ -32,6 +32,14 @@ #include "optimizer/appendinfo.h" #endif +/* + * For REL_16_STABLE, as of commit a61b1f74823 we need optimizer/inherit.h + * for get_rel_all_updated_cols(). + */ +#if PG_VERSION_NUM >= 160000 +#include "optimizer/inherit.h" +#endif + #include "access/xact.h" #include "utils/lsyscache.h" @@ -136,10 +144,21 @@ extern PGDLLIMPORT double cpu_tuple_cost; #define IFX_SYSTABLE_SCAN_SNAPSHOT NULL #endif -#if PG_VERSION_NUM >= 90500 -#define RTE_UPDATED_COLS(a) (a)->updatedCols +#if PG_VERSION_NUM >= 90500 && PG_VERSION_NUM < 160000 +#define RTE_UPDATED_COLS(planInfo, resultRel, set) \ + RangeTblEntry *rte = planner_rt_fetch((resultRel), (planInfo)); \ + (set) = bms_copy(rte->updatedCols); +#define BMS_LOOKUP_COL(set, attnum) bms_first_member((set)) +#elif PG_VERSION_NUM >= 160000 +#define RTE_UPDATED_COLS(planInfo, resultRel, set) \ + RelOptInfo *relInfo = find_base_rel((planInfo), (resultRel)); \ + (set) = get_rel_all_updated_cols((planInfo), (relInfo)); +#define BMS_LOOKUP_COL(set, attnum) bms_next_member((set), (attnum)) #else -#define RTE_UPDATED_COLS(a) (a)->modifiedCols +#define RTE_UPDATED_COLS(planInfo, resultRel, set) \ + RangeTblEntry *rte = planner_rt_fetch((resultRel), (planInfo)); \ + (set) = bms_copy(rte->modifiedCols); +#define BMS_LOOKUP_COL(set, attnum) bms_first_member((set)) #endif /* @@ -1434,12 +1453,82 @@ ifxPlanForeignModify(PlannerInfo *root, RelOptInfo *relInfo = root->simple_rel_array[resultRelation]; IfxCachedConnection *cached; IfxFdwExecutionState *scan_state; + IfxFdwPlanState *planState; /* - * Extract the state of the foreign scan. + * Check if there is a foreign scan state present. This would + * already have initialized all required objects on the foreign + * Informix server. + * + * In normal cases, ifxGetForeignRelSize() should already have + * initialized all required objects here, since we use the scan + * state to retrieve optimizer stats from Informix to get estimates + * based on the query back from the Informix server. + * + * This is not always true, so we need to check whether a foreign scan + * was already initialized or not (e.g. in the prepared statement + * case). Check if a private execution state was properly initialized, + * and if not, execute the required steps to initiate one ourselves. */ - scan_state = (IfxFdwExecutionState *) - ((IfxFdwPlanState *)relInfo->fdw_private)->state; + if (relInfo->fdw_private == NULL) { + + planState = palloc(sizeof(IfxFdwPlanState)); + + /* + * Establish remote informix connection or get + * a already cached connection from the informix connection + * cache. + */ + ifxSetupFdwScan(&coninfo, &scan_state, &plan_values, + rte->relid, IFX_PLAN_SCAN); + + /* + * Check for predicates that can be pushed down + * to the informix server, but skip it in case the user + * has set the disable_predicate_pushdown option... + */ + if (coninfo->predicate_pushdown) + { + /* + * Also save a list of excluded RestrictInfo structures not carrying any + * predicate found to be pushed down by ifxFilterQuals(). Those will + * passed later to ifxGetForeignPlan()... + */ + scan_state->stmt_info.predicate = ifxFilterQuals(root, relInfo, + &(planState->excl_restrictInfo), + rte->relid); + elog(DEBUG2, "predicate for pushdown: %s", scan_state->stmt_info.predicate); + } + else + { + elog(DEBUG2, "predicate pushdown disabled"); + scan_state->stmt_info.predicate = ""; + } + + /* + * Establish the remote query on the informix server. + * + * If we have an UPDATE or DELETE query, the foreign scan needs to + * employ an FOR UPDATE cursor, since we are going to reuse it + * during modify. + */ + if ((root->parse->commandType == CMD_UPDATE) + || (root->parse->commandType == CMD_DELETE)) + { + scan_state->stmt_info.cursorUsage = IFX_UPDATE_CURSOR; + } + + ifxPrepareScan(coninfo, scan_state); + + } else { + + /* + * Extract the state of the foreign scan. + */ + scan_state = (IfxFdwExecutionState *) + ((IfxFdwPlanState *)relInfo->fdw_private)->state; + + } /* * Don't reuse the connection info from the scan state, @@ -1490,8 +1579,23 @@ ifxPlanForeignModify(PlannerInfo *root, * cursor here feeded with the new values during ifxExecForeignInsert(). */ ifxSetupFdwScan(&coninfo, &state, &plan_values, rte->relid, IFX_PLAN_SCAN); + + /* + * ...don't forget the cursor name. + */ + state->stmt_info.cursor_name = ifxGenCursorName(state->stmt_info.refid); } + /* + * Check wether this foreign table has AFTER EACH ROW + * triggers attached. Currently this information is just + * for completeness, since we always include all columns + * in a foreign scan. + */ + ifxCheckForAfterRowTriggers(rte->relid, + state, + root->parse->commandType); + /* Sanity check, should not happen */ Assert((state != NULL) && (coninfo != NULL)); @@ -1519,9 +1623,10 @@ ifxPlanForeignModify(PlannerInfo *root, } /* - * Prepare and describe the statement. + * Generate a statement name for execution later. + * This is an unique statement identifier. */ - ifxPrepareModifyQuery(&state->stmt_info, coninfo, operation); + state->stmt_info.stmt_name = ifxGenStatementName(state->stmt_info.refid); /* * Serialize all required plan data for use in executor later. @@ -1542,11 +1647,6 @@ static void ifxPrepareModifyQuery(IfxStatementInfo *info, IfxConnectionInfo *coninfo, CmdType operation) { - /* - * Unique statement identifier. - */ - info->stmt_name = ifxGenStatementName(info->refid); - /* * Prepare the query. */ @@ -1560,11 +1660,6 @@ static void ifxPrepareModifyQuery(IfxStatementInfo *info, */ if (operation == CMD_INSERT) { - /* - * ...don't forget the cursor name. - */ - info->cursor_name = ifxGenCursorName(info->refid); - elog(DEBUG1, "declare cursor \"%s\" for statement \"%s\"", info->cursor_name, info->stmt_name); @@ -1656,6 +1751,11 @@ ifxBeginForeignModify(ModifyTableState *mstate, return; } + /* + * Prepare and describe the statement. + */ + ifxPrepareModifyQuery(&state->stmt_info, coninfo, mstate->operation); + /* * An INSERT action need to do much more preparing work * than UPDATE/DELETE: Since no foreign scan is involved, the @@ -1697,6 +1797,7 @@ ifxBeginForeignModify(ModifyTableState *mstate, else { /* CMD_UPDATE */ + state->stmt_info.descr_name = ifxGenDescrName(state->stmt_info.refid); ifxDescribeStmtInput(&state->stmt_info); } @@ -2043,13 +2144,15 @@ static void ifxPrepareParamsForModify(IfxFdwExecutionState *state, * * Shamelessly stolen from src/contrib/postgres_fdw. */ - RangeTblEntry *rte = planner_rt_fetch(resultRelation, planInfo); - Bitmapset *tmpset = bms_copy(RTE_UPDATED_COLS(rte)); + Bitmapset *tmpset = NULL; + int colnum = -1; AttrNumber col; - while ((col = bms_first_member(tmpset)) >= 0) + RTE_UPDATED_COLS(planInfo, resultRelation, tmpset); + + while ((colnum = BMS_LOOKUP_COL(tmpset, colnum)) >= 0) { - col += FirstLowInvalidHeapAttributeNumber; + col = colnum + FirstLowInvalidHeapAttributeNumber; if (col <= InvalidAttrNumber) /* shouldn't happen */ elog(ERROR, "system-column update is not supported"); state->affectedAttrNums = lappend_int(state->affectedAttrNums, @@ -3255,16 +3358,6 @@ static void ifxGetForeignRelSize(PlannerInfo *planInfo, ifxSetupFdwScan(&coninfo, &state, &plan_values, foreignTableId, IFX_PLAN_SCAN); - /* - * Check wether this foreign table has AFTER EACH ROW - * triggers attached. Currently this information is just - * for completeness, since we always include all columns - * in a foreign scan. - */ - ifxCheckForAfterRowTriggers(foreignTableId, - state, - planInfo->parse->commandType); - /* * Check for predicates that can be pushed down * to the informix server, but skip it in case the user diff --git a/sql/informix_fdw_tx.sql b/sql/informix_fdw_tx.sql index 15e5e47..0ca5b96 100644 --- a/sql/informix_fdw_tx.sql +++ b/sql/informix_fdw_tx.sql @@ -949,6 +949,207 @@ COMMIT; -- ALTER FOREIGN TABLE inttest OPTIONS(DROP disable_rowid); +-------------------------------------------------------------------------------- +-- Tests for PREPARE +-- +-- See discussion in github issue +-- https://github.com/credativ/informix_fdw/issues/31 +-------------------------------------------------------------------------------- + +-- +-- INSERT +-- + +BEGIN; + + +PREPARE ins_inttest(bigint) AS INSERT INTO inttest VALUES($1); + +EXECUTE ins_inttest (1); + +EXECUTE ins_inttest (2); + +EXECUTE ins_inttest (3); + +EXECUTE ins_inttest (4); + +EXECUTE ins_inttest (5); + +EXECUTE ins_inttest (6); + +EXECUTE ins_inttest (7); + +COMMIT; + +DEALLOCATE ins_inttest; + +-- +-- UPDATE +-- + +BEGIN; + +PREPARE upd_inttest(bigint) AS UPDATE inttest SET f1 = f1 WHERE f1 = $1; + +EXECUTE upd_inttest (1); + +EXECUTE upd_inttest (2); + +EXECUTE upd_inttest (3); + +EXECUTE upd_inttest (4); + +EXECUTE upd_inttest (5); + +EXECUTE upd_inttest (6); + +EXECUTE upd_inttest (7); + +COMMIT; + +DEALLOCATE upd_inttest; + +-- +-- DELETE +-- + +BEGIN; + +PREPARE del_inttest(bigint) AS DELETE FROM inttest WHERE f1 = $1; + +EXECUTE del_inttest (1); + +EXECUTE del_inttest (2); + +EXECUTE del_inttest (3); + +EXECUTE del_inttest (4); + +EXECUTE del_inttest (5); + +EXECUTE del_inttest (6); + +EXECUTE del_inttest (7); + +COMMIT; + +DEALLOCATE del_inttest; + +-------------------------------------------------------------------------------- +-- Trigger Tests +-------------------------------------------------------------------------------- +BEGIN; + +CREATE TABLE IF NOT EXISTS delete_fdw_trigger_test(id bigint primary key); + +-- +-- A before trigger testing before actions on INSERT/UPDATE/DELETE +-- on a foreign table +-- +CREATE OR REPLACE FUNCTION f_tg_test() +RETURNS trigger +LANGUAGE plpgsql +AS +$$ +BEGIN + IF TG_OP = 'DELETE' THEN + DELETE FROM inttest WHERE f1 = OLD.id; + RETURN OLD; + ELSIF TG_OP = 'UPDATE' THEN + UPDATE inttest SET f1 = NEW.id WHERE f1 = OLD.id; + RETURN NEW; + ELSIF TG_OP = 'INSERT' THEN + INSERT INTO inttest VALUES(NEW.id); + RETURN NEW; + ELSE + RAISE EXCEPTION 'unhandled trigger action %', TG_OP; + END IF; +END; +$$; + +-- +-- A broken trigger function referencing the wrong tuple identifiers +-- according to the trigger action (NEW vs. OLD) +-- +-- Basically the same as above. +-- +CREATE OR REPLACE FUNCTION f_tg_test_broken() +RETURNS trigger +LANGUAGE plpgsql +AS +$$ +BEGIN + IF TG_OP = 'DELETE' THEN + DELETE FROM inttest WHERE f1 = NEW.id; + RETURN OLD; + ELSIF TG_OP = 'UPDATE' THEN + UPDATE inttest SET f1 = NEW.id WHERE f1 = OLD.id; + RETURN NEW; + ELSIF TG_OP = 'INSERT' THEN + INSERT INTO inttest VALUES(OLD.id); + RETURN NEW; + ELSE + RAISE EXCEPTION 'unhandled trigger action %', TG_OP; + END IF; +END; +$$; + +CREATE TRIGGER tg_inttest +BEFORE DELETE OR UPDATE OR INSERT ON delete_fdw_trigger_test +FOR EACH ROW EXECUTE PROCEDURE f_tg_test(); + +TRUNCATE delete_fdw_trigger_test; +INSERT INTO delete_fdw_trigger_test VALUES(1), (2), (3); + +SELECT * FROM inttest; + +DELETE FROM delete_fdw_trigger_test WHERE id = 2; + +SELECT * FROM inttest; + +UPDATE delete_fdw_trigger_test SET id = 4 WHERE id = 3; + +SELECT * FROM inttest; + +INSERT INTO delete_fdw_trigger_test VALUES(5); + +SELECT * FROM inttest; + +DELETE FROM delete_fdw_trigger_test; + +SELECT * FROM inttest; + +DROP TRIGGER tg_inttest ON delete_fdw_trigger_test; + +CREATE TRIGGER tg_inttest +BEFORE DELETE OR UPDATE OR INSERT ON delete_fdw_trigger_test +FOR EACH ROW EXECUTE PROCEDURE f_tg_test_broken(); + +-- should fail +SAVEPOINT broken; +INSERT INTO delete_fdw_trigger_test VALUES(1), (2), (3); +ROLLBACK TO broken; + +SELECT * FROM inttest; + +-- should delete nothing +DELETE FROM delete_fdw_trigger_test WHERE id = 2; + +SELECT * FROM inttest; + +-- should update nothing +UPDATE delete_fdw_trigger_test SET id = 4 WHERE id = 3; + +SELECT * FROM inttest; + +DELETE FROM delete_fdw_trigger_test; + +SELECT * FROM inttest; + +DROP TRIGGER tg_inttest ON delete_fdw_trigger_test; + +COMMIT; + -------------------------------------------------------------------------------- -- Regression Tests End, Cleanup --------------------------------------------------------------------------------