Remove the refint extension.

refint was sample code from the pre-built-in-FK era and has long
been documented as superseded by the built-in foreign key
mechanism.  Recent fixes (see commits b0b6196386, 8cfbdf8f4d,
260e97733b, 611756948e, 1fbe2066dc, and 1541d91d1c) made it clear
that the code has more issues than its sample-code value justifies.

Author: Ayush Tiwari <ayushtiwari.slg01@gmail.com>
Reviewed-by: solai v <solai.cdac@gmail.com>
Reviewed-by: Álvaro Herrera <alvherre@kurilemu.de>
Reviewed-by: Daniel Gustafsson <daniel@yesql.se>
Reviewed-by: Fujii Masao <masao.fujii@gmail.com>
Discussion: https://postgr.es/m/CAJTYsWUHq8Ohc6-N-xamOPYz-q3qUYMtwQX-1%3DZi%3D5N1Q_GSEQ%40mail.gmail.com
This commit is contained in:
Nathan Bossart 2026-07-08 16:22:12 -05:00
parent 8e0c252753
commit 5e90e0914c
13 changed files with 45 additions and 995 deletions

View file

@ -1,23 +1,18 @@
# contrib/spi/Makefile
MODULES = autoinc insert_username moddatetime refint
MODULES = autoinc insert_username moddatetime
EXTENSION = autoinc insert_username moddatetime refint
EXTENSION = autoinc insert_username moddatetime
DATA = autoinc--1.0.sql \
insert_username--1.0.sql \
moddatetime--1.0.sql \
refint--1.0.sql
moddatetime--1.0.sql
PGFILEDESC = "spi - examples of using SPI and triggers"
REGRESS = autoinc refint
REGRESS = autoinc
DOCS = $(addsuffix .example, $(MODULES))
# this is needed for the regression tests;
# comment out if you want a quieter refint package for other uses
PG_CPPFLAGS = -DREFINT_VERBOSE
ifdef USE_PGXS
PG_CONFIG = pg_config
PGXS := $(shell $(PG_CONFIG) --pgxs)

View file

@ -1,113 +0,0 @@
CREATE EXTENSION refint;
create table pkeys (pkey1 int4 not null, pkey2 text not null);
create table fkeys (fkey1 int4, fkey2 text, fkey3 int);
create table fkeys2 (fkey21 int4, fkey22 text, pkey23 int not null);
create index fkeys_i on fkeys (fkey1, fkey2);
create index fkeys2_i on fkeys2 (fkey21, fkey22);
create index fkeys2p_i on fkeys2 (pkey23);
insert into pkeys values (10, '1');
insert into pkeys values (20, '2');
insert into pkeys values (30, '3');
insert into pkeys values (40, '4');
insert into pkeys values (50, '5');
insert into pkeys values (60, '6');
create unique index pkeys_i on pkeys (pkey1, pkey2);
--
-- For fkeys:
-- (fkey1, fkey2) --> pkeys (pkey1, pkey2)
-- (fkey3) --> fkeys2 (pkey23)
--
create trigger check_fkeys_pkey_exist
after insert or update on fkeys
for each row
execute function
check_primary_key ('fkey1', 'fkey2', 'pkeys', 'pkey1', 'pkey2');
create trigger check_fkeys_pkey2_exist
after insert or update on fkeys
for each row
execute function check_primary_key ('fkey3', 'fkeys2', 'pkey23');
--
-- For fkeys2:
-- (fkey21, fkey22) --> pkeys (pkey1, pkey2)
--
create trigger check_fkeys2_pkey_exist
after insert or update on fkeys2
for each row
execute procedure
check_primary_key ('fkey21', 'fkey22', 'pkeys', 'pkey1', 'pkey2');
--
-- For pkeys:
-- ON DELETE/UPDATE (pkey1, pkey2) CASCADE:
-- fkeys (fkey1, fkey2) and fkeys2 (fkey21, fkey22)
--
create trigger check_pkeys_fkey_cascade
after delete or update on pkeys
for each row
execute procedure
check_foreign_key (2, 'cascade', 'pkey1', 'pkey2',
'fkeys', 'fkey1', 'fkey2', 'fkeys2', 'fkey21', 'fkey22');
--
-- For fkeys2:
-- ON DELETE/UPDATE (pkey23) RESTRICT:
-- fkeys (fkey3)
--
create trigger check_fkeys2_fkey_restrict
after delete or update on fkeys2
for each row
execute procedure check_foreign_key (1, 'restrict', 'pkey23', 'fkeys', 'fkey3');
insert into fkeys2 values (10, '1', 1);
insert into fkeys2 values (30, '3', 2);
insert into fkeys2 values (40, '4', 5);
insert into fkeys2 values (50, '5', 3);
-- no key in pkeys
insert into fkeys2 values (70, '5', 3);
ERROR: tuple references non-existent key
DETAIL: Trigger "check_fkeys2_pkey_exist" found tuple referencing non-existent key in "pkeys".
insert into fkeys values (10, '1', 2);
insert into fkeys values (30, '3', 3);
insert into fkeys values (40, '4', 2);
insert into fkeys values (50, '5', 2);
-- no key in pkeys
insert into fkeys values (70, '5', 1);
ERROR: tuple references non-existent key
DETAIL: Trigger "check_fkeys_pkey_exist" found tuple referencing non-existent key in "pkeys".
-- no key in fkeys2
insert into fkeys values (60, '6', 4);
ERROR: tuple references non-existent key
DETAIL: Trigger "check_fkeys_pkey2_exist" found tuple referencing non-existent key in "fkeys2".
delete from pkeys where pkey1 = 30 and pkey2 = '3';
NOTICE: check_pkeys_fkey_cascade: 1 tuple(s) of fkeys are deleted
ERROR: "check_fkeys2_fkey_restrict": tuple is referenced in "fkeys"
CONTEXT: SQL statement "delete from fkeys2 where fkey21 = $1 and fkey22 = $2 "
delete from pkeys where pkey1 = 40 and pkey2 = '4';
NOTICE: check_pkeys_fkey_cascade: 1 tuple(s) of fkeys are deleted
NOTICE: check_pkeys_fkey_cascade: 1 tuple(s) of fkeys2 are deleted
update pkeys set pkey1 = 7, pkey2 = '70' where pkey1 = 50 and pkey2 = '5';
NOTICE: check_pkeys_fkey_cascade: 1 tuple(s) of fkeys are updated
NOTICE: check_pkeys_fkey_cascade: 1 tuple(s) of fkeys2 are updated
update pkeys set pkey1 = 7, pkey2 = '70' where pkey1 = 10 and pkey2 = '1';
ERROR: duplicate key value violates unique constraint "pkeys_i"
DETAIL: Key (pkey1, pkey2)=(7, 70) already exists.
SELECT trigger_name, event_manipulation, event_object_schema, event_object_table,
action_order, action_condition, action_orientation, action_timing,
action_reference_old_table, action_reference_new_table
FROM information_schema.triggers
WHERE event_object_table in ('pkeys', 'fkeys', 'fkeys2')
ORDER BY trigger_name COLLATE "C", 2;
trigger_name | event_manipulation | event_object_schema | event_object_table | action_order | action_condition | action_orientation | action_timing | action_reference_old_table | action_reference_new_table
----------------------------+--------------------+---------------------+--------------------+--------------+------------------+--------------------+---------------+----------------------------+----------------------------
check_fkeys2_fkey_restrict | DELETE | public | fkeys2 | 1 | | ROW | AFTER | |
check_fkeys2_fkey_restrict | UPDATE | public | fkeys2 | 1 | | ROW | AFTER | |
check_fkeys2_pkey_exist | INSERT | public | fkeys2 | 1 | | ROW | AFTER | |
check_fkeys2_pkey_exist | UPDATE | public | fkeys2 | 2 | | ROW | AFTER | |
check_fkeys_pkey2_exist | INSERT | public | fkeys | 1 | | ROW | AFTER | |
check_fkeys_pkey2_exist | UPDATE | public | fkeys | 1 | | ROW | AFTER | |
check_fkeys_pkey_exist | INSERT | public | fkeys | 2 | | ROW | AFTER | |
check_fkeys_pkey_exist | UPDATE | public | fkeys | 2 | | ROW | AFTER | |
check_pkeys_fkey_cascade | DELETE | public | pkeys | 1 | | ROW | AFTER | |
check_pkeys_fkey_cascade | UPDATE | public | pkeys | 1 | | ROW | AFTER | |
(10 rows)
DROP TABLE pkeys;
DROP TABLE fkeys;
DROP TABLE fkeys2;

View file

@ -79,35 +79,6 @@ install_data('moddatetime.example',
)
# this is needed for the regression tests;
# comment out if you want a quieter refint package for other uses
refint_cflags = ['-DREFINT_VERBOSE']
refint_sources = files(
'refint.c',
)
if host_system == 'windows'
refint_sources += rc_lib_gen.process(win32ver_rc, extra_args: [
'--NAME', 'refint',
'--FILEDESC', 'spi - examples of using SPI and triggers',])
endif
refint = shared_module('refint',
refint_sources,
c_args: refint_cflags,
kwargs: contrib_mod_args,
)
contrib_targets += refint
install_data('refint.control', 'refint--1.0.sql',
kwargs: contrib_data_args,
)
install_data('refint.example',
kwargs: contrib_doc_args,
)
tests += {
'name': 'spi',
'sd': meson.current_source_dir(),
@ -115,7 +86,6 @@ tests += {
'regress': {
'sql': [
'autoinc',
'refint',
],
},
}

View file

@ -1,14 +0,0 @@
/* contrib/spi/refint--1.0.sql */
-- complain if script is sourced in psql, rather than via CREATE EXTENSION
\echo Use "CREATE EXTENSION refint" to load this file. \quit
CREATE FUNCTION check_primary_key()
RETURNS trigger
AS 'MODULE_PATHNAME'
LANGUAGE C;
CREATE FUNCTION check_foreign_key()
RETURNS trigger
AS 'MODULE_PATHNAME'
LANGUAGE C;

View file

@ -1,538 +0,0 @@
/*
* contrib/spi/refint.c
*
*
* refint.c -- set of functions to define referential integrity
* constraints using general triggers.
*/
#include "postgres.h"
#include <ctype.h>
#include "commands/trigger.h"
#include "executor/spi.h"
#include "utils/builtins.h"
#include "utils/rel.h"
PG_MODULE_MAGIC_EXT(
.name = "refint",
.version = PG_VERSION
);
/*
* check_primary_key () -- check that key in tuple being inserted/updated
* references existing tuple in "primary" table.
* Though it's called without args You have to specify referenced
* table/keys while creating trigger: key field names in triggered table,
* referenced table name, referenced key field names:
* EXECUTE PROCEDURE
* check_primary_key ('Fkey1', 'Fkey2', 'Ptable', 'Pkey1', 'Pkey2').
*/
PG_FUNCTION_INFO_V1(check_primary_key);
Datum
check_primary_key(PG_FUNCTION_ARGS)
{
TriggerData *trigdata = (TriggerData *) fcinfo->context;
Trigger *trigger; /* to get trigger name */
int nargs; /* # of args specified in CREATE TRIGGER */
char **args; /* arguments: column names and table name */
int nkeys; /* # of key columns (= nargs / 2) */
Datum *kvals; /* key values */
char *relname; /* referenced relation name */
Relation rel; /* triggered relation */
HeapTuple tuple = NULL; /* tuple to return */
TupleDesc tupdesc; /* tuple description */
SPIPlanPtr pplan; /* prepared plan */
Oid *argtypes = NULL; /* key types to prepare execution plan */
bool isnull; /* to know is some column NULL or not */
int ret;
int i;
StringInfoData sql;
#ifdef DEBUG_QUERY
elog(DEBUG4, "check_primary_key: Enter Function");
#endif
/*
* Some checks first...
*/
/* Called by trigger manager ? */
if (!CALLED_AS_TRIGGER(fcinfo))
/* internal error */
elog(ERROR, "check_primary_key: not fired by trigger manager");
/* Should be called for ROW trigger */
if (!TRIGGER_FIRED_FOR_ROW(trigdata->tg_event))
/* internal error */
elog(ERROR, "check_primary_key: must be fired for row");
if (!TRIGGER_FIRED_AFTER(trigdata->tg_event))
/* internal error */
elog(ERROR, "check_primary_key: must be fired by AFTER trigger");
/* If INSERTion then must check Tuple to being inserted */
if (TRIGGER_FIRED_BY_INSERT(trigdata->tg_event))
tuple = trigdata->tg_trigtuple;
/* Not should be called for DELETE */
else if (TRIGGER_FIRED_BY_DELETE(trigdata->tg_event))
/* internal error */
elog(ERROR, "check_primary_key: cannot process DELETE events");
/* If UPDATE, then must check new Tuple, not old one */
else
tuple = trigdata->tg_newtuple;
trigger = trigdata->tg_trigger;
nargs = trigger->tgnargs;
args = trigger->tgargs;
if (nargs % 2 != 1) /* odd number of arguments! */
/* internal error */
elog(ERROR, "check_primary_key: odd number of arguments should be specified");
nkeys = nargs / 2;
relname = args[nkeys];
rel = trigdata->tg_relation;
tupdesc = rel->rd_att;
/* Connect to SPI manager */
SPI_connect();
/*
* We use SPI plan preparation feature, so allocate space to place key
* values.
*/
kvals = (Datum *) palloc(nkeys * sizeof(Datum));
/* allocate argtypes for preparation */
argtypes = (Oid *) palloc(nkeys * sizeof(Oid));
/* For each column in key ... */
for (i = 0; i < nkeys; i++)
{
/* get index of column in tuple */
int fnumber = SPI_fnumber(tupdesc, args[i]);
/* Bad guys may give us un-existing column in CREATE TRIGGER */
if (fnumber <= 0)
ereport(ERROR,
(errcode(ERRCODE_UNDEFINED_COLUMN),
errmsg("there is no attribute \"%s\" in relation \"%s\"",
args[i], SPI_getrelname(rel))));
/* Well, get binary (in internal format) value of column */
kvals[i] = SPI_getbinval(tuple, tupdesc, fnumber, &isnull);
/*
* If it's NULL then nothing to do! DON'T FORGET call SPI_finish ()!
* DON'T FORGET return tuple! Executor inserts tuple you're returning!
* If you return NULL then nothing will be inserted!
*/
if (isnull)
{
SPI_finish();
return PointerGetDatum(tuple);
}
/* Get typeId of column */
argtypes[i] = SPI_gettypeid(tupdesc, fnumber);
}
initStringInfo(&sql);
/*
* Construct query: SELECT 1 FROM _referenced_relation_ WHERE Pkey1 = $1
* [AND Pkey2 = $2 [...]]
*/
appendStringInfo(&sql, "select 1 from %s where ", relname);
for (i = 1; i <= nkeys; i++)
{
appendStringInfo(&sql, "%s = $%d ", args[i + nkeys], i);
if (i < nkeys)
appendStringInfoString(&sql, "and ");
}
/* Prepare plan for query */
pplan = SPI_prepare(sql.data, nkeys, argtypes);
if (pplan == NULL)
/* internal error */
elog(ERROR, "check_primary_key: SPI_prepare returned %s", SPI_result_code_string(SPI_result));
pfree(sql.data);
/*
* Ok, execute prepared plan.
*/
ret = SPI_execp(pplan, kvals, NULL, 1);
/* we have no NULLs - so we pass ^^^^ here */
if (ret < 0)
/* internal error */
elog(ERROR, "check_primary_key: SPI_execp returned %d", ret);
/*
* If there are no tuples returned by SELECT then ...
*/
if (SPI_processed == 0)
ereport(ERROR,
(errcode(ERRCODE_TRIGGERED_ACTION_EXCEPTION),
errmsg("tuple references non-existent key"),
errdetail("Trigger \"%s\" found tuple referencing non-existent key in \"%s\".", trigger->tgname, relname)));
SPI_finish();
return PointerGetDatum(tuple);
}
/*
* check_foreign_key () -- check that key in tuple being deleted/updated
* is not referenced by tuples in "foreign" table(s).
* Though it's called without args You have to specify (while creating trigger):
* number of references, action to do if key referenced
* ('restrict' | 'setnull' | 'cascade'), key field names in triggered
* ("primary") table and referencing table(s)/keys:
* EXECUTE PROCEDURE
* check_foreign_key (2, 'restrict', 'Pkey1', 'Pkey2',
* 'Ftable1', 'Fkey11', 'Fkey12', 'Ftable2', 'Fkey21', 'Fkey22').
*/
PG_FUNCTION_INFO_V1(check_foreign_key);
Datum
check_foreign_key(PG_FUNCTION_ARGS)
{
TriggerData *trigdata = (TriggerData *) fcinfo->context;
Trigger *trigger; /* to get trigger name */
int nargs; /* # of args specified in CREATE TRIGGER */
char **args; /* arguments: as described above */
char **args_temp;
int nrefs; /* number of references (== # of plans) */
char action; /* 'R'estrict | 'S'etnull | 'C'ascade */
int nkeys; /* # of key columns */
Datum *kvals; /* key values */
char *relname; /* referencing relation name */
Relation rel; /* triggered relation */
HeapTuple trigtuple = NULL; /* tuple to being changed */
HeapTuple newtuple = NULL; /* tuple to return */
TupleDesc tupdesc; /* tuple description */
SPIPlanPtr *splan; /* prepared plan(s) */
Oid *argtypes = NULL; /* key types to prepare execution plan */
bool isnull; /* to know is some column NULL or not */
bool isequal = true; /* are keys in both tuples equal (in UPDATE) */
int is_update = 0;
int ret;
int i,
r;
char **args2;
#ifdef DEBUG_QUERY
elog(DEBUG4, "check_foreign_key: Enter Function");
#endif
/*
* Some checks first...
*/
/* Called by trigger manager ? */
if (!CALLED_AS_TRIGGER(fcinfo))
/* internal error */
elog(ERROR, "check_foreign_key: not fired by trigger manager");
/* Should be called for ROW trigger */
if (!TRIGGER_FIRED_FOR_ROW(trigdata->tg_event))
/* internal error */
elog(ERROR, "check_foreign_key: must be fired for row");
/* Not should be called for INSERT */
if (TRIGGER_FIRED_BY_INSERT(trigdata->tg_event))
/* internal error */
elog(ERROR, "check_foreign_key: cannot process INSERT events");
if (!TRIGGER_FIRED_AFTER(trigdata->tg_event))
/* internal error */
elog(ERROR, "check_foreign_key: must be fired by AFTER trigger");
/* Have to check tg_trigtuple - tuple being deleted */
trigtuple = trigdata->tg_trigtuple;
/*
* But if this is UPDATE then we have to return tg_newtuple. Also, if key
* in tg_newtuple is the same as in tg_trigtuple then nothing to do.
*/
is_update = 0;
if (TRIGGER_FIRED_BY_UPDATE(trigdata->tg_event))
{
newtuple = trigdata->tg_newtuple;
is_update = 1;
}
trigger = trigdata->tg_trigger;
nargs = trigger->tgnargs;
args = trigger->tgargs;
if (nargs < 5) /* nrefs, action, key, Relation, key - at
* least */
/* internal error */
elog(ERROR, "check_foreign_key: too short %d (< 5) list of arguments", nargs);
nrefs = pg_strtoint32(args[0]);
if (nrefs < 1)
/* internal error */
elog(ERROR, "check_foreign_key: %d (< 1) number of references specified", nrefs);
action = pg_ascii_tolower((unsigned char) *(args[1]));
if (action != 'r' && action != 'c' && action != 's')
/* internal error */
elog(ERROR, "check_foreign_key: invalid action %s", args[1]);
nargs -= 2;
args += 2;
nkeys = (nargs - nrefs) / (nrefs + 1);
if (nkeys <= 0 || nargs != (nrefs + nkeys * (nrefs + 1)))
/* internal error */
elog(ERROR, "check_foreign_key: invalid number of arguments %d for %d references",
nargs + 2, nrefs);
rel = trigdata->tg_relation;
tupdesc = rel->rd_att;
/* Connect to SPI manager */
SPI_connect();
/*
* We use SPI plan preparation feature, so allocate space to place key
* values.
*/
kvals = (Datum *) palloc(nkeys * sizeof(Datum));
/* allocate argtypes for preparation */
argtypes = (Oid *) palloc(nkeys * sizeof(Oid));
/* For each column in key ... */
for (i = 0; i < nkeys; i++)
{
/* get index of column in tuple */
int fnumber = SPI_fnumber(tupdesc, args[i]);
/* Bad guys may give us un-existing column in CREATE TRIGGER */
if (fnumber <= 0)
ereport(ERROR,
(errcode(ERRCODE_UNDEFINED_COLUMN),
errmsg("there is no attribute \"%s\" in relation \"%s\"",
args[i], SPI_getrelname(rel))));
/* Well, get binary (in internal format) value of column */
kvals[i] = SPI_getbinval(trigtuple, tupdesc, fnumber, &isnull);
/*
* If it's NULL then nothing to do! DON'T FORGET call SPI_finish ()!
* DON'T FORGET return tuple! Executor inserts tuple you're returning!
* If you return NULL then nothing will be inserted!
*/
if (isnull)
{
SPI_finish();
return PointerGetDatum((newtuple == NULL) ? trigtuple : newtuple);
}
/*
* If UPDATE then get column value from new tuple being inserted and
* compare is this the same as old one. For the moment we use string
* presentation of values...
*/
if (newtuple != NULL)
{
char *oldval = SPI_getvalue(trigtuple, tupdesc, fnumber);
char *newval;
/* this shouldn't happen! SPI_ERROR_NOOUTFUNC ? */
if (oldval == NULL)
/* internal error */
elog(ERROR, "check_foreign_key: SPI_getvalue returned %s", SPI_result_code_string(SPI_result));
newval = SPI_getvalue(newtuple, tupdesc, fnumber);
if (newval == NULL || strcmp(oldval, newval) != 0)
isequal = false;
}
/* Get typeId of column */
argtypes[i] = SPI_gettypeid(tupdesc, fnumber);
}
args_temp = args;
nargs -= nkeys;
args += nkeys;
args2 = args;
splan = (SPIPlanPtr *) palloc(nrefs * sizeof(SPIPlanPtr));
for (r = 0; r < nrefs; r++)
{
StringInfoData sql;
SPIPlanPtr pplan;
initStringInfo(&sql);
relname = args2[0];
/*---------
* For 'R'estrict action we construct SELECT query:
*
* SELECT 1
* FROM _referencing_relation_
* WHERE Fkey1 = $1 [AND Fkey2 = $2 [...]]
*
* to check is tuple referenced or not.
*---------
*/
if (action == 'r')
appendStringInfo(&sql, "select 1 from %s where ", relname);
/*---------
* For 'C'ascade action we construct DELETE query
*
* DELETE
* FROM _referencing_relation_
* WHERE Fkey1 = $1 [AND Fkey2 = $2 [...]]
*
* to delete all referencing tuples.
*---------
*/
/*
* Max : Cascade with UPDATE query i create update query that updates
* new key values in referenced tables
*/
else if (action == 'c')
{
if (is_update == 1)
{
int fn;
char *nv;
int k;
appendStringInfo(&sql, "update %s set ", relname);
for (k = 1; k <= nkeys; k++)
{
fn = SPI_fnumber(tupdesc, args_temp[k - 1]);
Assert(fn > 0); /* already checked above */
nv = SPI_getvalue(newtuple, tupdesc, fn);
appendStringInfo(&sql, " %s = %s ",
args2[k],
nv ? quote_literal_cstr(nv) : "NULL");
if (k < nkeys)
appendStringInfoString(&sql, ", ");
}
appendStringInfoString(&sql, " where ");
}
else
/* DELETE */
appendStringInfo(&sql, "delete from %s where ", relname);
}
/*
* For 'S'etnull action we construct UPDATE query - UPDATE
* _referencing_relation_ SET Fkey1 null [, Fkey2 null [...]] WHERE
* Fkey1 = $1 [AND Fkey2 = $2 [...]] - to set key columns in all
* referencing tuples to NULL.
*/
else if (action == 's')
{
appendStringInfo(&sql, "update %s set ", relname);
for (i = 1; i <= nkeys; i++)
{
appendStringInfo(&sql, "%s = null", args2[i]);
if (i < nkeys)
appendStringInfoString(&sql, ", ");
}
appendStringInfoString(&sql, " where ");
}
/* Construct WHERE qual */
for (i = 1; i <= nkeys; i++)
{
appendStringInfo(&sql, "%s = $%d ", args2[i], i);
if (i < nkeys)
appendStringInfoString(&sql, "and ");
}
/* Prepare plan for query */
pplan = SPI_prepare(sql.data, nkeys, argtypes);
if (pplan == NULL)
/* internal error */
elog(ERROR, "check_foreign_key: SPI_prepare returned %s", SPI_result_code_string(SPI_result));
splan[r] = pplan;
args2 += nkeys + 1; /* to the next relation */
#ifdef DEBUG_QUERY
elog(DEBUG4, "check_foreign_key Debug Query is : %s ", sql.data);
#endif
pfree(sql.data);
}
/*
* If UPDATE and key is not changed ...
*/
if (newtuple != NULL && isequal)
{
SPI_finish();
return PointerGetDatum(newtuple);
}
/*
* Ok, execute prepared plan(s).
*/
for (r = 0; r < nrefs; r++)
{
/*
* For 'R'estrict we may to execute plan for one tuple only, for other
* actions - for all tuples.
*/
int tcount = (action == 'r') ? 1 : 0;
relname = args[0];
ret = SPI_execp(splan[r], kvals, NULL, tcount);
/* we have no NULLs - so we pass ^^^^ here */
if (ret < 0)
ereport(ERROR,
(errcode(ERRCODE_TRIGGERED_ACTION_EXCEPTION),
errmsg("SPI_execp returned %d", ret)));
/* If action is 'R'estrict ... */
if (action == 'r')
{
/* If there is tuple returned by SELECT then ... */
if (SPI_processed > 0)
ereport(ERROR,
(errcode(ERRCODE_TRIGGERED_ACTION_EXCEPTION),
errmsg("\"%s\": tuple is referenced in \"%s\"",
trigger->tgname, relname)));
}
else
{
#ifdef REFINT_VERBOSE
const char *operation;
if (action == 'c')
operation = is_update ? "updated" : "deleted";
else
operation = "set to null";
elog(NOTICE, "%s: " UINT64_FORMAT " tuple(s) of %s are %s",
trigger->tgname, SPI_processed, relname, operation);
#endif
}
args += nkeys + 1; /* to the next relation */
}
SPI_finish();
return PointerGetDatum((newtuple == NULL) ? trigtuple : newtuple);
}

View file

@ -1,5 +0,0 @@
# refint extension
comment = 'functions for implementing referential integrity (obsolete)'
default_version = '1.0'
module_pathname = '$libdir/refint'
relocatable = true

View file

@ -1,82 +0,0 @@
--Column ID of table A is primary key:
CREATE TABLE A (
ID int4 not null
);
CREATE UNIQUE INDEX AI ON A (ID);
--Columns REFB of table B and REFC of C are foreign keys referencing ID of A:
CREATE TABLE B (
REFB int4
);
CREATE INDEX BI ON B (REFB);
CREATE TABLE C (
REFC int4
);
CREATE INDEX CI ON C (REFC);
--Trigger for table A:
CREATE TRIGGER AT BEFORE DELETE OR UPDATE ON A FOR EACH ROW
EXECUTE PROCEDURE
check_foreign_key (2, 'cascade', 'ID', 'B', 'REFB', 'C', 'REFC');
/*
2 - means that check must be performed for foreign keys of 2 tables.
cascade - defines that corresponding keys must be deleted.
ID - name of primary key column in triggered table (A). You may
use as many columns as you need.
B - name of (first) table with foreign keys.
REFB - name of foreign key column in this table. You may use as many
columns as you need, but number of key columns in referenced
table (A) must be the same.
C - name of second table with foreign keys.
REFC - name of foreign key column in this table.
*/
--Trigger for table B:
CREATE TRIGGER BT BEFORE INSERT OR UPDATE ON B FOR EACH ROW
EXECUTE PROCEDURE
check_primary_key ('REFB', 'A', 'ID');
/*
REFB - name of foreign key column in triggered (B) table. You may use as
many columns as you need, but number of key columns in referenced
table must be the same.
A - referenced table name.
ID - name of primary key column in referenced table.
*/
--Trigger for table C:
CREATE TRIGGER CT BEFORE INSERT OR UPDATE ON C FOR EACH ROW
EXECUTE PROCEDURE
check_primary_key ('REFC', 'A', 'ID');
-- Now try
INSERT INTO A VALUES (10);
INSERT INTO A VALUES (20);
INSERT INTO A VALUES (30);
INSERT INTO A VALUES (40);
INSERT INTO A VALUES (50);
INSERT INTO B VALUES (1); -- invalid reference
INSERT INTO B VALUES (10);
INSERT INTO B VALUES (30);
INSERT INTO B VALUES (30);
INSERT INTO C VALUES (11); -- invalid reference
INSERT INTO C VALUES (20);
INSERT INTO C VALUES (20);
INSERT INTO C VALUES (30);
DELETE FROM A WHERE ID = 10;
DELETE FROM A WHERE ID = 20;
DELETE FROM A WHERE ID = 30;
SELECT * FROM A;
SELECT * FROM B;
SELECT * FROM C;

View file

@ -1,97 +0,0 @@
CREATE EXTENSION refint;
create table pkeys (pkey1 int4 not null, pkey2 text not null);
create table fkeys (fkey1 int4, fkey2 text, fkey3 int);
create table fkeys2 (fkey21 int4, fkey22 text, pkey23 int not null);
create index fkeys_i on fkeys (fkey1, fkey2);
create index fkeys2_i on fkeys2 (fkey21, fkey22);
create index fkeys2p_i on fkeys2 (pkey23);
insert into pkeys values (10, '1');
insert into pkeys values (20, '2');
insert into pkeys values (30, '3');
insert into pkeys values (40, '4');
insert into pkeys values (50, '5');
insert into pkeys values (60, '6');
create unique index pkeys_i on pkeys (pkey1, pkey2);
--
-- For fkeys:
-- (fkey1, fkey2) --> pkeys (pkey1, pkey2)
-- (fkey3) --> fkeys2 (pkey23)
--
create trigger check_fkeys_pkey_exist
after insert or update on fkeys
for each row
execute function
check_primary_key ('fkey1', 'fkey2', 'pkeys', 'pkey1', 'pkey2');
create trigger check_fkeys_pkey2_exist
after insert or update on fkeys
for each row
execute function check_primary_key ('fkey3', 'fkeys2', 'pkey23');
--
-- For fkeys2:
-- (fkey21, fkey22) --> pkeys (pkey1, pkey2)
--
create trigger check_fkeys2_pkey_exist
after insert or update on fkeys2
for each row
execute procedure
check_primary_key ('fkey21', 'fkey22', 'pkeys', 'pkey1', 'pkey2');
--
-- For pkeys:
-- ON DELETE/UPDATE (pkey1, pkey2) CASCADE:
-- fkeys (fkey1, fkey2) and fkeys2 (fkey21, fkey22)
--
create trigger check_pkeys_fkey_cascade
after delete or update on pkeys
for each row
execute procedure
check_foreign_key (2, 'cascade', 'pkey1', 'pkey2',
'fkeys', 'fkey1', 'fkey2', 'fkeys2', 'fkey21', 'fkey22');
--
-- For fkeys2:
-- ON DELETE/UPDATE (pkey23) RESTRICT:
-- fkeys (fkey3)
--
create trigger check_fkeys2_fkey_restrict
after delete or update on fkeys2
for each row
execute procedure check_foreign_key (1, 'restrict', 'pkey23', 'fkeys', 'fkey3');
insert into fkeys2 values (10, '1', 1);
insert into fkeys2 values (30, '3', 2);
insert into fkeys2 values (40, '4', 5);
insert into fkeys2 values (50, '5', 3);
-- no key in pkeys
insert into fkeys2 values (70, '5', 3);
insert into fkeys values (10, '1', 2);
insert into fkeys values (30, '3', 3);
insert into fkeys values (40, '4', 2);
insert into fkeys values (50, '5', 2);
-- no key in pkeys
insert into fkeys values (70, '5', 1);
-- no key in fkeys2
insert into fkeys values (60, '6', 4);
delete from pkeys where pkey1 = 30 and pkey2 = '3';
delete from pkeys where pkey1 = 40 and pkey2 = '4';
update pkeys set pkey1 = 7, pkey2 = '70' where pkey1 = 50 and pkey2 = '5';
update pkeys set pkey1 = 7, pkey2 = '70' where pkey1 = 10 and pkey2 = '1';
SELECT trigger_name, event_manipulation, event_object_schema, event_object_table,
action_order, action_condition, action_orientation, action_timing,
action_reference_old_table, action_reference_new_table
FROM information_schema.triggers
WHERE event_object_table in ('pkeys', 'fkeys', 'fkeys2')
ORDER BY trigger_name COLLATE "C", 2;
DROP TABLE pkeys;
DROP TABLE fkeys;
DROP TABLE fkeys2;

View file

@ -0,0 +1,24 @@
<!-- doc/src/sgml/appendix-obsolete-refint.sgml -->
<!--
See doc/src/sgml/appendix-obsolete.sgml for why this file exists. Do not change the id attribute.
-->
<sect1 id="contrib-spi-refint">
<title>refint Extension Removed</title>
<indexterm zone="contrib-spi-refint">
<primary>refint</primary>
</indexterm>
<para>
<productname>PostgreSQL</productname> 19 and below shipped an extension
named <literal>refint</literal> (part of the <literal>spi</literal> contrib
module) that provided the trigger functions
<function>check_primary_key</function> and
<function>check_foreign_key</function> as an early way to enforce
referential integrity. This functionality was long superseded by the
built-in foreign key mechanism (see <xref linkend="ddl-constraints-fk"/>),
and the extension was removed in <productname>PostgreSQL</productname> 20.
</para>
</sect1>

View file

@ -39,5 +39,6 @@
&obsolete-pgresetxlog;
&obsolete-pgreceivexlog;
&obsolete-auth-radius;
&obsolete-refint;
</appendix>

View file

@ -24,113 +24,6 @@
separately-installable extension.
</para>
<sect2 id="contrib-spi-refint">
<title>refint &mdash; Functions for Implementing Referential Integrity</title>
<para>
<function>check_primary_key()</function> and
<function>check_foreign_key()</function> are used to check foreign key constraints.
(This functionality is long since superseded by the built-in foreign
key mechanism, of course, but the module is still useful as an example.)
</para>
<note>
<para>
<filename>refint</filename> requires a
<link linkend="ddl-schemas-patterns">secure schema usage pattern</link> and
data types where the equality operator is named <literal>=</literal>.
</para>
</note>
<para>
<function>check_primary_key()</function> checks the referencing table.
To use, create an <literal>AFTER INSERT OR UPDATE</literal> trigger using this
function on a table referencing another table. Specify as the trigger
arguments: the referencing table's column name(s) which form the foreign
key, the referenced table name, and the column names in the referenced table
which form the primary/unique key. To handle multiple foreign
keys, create a trigger for each reference.
</para>
<note>
<para>
The <emphasis>referenced</emphasis> table name and column name arguments to
<function>check_primary_key()</function> are copied as-is into internally
generated SQL statements and therefore must be double-quoted by the user as
necessary in the <command>CREATE TRIGGER</command> command. See
<xref linkend="sql-syntax-identifiers"/> for more information about quoting
SQL identifiers. Conversely, the <emphasis>referencing</emphasis> table
column name arguments should not be double quoted. See the following mock
example of proper use of <function>check_primary_key()</function>:
<programlisting>
CREATE TRIGGER mytrigger
AFTER INSERT OR UPDATE ON referencing_table
FOR EACH ROW EXECUTE PROCEDURE
check_primary_key (
'column A', 'column B', -- referencing table columns
'myschema."referenced table"', -- referenced table
'"column A"', '"column B"' -- referenced table columns
);
</programlisting>
</para>
</note>
<para>
<function>check_foreign_key()</function> checks the referenced table.
To use, create an <literal>AFTER DELETE OR UPDATE</literal> trigger using this
function on a table referenced by other table(s). Specify as the trigger
arguments: the number of referencing tables for which the function has to
perform checking, the action if a referencing key is found
(<literal>cascade</literal> &mdash; to delete the referencing row,
<literal>restrict</literal> &mdash; to abort transaction if referencing keys
exist, <literal>setnull</literal> &mdash; to set referencing key fields to null),
the referenced table's column names which form the primary/unique key, then
the referencing table name and column names (repeated for as many
referencing tables as were specified by first argument). Note that the
primary/unique key columns should be marked NOT NULL and should have a
unique index.
</para>
<note>
<para>
The <emphasis>referencing</emphasis> table name and column name arguments
to <function>check_foreign_key()</function> are copied as-is into
internally generated SQL statements and therefore must be double-quoted by
the user as necessary in the <command>CREATE TRIGGER</command> command.
See <xref linkend="sql-syntax-identifiers"/> for more information about
quoting SQL identifiers. Conversely, the <emphasis>referenced</emphasis>
table column name arguments should not be double quoted. See the following
mock example of proper use of <function>check_foreign_key()</function>:
<programlisting>
CREATE TRIGGER mytrigger
AFTER DELETE OR UPDATE ON referenced_table
FOR EACH ROW EXECUTE PROCEDURE
check_foreign_key (
1, -- number of referencing tables
'cascade', -- action
'column A', 'column B', -- referenced table columns
'myschema."referencing table"', -- referencing table
'"column A"', '"column B"' -- referencing table columns
);
</programlisting>
</para>
</note>
<para>
Note that if these triggers are executed from
another <literal>BEFORE</literal> trigger, they can fail unexpectedly. For
example, if a user inserts row1 and then the <literal>BEFORE</literal>
trigger inserts row2 and calls a trigger with the
<function>check_foreign_key()</function>,
the <function>check_foreign_key()</function>
function will not see row1 and will fail.
</para>
<para>
There are examples in <filename>refint.example</filename>.
</para>
</sect2>
<sect2 id="contrib-spi-autoinc">
<title>autoinc &mdash; Functions for Autoincrementing Fields</title>

View file

@ -209,3 +209,4 @@
<!ENTITY obsolete-pgresetxlog SYSTEM "appendix-obsolete-pgresetxlog.sgml">
<!ENTITY obsolete-pgreceivexlog SYSTEM "appendix-obsolete-pgreceivexlog.sgml">
<!ENTITY obsolete-auth-radius SYSTEM "appendix-obsolete-auth-radius.sgml">
<!ENTITY obsolete-refint SYSTEM "appendix-obsolete-refint.sgml">

View file

@ -112,6 +112,21 @@ sub adjust_database_contents
'drop extension if exists test_ext7');
}
# refint was removed in v20
if ($old_version < 20)
{
if ($dbnames{"contrib_regression_autoinc"})
{
_add_st($result, 'contrib_regression_autoinc',
'drop extension if exists refint cascade');
}
if ($dbnames{"regression_spi"})
{
_add_st($result, 'regression_spi',
'drop extension if exists refint cascade');
}
}
# btree_gist inet/cidr indexes cannot be upgraded to v19
if ($old_version < 19)
{