Skip to content

Commit f0b202d

Browse files
committed
Support ADD COLUMN on continuous aggregates
Allow ALTER MATERIALIZED VIEW <cagg> ADD COLUMN <name> <type> GENERATED ALWAYS AS (<aggregate>) STORED. The new column is added to the materialization hypertable and surfaced through the partial, direct, and user views. AccessExclusiveLock's are taken on each of them at the beginning. New columns will have NULL values on already existing rows from the CAgg, new rows inserted after the ADD COLUMN statement will have appropriate values. To backfill previous rows, forced refresh is required.
1 parent 44ef2da commit f0b202d

18 files changed

Lines changed: 3197 additions & 1 deletion

.unreleased/pr_9825

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Implements: #9825 Support ADD COLUMN on continuous aggregates

src/cross_module_fn.c

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,13 @@ continuous_agg_update_options_default(ContinuousAgg *cagg, WithClauseResult *wit
229229
pg_unreachable();
230230
}
231231

232+
static void
233+
continuous_agg_add_column_default(ContinuousAgg *cagg, AlterTableStmt *stmt)
234+
{
235+
error_no_default_fn_community();
236+
pg_unreachable();
237+
}
238+
232239
static void
233240
continuous_agg_invalidate_raw_ht_all_default(const Hypertable *raw_ht, int64 start, int64 end)
234241
{
@@ -337,6 +344,7 @@ TSDLLEXPORT CrossModuleFunctions ts_cm_functions_default = {
337344
.continuous_agg_invalidate_mat_ht = continuous_agg_invalidate_mat_ht_all_default,
338345
.continuous_agg_dml_invalidate = continuous_agg_dml_invalidate_default,
339346
.continuous_agg_update_options = continuous_agg_update_options_default,
347+
.continuous_agg_add_column = continuous_agg_add_column_default,
340348
.continuous_agg_apply_rewrites_tsl = NULL,
341349
.continuous_agg_validate_query = error_no_default_fn_pg_community,
342350
.continuous_agg_get_bucket_function = error_no_default_fn_pg_community,

src/cross_module_fn.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ typedef struct CrossModuleFunctions
105105
bool update);
106106
void (*continuous_agg_update_options)(ContinuousAgg *cagg,
107107
WithClauseResult *with_clause_options);
108+
void (*continuous_agg_add_column)(ContinuousAgg *cagg, AlterTableStmt *stmt);
108109
Query *(*continuous_agg_apply_rewrites_tsl)(Query *parse);
109110
PGFunction continuous_agg_validate_query;
110111
PGFunction continuous_agg_get_bucket_function;

src/process_utility.c

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5048,6 +5048,36 @@ process_altertable_start_matview(ProcessUtilityArgs *args)
50485048

50495049
continuous_agg_with_clause_perm_check(cagg, view_relid);
50505050

5051+
/*
5052+
* CAgg add column handler.
5053+
* Split `stmt->cmds` into the GENERATED ALWAYS AS and everything else
5054+
* (handled by the switch below). Process all ADD COLUMNs at once since
5055+
* they require a rewrite of the view, and doing them one by one would
5056+
* be inefficient.
5057+
*/
5058+
{
5059+
List *addcol_cmds = NIL;
5060+
List *other_cmds = NIL;
5061+
foreach (lc, stmt->cmds)
5062+
{
5063+
AlterTableCmd *cmd = (AlterTableCmd *) lfirst(lc);
5064+
if (cmd->subtype == AT_AddColumn)
5065+
addcol_cmds = lappend(addcol_cmds, cmd);
5066+
else
5067+
other_cmds = lappend(other_cmds, cmd);
5068+
}
5069+
5070+
if (addcol_cmds != NIL)
5071+
{
5072+
AlterTableStmt addcol_stmt = *stmt;
5073+
addcol_stmt.cmds = addcol_cmds;
5074+
ts_cm_functions->continuous_agg_add_column(cagg, &addcol_stmt);
5075+
CommandCounterIncrement();
5076+
5077+
stmt->cmds = other_cmds;
5078+
}
5079+
}
5080+
50515081
foreach (lc, stmt->cmds)
50525082
{
50535083
AlterTableCmd *cmd = (AlterTableCmd *) lfirst(lc);

tsl/src/continuous_aggs/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
set(SOURCES
2+
${CMAKE_CURRENT_SOURCE_DIR}/add_column.c
23
${CMAKE_CURRENT_SOURCE_DIR}/common.c
34
${CMAKE_CURRENT_SOURCE_DIR}/create.c
45
${CMAKE_CURRENT_SOURCE_DIR}/finalize.c
Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
/*
2+
* This file and its contents are licensed under the Timescale License.
3+
* Please see the included NOTICE for copyright information and
4+
* LICENSE-TIMESCALE for a copy of the license.
5+
*/
6+
7+
#include <postgres.h>
8+
9+
#include <access/heapam.h>
10+
#include <commands/tablecmds.h>
11+
#include <commands/view.h>
12+
#include <fmgr.h>
13+
#include <miscadmin.h>
14+
#include <nodes/makefuncs.h>
15+
#include <nodes/parsenodes.h>
16+
#include <storage/lmgr.h>
17+
#include <storage/lockdefs.h>
18+
#include <tcop/tcopprot.h>
19+
#include <utils/builtins.h>
20+
#include <utils/lsyscache.h>
21+
#include <utils/rel.h>
22+
#include <utils/snapmgr.h>
23+
24+
#include "compression/create.h"
25+
#include "continuous_aggs/common.h"
26+
#include "continuous_aggs/create.h"
27+
#include "debug_point.h"
28+
#include "hypertable.h"
29+
#include "ts_catalog/continuous_agg.h"
30+
31+
#include "add_column.h"
32+
33+
/*
34+
* Pull the CONSTR_GENERATED STORED constraint out of a ColumnDef's
35+
* constraint list. Returns NULL if there is no such constraint.
36+
*/
37+
static Constraint *
38+
get_stored_generated_constraint(ColumnDef *coldef)
39+
{
40+
ListCell *lc;
41+
foreach (lc, coldef->constraints)
42+
{
43+
Constraint *c = lfirst_node(Constraint, lc);
44+
if (c->contype == CONSTR_GENERATED
45+
#if PG18_GE
46+
&& c->generated_kind == ATTRIBUTE_GENERATED_STORED
47+
#endif
48+
)
49+
return c;
50+
}
51+
return NULL;
52+
}
53+
54+
/*
55+
* Validate one ADD COLUMN ... GENERATED ALWAYS AS (agg_expr) STORED.
56+
*
57+
* We do only the two checks here:
58+
* 1. The cmd must carry a CONSTR_GENERATED STORED clause (rejects VIRTUAL
59+
* and the no-GENERATED-at-all case).
60+
* 2. No other column-level constraints (NOT NULL / COLLATE / STORAGE /
61+
* COMPRESSION) are allowed -- the mat-HT ALTER strips them silently
62+
* and the SELECT we feed DefineView wouldn't carry them either, so
63+
* they'd be ignored. Reject explicitly.
64+
*
65+
* Everything else (must-be-aggregate, columns-must-exist, type-matches,
66+
* unique-column-name, JOIN-source resolution) falls out of PG's CREATE OR
67+
* REPLACE VIEW analysis when we re-define each view.
68+
*/
69+
static void
70+
validate_one_aggregation_cmd(ColumnDef *coldef)
71+
{
72+
Constraint *gen = get_stored_generated_constraint(coldef);
73+
if (gen == NULL || gen->raw_expr == NULL)
74+
{
75+
ereport(ERROR,
76+
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
77+
errmsg("ADD COLUMN on a continuous aggregate must use GENERATED ALWAYS AS "
78+
"(<aggregate>) STORED")));
79+
}
80+
81+
if (list_length(coldef->constraints) > 1)
82+
{
83+
ereport(ERROR,
84+
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
85+
errmsg("only the GENERATED ALWAYS AS (...) STORED clause is supported on "
86+
"this ADD COLUMN; column \"%s\" has additional constraints",
87+
coldef->colname),
88+
errhint("Drop the additional constraints (NOT NULL / COLLATE / STORAGE / "
89+
"COMPRESSION / etc.) on this column.")));
90+
}
91+
}
92+
93+
/*
94+
* Run ALTER TABLE _materialized_hypertable_<N> ADD COLUMN <name> <type>
95+
* for each cmd in `stmt->cmds`. The new ColumnDefs strip the GENERATED
96+
* constraint so PG sees a plain ADD COLUMN with no DEFAULT.
97+
*/
98+
static void
99+
add_columns_to_mat_hypertable(Oid mat_ht_oid, AlterTableStmt *stmt)
100+
{
101+
List *cmds = NIL;
102+
List *stripped_defs = NIL;
103+
ListCell *lc;
104+
foreach (lc, stmt->cmds)
105+
{
106+
AlterTableCmd *src = lfirst_node(AlterTableCmd, lc);
107+
ColumnDef *src_def = castNode(ColumnDef, src->def);
108+
109+
ColumnDef *dst_def = copyObject(src_def);
110+
dst_def->constraints = NIL;
111+
dst_def->raw_default = NULL;
112+
dst_def->cooked_default = NULL;
113+
dst_def->generated = '\0';
114+
115+
AlterTableCmd *dst = makeNode(AlterTableCmd);
116+
dst->subtype = AT_AddColumn;
117+
dst->name = NULL;
118+
dst->def = (Node *) dst_def;
119+
dst->missing_ok = false;
120+
121+
cmds = lappend(cmds, dst);
122+
stripped_defs = lappend(stripped_defs, dst_def);
123+
}
124+
125+
AlterTableInternal(mat_ht_oid, cmds, true /* recurse */);
126+
CommandCounterIncrement();
127+
128+
Hypertable *mat_ht = ts_hypertable_get_by_id(ts_hypertable_relid_to_id(mat_ht_oid));
129+
Assert(mat_ht != NULL);
130+
if (TS_HYPERTABLE_HAS_COMPRESSION_TABLE(mat_ht))
131+
{
132+
ListCell *def_cell;
133+
foreach (def_cell, stripped_defs)
134+
{
135+
ColumnDef *def = (ColumnDef *) lfirst(def_cell);
136+
tsl_process_compress_table_add_column(mat_ht, def);
137+
}
138+
CommandCounterIncrement();
139+
}
140+
}
141+
142+
/*
143+
* Rebuild a CAgg-owned view (partial/direct/user) by appending
144+
* `new_targets` to its SELECT list and feeding the result back into DefineView.
145+
*/
146+
static void
147+
rebuild_cagg_view(NameData schema, NameData name, List *new_targets)
148+
{
149+
Oid view_oid =
150+
ts_get_relation_relid(NameStr(schema), NameStr(name), /* return_invalid */ false);
151+
152+
/* pg_get_viewdef caches an SPI plan internally and reads pg_rewrite under
153+
* its own snapshot, which doesn't see catalog updates we did earlier in the
154+
* same statement (e.g. rewrite of view rules). */
155+
PushActiveSnapshot(GetTransactionSnapshot());
156+
Datum def_datum = DirectFunctionCall1(pg_get_viewdef, ObjectIdGetDatum(view_oid));
157+
PopActiveSnapshot();
158+
char *view_def = TextDatumGetCString(def_datum);
159+
160+
List *parsetrees = pg_parse_query(view_def);
161+
Assert(list_length(parsetrees) == 1);
162+
RawStmt *raw = (RawStmt *) linitial(parsetrees);
163+
Assert(IsA(raw->stmt, SelectStmt));
164+
165+
SelectStmt *select = (SelectStmt *) raw->stmt;
166+
167+
select->targetList = list_concat(select->targetList, new_targets);
168+
169+
ViewStmt *vstmt = makeNode(ViewStmt);
170+
vstmt->view = makeRangeVar(pstrdup(NameStr(schema)), pstrdup(NameStr(name)), -1);
171+
vstmt->aliases = NIL;
172+
vstmt->query = (Node *) select;
173+
vstmt->replace = true;
174+
vstmt->options = NIL;
175+
vstmt->withCheckOption = NO_CHECK_OPTION;
176+
177+
/* Partial and direct views live in `_timescaledb_internal`, which the
178+
* invoking user generally doesn't have CREATE rights on. Switch to the
179+
* catalog owner for the duration of DefineView. */
180+
Oid uid = InvalidOid;
181+
Oid saved_uid = InvalidOid;
182+
int sec_ctx = 0;
183+
SWITCH_TO_TS_USER(NameStr(schema), uid, saved_uid, sec_ctx);
184+
DefineView(vstmt, view_def, 0, strlen(view_def));
185+
RESTORE_USER(uid, saved_uid, sec_ctx);
186+
CommandCounterIncrement();
187+
}
188+
189+
static ResTarget *
190+
make_expr_target(ColumnDef *coldef, Node *raw_expr)
191+
{
192+
ResTarget *rt = makeNode(ResTarget);
193+
rt->name = pstrdup(coldef->colname);
194+
rt->indirection = NIL;
195+
rt->val = copyObject(raw_expr);
196+
rt->location = -1;
197+
return rt;
198+
}
199+
200+
static ResTarget *
201+
make_colref_target(ColumnDef *coldef)
202+
{
203+
ColumnRef *cref = makeNode(ColumnRef);
204+
cref->fields = list_make1(makeString(pstrdup(coldef->colname)));
205+
cref->location = -1;
206+
207+
ResTarget *rt = makeNode(ResTarget);
208+
rt->name = pstrdup(coldef->colname);
209+
rt->indirection = NIL;
210+
rt->val = (Node *) cref;
211+
rt->location = -1;
212+
return rt;
213+
}
214+
215+
void
216+
continuous_agg_add_column(ContinuousAgg *cagg, AlterTableStmt *stmt)
217+
{
218+
ts_cagg_permissions_check(cagg->relid, GetUserId());
219+
220+
/* Lock all the relations we'll touch, in the same order as
221+
* DROP so concurrent DROP and ADD COLUMN cannot deadlock. */
222+
Oid cagg_relid = cagg->relid;
223+
DEBUG_WAITPOINT("cagg_add_column_before_uv_lock");
224+
LockRelationOid(cagg_relid, AccessExclusiveLock);
225+
226+
/* A concurrent DROP could have committed between the cagg lookup and user-view lock
227+
* here. */
228+
cagg = cagg_get_by_relid_or_fail(cagg_relid);
229+
Oid mat_ht_oid = ts_hypertable_id_to_relid(cagg->data.mat_hypertable_id, false);
230+
Oid partial_view_oid = ts_get_relation_relid(NameStr(cagg->data.partial_view_schema),
231+
NameStr(cagg->data.partial_view_name),
232+
false);
233+
Oid direct_view_oid = ts_get_relation_relid(NameStr(cagg->data.direct_view_schema),
234+
NameStr(cagg->data.direct_view_name),
235+
false);
236+
DEBUG_WAITPOINT("cagg_add_column_before_ht_lock");
237+
LockRelationOid(mat_ht_oid, AccessExclusiveLock);
238+
LockRelationOid(partial_view_oid, AccessExclusiveLock);
239+
LockRelationOid(direct_view_oid, AccessExclusiveLock);
240+
DEBUG_WAITPOINT("cagg_add_column_after_locks");
241+
242+
ListCell *lc;
243+
foreach (lc, stmt->cmds)
244+
{
245+
AlterTableCmd *cmd = lfirst_node(AlterTableCmd, lc);
246+
ColumnDef *coldef = castNode(ColumnDef, cmd->def);
247+
validate_one_aggregation_cmd(coldef);
248+
}
249+
250+
/* Build the per-view ResTarget lists. Partial / direct views get
251+
* the user's aggregate expression; the user view gets a column
252+
* reference to the freshly-added mat-HT column. */
253+
List *expr_targets = NIL;
254+
List *colref_targets = NIL;
255+
foreach (lc, stmt->cmds)
256+
{
257+
AlterTableCmd *cmd = lfirst_node(AlterTableCmd, lc);
258+
ColumnDef *coldef = castNode(ColumnDef, cmd->def);
259+
Constraint *gen = get_stored_generated_constraint(coldef);
260+
expr_targets = lappend(expr_targets, make_expr_target(coldef, gen->raw_expr));
261+
colref_targets = lappend(colref_targets, make_colref_target(coldef));
262+
}
263+
264+
rebuild_cagg_view(cagg->data.partial_view_schema,
265+
cagg->data.partial_view_name,
266+
list_copy_deep(expr_targets));
267+
268+
rebuild_cagg_view(cagg->data.direct_view_schema,
269+
cagg->data.direct_view_name,
270+
list_copy_deep(expr_targets));
271+
272+
add_columns_to_mat_hypertable(mat_ht_oid, stmt);
273+
274+
/* If the CAgg is real-time (materialized_only=false), the user view is
275+
* a UNION ALL that our rebuild_cagg_view doesn't handle. Flip to
276+
* materialized-only so the user view becomes a plain SELECT from the mat
277+
* HT, do all our rebuilds against that single-SELECT shape, then flip back
278+
* at the end so the final user view is a UNION ALL again. */
279+
bool was_realtime = !cagg->data.materialized_only;
280+
if (was_realtime)
281+
{
282+
cagg_flip_realtime_view_definition(cagg,
283+
ts_hypertable_get_by_id(cagg->data.mat_hypertable_id));
284+
}
285+
rebuild_cagg_view(cagg->data.user_view_schema, cagg->data.user_view_name, colref_targets);
286+
if (was_realtime)
287+
{
288+
cagg_flip_realtime_view_definition(cagg,
289+
ts_hypertable_get_by_id(cagg->data.mat_hypertable_id));
290+
}
291+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/*
2+
* This file and its contents are licensed under the Timescale License.
3+
* Please see the included NOTICE for copyright information and
4+
* LICENSE-TIMESCALE for a copy of the license.
5+
*/
6+
#pragma once
7+
8+
#include <postgres.h>
9+
#include <nodes/parsenodes.h>
10+
11+
#include "ts_catalog/continuous_agg.h"
12+
13+
extern void continuous_agg_add_column(ContinuousAgg *cagg, AlterTableStmt *stmt);

tsl/src/continuous_aggs/create.c

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,6 @@ static void cagg_create_hypertable(int32 hypertable_id, Oid mat_tbloid, const ch
9696
int64 mat_tbltimecol_interval);
9797
static void mattablecolumninfo_add_mattable_index(MaterializationHypertableColumnInfo *matcolinfo,
9898
Hypertable *ht);
99-
static ObjectAddress create_view_for_query(Query *selquery, RangeVar *viewrel);
10099
static void fixup_userview_query_tlist(Query *userquery, List *tlist_aliases);
101100
static void cagg_create(const CreateTableAsStmt *create_stmt, ViewStmt *stmt, Query *panquery,
102101
ContinuousAggTimeBucketInfo *bucket_info,

0 commit comments

Comments
 (0)