|
| 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 | +} |
0 commit comments