mirror of
https://github.com/postgres/postgres.git
synced 2025-07-17 06:41:09 +03:00
Generated columns
This is an SQL-standard feature that allows creating columns that are computed from expressions rather than assigned, similar to a view or materialized view but on a column basis. This implements one kind of generated column: stored (computed on write). Another kind, virtual (computed on read), is planned for the future, and some room is left for it. Reviewed-by: Michael Paquier <michael@paquier.xyz> Reviewed-by: Pavel Stehule <pavel.stehule@gmail.com> Discussion: https://www.postgresql.org/message-id/flat/b151f851-4019-bdb1-699e-ebab07d2f40a@2ndquadrant.com
This commit is contained in:
@ -67,6 +67,10 @@ SELECT * FROM users;
|
||||
-- dump trigger data
|
||||
CREATE TABLE trigger_test
|
||||
(i int, v text );
|
||||
CREATE TABLE trigger_test_generated (
|
||||
i int,
|
||||
j int GENERATED ALWAYS AS (i * 2) STORED
|
||||
);
|
||||
CREATE FUNCTION trigger_data() RETURNS trigger LANGUAGE plpythonu AS $$
|
||||
|
||||
if 'relid' in TD:
|
||||
@ -203,6 +207,77 @@ NOTICE: TD[when] => BEFORE
|
||||
DROP TRIGGER show_trigger_data_trig_stmt on trigger_test;
|
||||
DROP TRIGGER show_trigger_data_trig_before on trigger_test;
|
||||
DROP TRIGGER show_trigger_data_trig_after on trigger_test;
|
||||
CREATE TRIGGER show_trigger_data_trig_before
|
||||
BEFORE INSERT OR UPDATE OR DELETE ON trigger_test_generated
|
||||
FOR EACH ROW EXECUTE PROCEDURE trigger_data();
|
||||
CREATE TRIGGER show_trigger_data_trig_after
|
||||
AFTER INSERT OR UPDATE OR DELETE ON trigger_test_generated
|
||||
FOR EACH ROW EXECUTE PROCEDURE trigger_data();
|
||||
insert into trigger_test_generated (i) values (1);
|
||||
NOTICE: TD[args] => None
|
||||
NOTICE: TD[event] => INSERT
|
||||
NOTICE: TD[level] => ROW
|
||||
NOTICE: TD[name] => show_trigger_data_trig_before
|
||||
NOTICE: TD[new] => {'i': 1}
|
||||
NOTICE: TD[old] => None
|
||||
NOTICE: TD[relid] => bogus:12345
|
||||
NOTICE: TD[table_name] => trigger_test_generated
|
||||
NOTICE: TD[table_schema] => public
|
||||
NOTICE: TD[when] => BEFORE
|
||||
NOTICE: TD[args] => None
|
||||
NOTICE: TD[event] => INSERT
|
||||
NOTICE: TD[level] => ROW
|
||||
NOTICE: TD[name] => show_trigger_data_trig_after
|
||||
NOTICE: TD[new] => {'i': 1, 'j': 2}
|
||||
NOTICE: TD[old] => None
|
||||
NOTICE: TD[relid] => bogus:12345
|
||||
NOTICE: TD[table_name] => trigger_test_generated
|
||||
NOTICE: TD[table_schema] => public
|
||||
NOTICE: TD[when] => AFTER
|
||||
update trigger_test_generated set i = 11 where i = 1;
|
||||
NOTICE: TD[args] => None
|
||||
NOTICE: TD[event] => UPDATE
|
||||
NOTICE: TD[level] => ROW
|
||||
NOTICE: TD[name] => show_trigger_data_trig_before
|
||||
NOTICE: TD[new] => {'i': 11}
|
||||
NOTICE: TD[old] => {'i': 1, 'j': 2}
|
||||
NOTICE: TD[relid] => bogus:12345
|
||||
NOTICE: TD[table_name] => trigger_test_generated
|
||||
NOTICE: TD[table_schema] => public
|
||||
NOTICE: TD[when] => BEFORE
|
||||
NOTICE: TD[args] => None
|
||||
NOTICE: TD[event] => UPDATE
|
||||
NOTICE: TD[level] => ROW
|
||||
NOTICE: TD[name] => show_trigger_data_trig_after
|
||||
NOTICE: TD[new] => {'i': 11, 'j': 22}
|
||||
NOTICE: TD[old] => {'i': 1, 'j': 2}
|
||||
NOTICE: TD[relid] => bogus:12345
|
||||
NOTICE: TD[table_name] => trigger_test_generated
|
||||
NOTICE: TD[table_schema] => public
|
||||
NOTICE: TD[when] => AFTER
|
||||
delete from trigger_test_generated;
|
||||
NOTICE: TD[args] => None
|
||||
NOTICE: TD[event] => DELETE
|
||||
NOTICE: TD[level] => ROW
|
||||
NOTICE: TD[name] => show_trigger_data_trig_before
|
||||
NOTICE: TD[new] => None
|
||||
NOTICE: TD[old] => {'i': 11, 'j': 22}
|
||||
NOTICE: TD[relid] => bogus:12345
|
||||
NOTICE: TD[table_name] => trigger_test_generated
|
||||
NOTICE: TD[table_schema] => public
|
||||
NOTICE: TD[when] => BEFORE
|
||||
NOTICE: TD[args] => None
|
||||
NOTICE: TD[event] => DELETE
|
||||
NOTICE: TD[level] => ROW
|
||||
NOTICE: TD[name] => show_trigger_data_trig_after
|
||||
NOTICE: TD[new] => None
|
||||
NOTICE: TD[old] => {'i': 11, 'j': 22}
|
||||
NOTICE: TD[relid] => bogus:12345
|
||||
NOTICE: TD[table_name] => trigger_test_generated
|
||||
NOTICE: TD[table_schema] => public
|
||||
NOTICE: TD[when] => AFTER
|
||||
DROP TRIGGER show_trigger_data_trig_before ON trigger_test_generated;
|
||||
DROP TRIGGER show_trigger_data_trig_after ON trigger_test_generated;
|
||||
insert into trigger_test values(1,'insert');
|
||||
CREATE VIEW trigger_test_view AS SELECT * FROM trigger_test;
|
||||
CREATE TRIGGER show_trigger_data_trig
|
||||
@ -524,3 +599,22 @@ INFO: old: 1 -> a
|
||||
INFO: new: 1 -> b
|
||||
DROP TABLE transition_table_test;
|
||||
DROP FUNCTION transition_table_test_f();
|
||||
-- dealing with generated columns
|
||||
CREATE FUNCTION generated_test_func1() RETURNS trigger
|
||||
LANGUAGE plpythonu
|
||||
AS $$
|
||||
TD['new']['j'] = 5 # not allowed
|
||||
return 'MODIFY'
|
||||
$$;
|
||||
CREATE TRIGGER generated_test_trigger1 BEFORE INSERT ON trigger_test_generated
|
||||
FOR EACH ROW EXECUTE PROCEDURE generated_test_func1();
|
||||
TRUNCATE trigger_test_generated;
|
||||
INSERT INTO trigger_test_generated (i) VALUES (1);
|
||||
ERROR: cannot set generated column "j"
|
||||
CONTEXT: while modifying trigger row
|
||||
PL/Python function "generated_test_func1"
|
||||
SELECT * FROM trigger_test_generated;
|
||||
i | j
|
||||
---+---
|
||||
(0 rows)
|
||||
|
||||
|
@ -357,7 +357,7 @@ PLy_cursor_iternext(PyObject *self)
|
||||
exec_ctx->curr_proc);
|
||||
|
||||
ret = PLy_input_from_tuple(&cursor->result, SPI_tuptable->vals[0],
|
||||
SPI_tuptable->tupdesc);
|
||||
SPI_tuptable->tupdesc, true);
|
||||
}
|
||||
|
||||
SPI_freetuptable(SPI_tuptable);
|
||||
@ -453,7 +453,8 @@ PLy_cursor_fetch(PyObject *self, PyObject *args)
|
||||
{
|
||||
PyObject *row = PLy_input_from_tuple(&cursor->result,
|
||||
SPI_tuptable->vals[i],
|
||||
SPI_tuptable->tupdesc);
|
||||
SPI_tuptable->tupdesc,
|
||||
true);
|
||||
|
||||
PyList_SetItem(ret->rows, i, row);
|
||||
}
|
||||
|
@ -13,6 +13,7 @@
|
||||
#include "executor/spi.h"
|
||||
#include "funcapi.h"
|
||||
#include "utils/builtins.h"
|
||||
#include "utils/lsyscache.h"
|
||||
#include "utils/rel.h"
|
||||
#include "utils/typcache.h"
|
||||
|
||||
@ -751,6 +752,11 @@ PLy_trigger_build_args(FunctionCallInfo fcinfo, PLyProcedure *proc, HeapTuple *r
|
||||
PyDict_SetItemString(pltdata, "level", pltlevel);
|
||||
Py_DECREF(pltlevel);
|
||||
|
||||
/*
|
||||
* Note: In BEFORE trigger, stored generated columns are not computed yet,
|
||||
* so don't make them accessible in NEW row.
|
||||
*/
|
||||
|
||||
if (TRIGGER_FIRED_BY_INSERT(tdata->tg_event))
|
||||
{
|
||||
pltevent = PyString_FromString("INSERT");
|
||||
@ -758,7 +764,8 @@ PLy_trigger_build_args(FunctionCallInfo fcinfo, PLyProcedure *proc, HeapTuple *r
|
||||
PyDict_SetItemString(pltdata, "old", Py_None);
|
||||
pytnew = PLy_input_from_tuple(&proc->result_in,
|
||||
tdata->tg_trigtuple,
|
||||
rel_descr);
|
||||
rel_descr,
|
||||
!TRIGGER_FIRED_BEFORE(tdata->tg_event));
|
||||
PyDict_SetItemString(pltdata, "new", pytnew);
|
||||
Py_DECREF(pytnew);
|
||||
*rv = tdata->tg_trigtuple;
|
||||
@ -770,7 +777,8 @@ PLy_trigger_build_args(FunctionCallInfo fcinfo, PLyProcedure *proc, HeapTuple *r
|
||||
PyDict_SetItemString(pltdata, "new", Py_None);
|
||||
pytold = PLy_input_from_tuple(&proc->result_in,
|
||||
tdata->tg_trigtuple,
|
||||
rel_descr);
|
||||
rel_descr,
|
||||
true);
|
||||
PyDict_SetItemString(pltdata, "old", pytold);
|
||||
Py_DECREF(pytold);
|
||||
*rv = tdata->tg_trigtuple;
|
||||
@ -781,12 +789,14 @@ PLy_trigger_build_args(FunctionCallInfo fcinfo, PLyProcedure *proc, HeapTuple *r
|
||||
|
||||
pytnew = PLy_input_from_tuple(&proc->result_in,
|
||||
tdata->tg_newtuple,
|
||||
rel_descr);
|
||||
rel_descr,
|
||||
!TRIGGER_FIRED_BEFORE(tdata->tg_event));
|
||||
PyDict_SetItemString(pltdata, "new", pytnew);
|
||||
Py_DECREF(pytnew);
|
||||
pytold = PLy_input_from_tuple(&proc->result_in,
|
||||
tdata->tg_trigtuple,
|
||||
rel_descr);
|
||||
rel_descr,
|
||||
true);
|
||||
PyDict_SetItemString(pltdata, "old", pytold);
|
||||
Py_DECREF(pytold);
|
||||
*rv = tdata->tg_newtuple;
|
||||
@ -952,6 +962,11 @@ PLy_modify_tuple(PLyProcedure *proc, PyObject *pltd, TriggerData *tdata,
|
||||
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
|
||||
errmsg("cannot set system attribute \"%s\"",
|
||||
plattstr)));
|
||||
if (TupleDescAttr(tupdesc, attn - 1)->attgenerated)
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_E_R_I_E_TRIGGER_PROTOCOL_VIOLATED),
|
||||
errmsg("cannot set generated column \"%s\"",
|
||||
plattstr)));
|
||||
|
||||
plval = PyDict_GetItem(plntup, platt);
|
||||
if (plval == NULL)
|
||||
|
@ -419,7 +419,8 @@ PLy_spi_execute_fetch_result(SPITupleTable *tuptable, uint64 rows, int status)
|
||||
{
|
||||
PyObject *row = PLy_input_from_tuple(&ininfo,
|
||||
tuptable->vals[i],
|
||||
tuptable->tupdesc);
|
||||
tuptable->tupdesc,
|
||||
true);
|
||||
|
||||
PyList_SetItem(result->rows, i, row);
|
||||
}
|
||||
|
@ -41,7 +41,7 @@ static PyObject *PLyList_FromArray(PLyDatumToOb *arg, Datum d);
|
||||
static PyObject *PLyList_FromArray_recurse(PLyDatumToOb *elm, int *dims, int ndim, int dim,
|
||||
char **dataptr_p, bits8 **bitmap_p, int *bitmask_p);
|
||||
static PyObject *PLyDict_FromComposite(PLyDatumToOb *arg, Datum d);
|
||||
static PyObject *PLyDict_FromTuple(PLyDatumToOb *arg, HeapTuple tuple, TupleDesc desc);
|
||||
static PyObject *PLyDict_FromTuple(PLyDatumToOb *arg, HeapTuple tuple, TupleDesc desc, bool include_generated);
|
||||
|
||||
/* conversion from Python objects to Datums */
|
||||
static Datum PLyObject_ToBool(PLyObToDatum *arg, PyObject *plrv,
|
||||
@ -134,7 +134,7 @@ PLy_output_convert(PLyObToDatum *arg, PyObject *val, bool *isnull)
|
||||
* but in practice all callers have the right tupdesc available.
|
||||
*/
|
||||
PyObject *
|
||||
PLy_input_from_tuple(PLyDatumToOb *arg, HeapTuple tuple, TupleDesc desc)
|
||||
PLy_input_from_tuple(PLyDatumToOb *arg, HeapTuple tuple, TupleDesc desc, bool include_generated)
|
||||
{
|
||||
PyObject *dict;
|
||||
PLyExecutionContext *exec_ctx = PLy_current_execution_context();
|
||||
@ -148,7 +148,7 @@ PLy_input_from_tuple(PLyDatumToOb *arg, HeapTuple tuple, TupleDesc desc)
|
||||
|
||||
oldcontext = MemoryContextSwitchTo(scratch_context);
|
||||
|
||||
dict = PLyDict_FromTuple(arg, tuple, desc);
|
||||
dict = PLyDict_FromTuple(arg, tuple, desc, include_generated);
|
||||
|
||||
MemoryContextSwitchTo(oldcontext);
|
||||
|
||||
@ -804,7 +804,7 @@ PLyDict_FromComposite(PLyDatumToOb *arg, Datum d)
|
||||
tmptup.t_len = HeapTupleHeaderGetDatumLength(td);
|
||||
tmptup.t_data = td;
|
||||
|
||||
dict = PLyDict_FromTuple(arg, &tmptup, tupdesc);
|
||||
dict = PLyDict_FromTuple(arg, &tmptup, tupdesc, true);
|
||||
|
||||
ReleaseTupleDesc(tupdesc);
|
||||
|
||||
@ -815,7 +815,7 @@ PLyDict_FromComposite(PLyDatumToOb *arg, Datum d)
|
||||
* Transform a tuple into a Python dict object.
|
||||
*/
|
||||
static PyObject *
|
||||
PLyDict_FromTuple(PLyDatumToOb *arg, HeapTuple tuple, TupleDesc desc)
|
||||
PLyDict_FromTuple(PLyDatumToOb *arg, HeapTuple tuple, TupleDesc desc, bool include_generated)
|
||||
{
|
||||
PyObject *volatile dict;
|
||||
|
||||
@ -842,6 +842,13 @@ PLyDict_FromTuple(PLyDatumToOb *arg, HeapTuple tuple, TupleDesc desc)
|
||||
if (attr->attisdropped)
|
||||
continue;
|
||||
|
||||
if (attr->attgenerated)
|
||||
{
|
||||
/* don't include unless requested */
|
||||
if (!include_generated)
|
||||
continue;
|
||||
}
|
||||
|
||||
key = NameStr(attr->attname);
|
||||
vattr = heap_getattr(tuple, (i + 1), desc, &is_null);
|
||||
|
||||
|
@ -151,7 +151,7 @@ extern Datum PLy_output_convert(PLyObToDatum *arg, PyObject *val,
|
||||
bool *isnull);
|
||||
|
||||
extern PyObject *PLy_input_from_tuple(PLyDatumToOb *arg, HeapTuple tuple,
|
||||
TupleDesc desc);
|
||||
TupleDesc desc, bool include_generated);
|
||||
|
||||
extern void PLy_input_setup_func(PLyDatumToOb *arg, MemoryContext arg_mcxt,
|
||||
Oid typeOid, int32 typmod,
|
||||
|
@ -67,6 +67,11 @@ SELECT * FROM users;
|
||||
CREATE TABLE trigger_test
|
||||
(i int, v text );
|
||||
|
||||
CREATE TABLE trigger_test_generated (
|
||||
i int,
|
||||
j int GENERATED ALWAYS AS (i * 2) STORED
|
||||
);
|
||||
|
||||
CREATE FUNCTION trigger_data() RETURNS trigger LANGUAGE plpythonu AS $$
|
||||
|
||||
if 'relid' in TD:
|
||||
@ -109,6 +114,21 @@ DROP TRIGGER show_trigger_data_trig_stmt on trigger_test;
|
||||
DROP TRIGGER show_trigger_data_trig_before on trigger_test;
|
||||
DROP TRIGGER show_trigger_data_trig_after on trigger_test;
|
||||
|
||||
CREATE TRIGGER show_trigger_data_trig_before
|
||||
BEFORE INSERT OR UPDATE OR DELETE ON trigger_test_generated
|
||||
FOR EACH ROW EXECUTE PROCEDURE trigger_data();
|
||||
|
||||
CREATE TRIGGER show_trigger_data_trig_after
|
||||
AFTER INSERT OR UPDATE OR DELETE ON trigger_test_generated
|
||||
FOR EACH ROW EXECUTE PROCEDURE trigger_data();
|
||||
|
||||
insert into trigger_test_generated (i) values (1);
|
||||
update trigger_test_generated set i = 11 where i = 1;
|
||||
delete from trigger_test_generated;
|
||||
|
||||
DROP TRIGGER show_trigger_data_trig_before ON trigger_test_generated;
|
||||
DROP TRIGGER show_trigger_data_trig_after ON trigger_test_generated;
|
||||
|
||||
insert into trigger_test values(1,'insert');
|
||||
CREATE VIEW trigger_test_view AS SELECT * FROM trigger_test;
|
||||
|
||||
@ -430,3 +450,20 @@ UPDATE transition_table_test SET name = 'b';
|
||||
|
||||
DROP TABLE transition_table_test;
|
||||
DROP FUNCTION transition_table_test_f();
|
||||
|
||||
|
||||
-- dealing with generated columns
|
||||
|
||||
CREATE FUNCTION generated_test_func1() RETURNS trigger
|
||||
LANGUAGE plpythonu
|
||||
AS $$
|
||||
TD['new']['j'] = 5 # not allowed
|
||||
return 'MODIFY'
|
||||
$$;
|
||||
|
||||
CREATE TRIGGER generated_test_trigger1 BEFORE INSERT ON trigger_test_generated
|
||||
FOR EACH ROW EXECUTE PROCEDURE generated_test_func1();
|
||||
|
||||
TRUNCATE trigger_test_generated;
|
||||
INSERT INTO trigger_test_generated (i) VALUES (1);
|
||||
SELECT * FROM trigger_test_generated;
|
||||
|
Reference in New Issue
Block a user