Line data Source code
1 : /*-------------------------------------------------------------------------
2 : * stat_utils.c
3 : *
4 : * PostgreSQL statistics manipulation utilities.
5 : *
6 : * Code supporting the direct manipulation of statistics.
7 : *
8 : * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
9 : * Portions Copyright (c) 1994, Regents of the University of California
10 : *
11 : * IDENTIFICATION
12 : * src/backend/statistics/stat_utils.c
13 : *
14 : *-------------------------------------------------------------------------
15 : */
16 :
17 : #include "postgres.h"
18 :
19 : #include "access/htup_details.h"
20 : #include "access/relation.h"
21 : #include "catalog/index.h"
22 : #include "catalog/namespace.h"
23 : #include "catalog/pg_class.h"
24 : #include "catalog/pg_database.h"
25 : #include "funcapi.h"
26 : #include "miscadmin.h"
27 : #include "statistics/stat_utils.h"
28 : #include "storage/lmgr.h"
29 : #include "utils/acl.h"
30 : #include "utils/array.h"
31 : #include "utils/builtins.h"
32 : #include "utils/lsyscache.h"
33 : #include "utils/rel.h"
34 : #include "utils/syscache.h"
35 :
36 : /*
37 : * Ensure that a given argument is not null.
38 : */
39 : void
40 8646 : stats_check_required_arg(FunctionCallInfo fcinfo,
41 : struct StatsArgInfo *arginfo,
42 : int argnum)
43 : {
44 8646 : if (PG_ARGISNULL(argnum))
45 48 : ereport(ERROR,
46 : (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
47 : errmsg("argument \"%s\" must not be null",
48 : arginfo[argnum].argname)));
49 8598 : }
50 :
51 : /*
52 : * Check that argument is either NULL or a one dimensional array with no
53 : * NULLs.
54 : *
55 : * If a problem is found, emit a WARNING, and return false. Otherwise return
56 : * true.
57 : */
58 : bool
59 4110 : stats_check_arg_array(FunctionCallInfo fcinfo,
60 : struct StatsArgInfo *arginfo,
61 : int argnum)
62 : {
63 : ArrayType *arr;
64 :
65 4110 : if (PG_ARGISNULL(argnum))
66 3382 : return true;
67 :
68 728 : arr = DatumGetArrayTypeP(PG_GETARG_DATUM(argnum));
69 :
70 728 : if (ARR_NDIM(arr) != 1)
71 : {
72 0 : ereport(WARNING,
73 : (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
74 : errmsg("argument \"%s\" must not be a multidimensional array",
75 : arginfo[argnum].argname)));
76 0 : return false;
77 : }
78 :
79 728 : if (array_contains_nulls(arr))
80 : {
81 6 : ereport(WARNING,
82 : (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
83 : errmsg("argument \"%s\" array must not contain null values",
84 : arginfo[argnum].argname)));
85 6 : return false;
86 : }
87 :
88 722 : return true;
89 : }
90 :
91 : /*
92 : * Enforce parameter pairs that must be specified together (or not at all) for
93 : * a particular stakind, such as most_common_vals and most_common_freqs for
94 : * STATISTIC_KIND_MCV.
95 : *
96 : * If a problem is found, emit a WARNING, and return false. Otherwise return
97 : * true.
98 : */
99 : bool
100 4110 : stats_check_arg_pair(FunctionCallInfo fcinfo,
101 : struct StatsArgInfo *arginfo,
102 : int argnum1, int argnum2)
103 : {
104 4110 : if (PG_ARGISNULL(argnum1) && PG_ARGISNULL(argnum2))
105 3366 : return true;
106 :
107 744 : if (PG_ARGISNULL(argnum1) || PG_ARGISNULL(argnum2))
108 : {
109 42 : int nullarg = PG_ARGISNULL(argnum1) ? argnum1 : argnum2;
110 42 : int otherarg = PG_ARGISNULL(argnum1) ? argnum2 : argnum1;
111 :
112 42 : ereport(WARNING,
113 : (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
114 : errmsg("argument \"%s\" must be specified when argument \"%s\" is specified",
115 : arginfo[nullarg].argname,
116 : arginfo[otherarg].argname)));
117 :
118 42 : return false;
119 : }
120 :
121 702 : return true;
122 : }
123 :
124 : /*
125 : * A role has privileges to set statistics on the relation if any of the
126 : * following are true:
127 : * - the role owns the current database and the relation is not shared
128 : * - the role has the MAINTAIN privilege on the relation
129 : */
130 : void
131 3594 : RangeVarCallbackForStats(const RangeVar *relation,
132 : Oid relId, Oid oldRelId, void *arg)
133 : {
134 3594 : Oid *locked_oid = (Oid *) arg;
135 3594 : Oid table_oid = relId;
136 : HeapTuple tuple;
137 : Form_pg_class form;
138 : char relkind;
139 :
140 : /*
141 : * If we previously locked some other index's heap, and the name we're
142 : * looking up no longer refers to that relation, release the now-useless
143 : * lock.
144 : */
145 3594 : if (relId != oldRelId && OidIsValid(*locked_oid))
146 : {
147 0 : UnlockRelationOid(*locked_oid, ShareUpdateExclusiveLock);
148 0 : *locked_oid = InvalidOid;
149 : }
150 :
151 : /* If the relation does not exist, there's nothing more to do. */
152 3594 : if (!OidIsValid(relId))
153 12 : return;
154 :
155 : /* If the relation does exist, check whether it's an index. */
156 3582 : relkind = get_rel_relkind(relId);
157 3582 : if (relkind == RELKIND_INDEX ||
158 : relkind == RELKIND_PARTITIONED_INDEX)
159 590 : table_oid = IndexGetRelation(relId, false);
160 :
161 : /*
162 : * If retrying yields the same OID, there are a couple of extremely
163 : * unlikely scenarios we need to handle.
164 : */
165 3582 : if (relId == oldRelId)
166 : {
167 : /*
168 : * If a previous lookup found an index, but the current lookup did
169 : * not, the index was dropped and the OID was reused for something
170 : * else between lookups. In theory, we could simply drop our lock on
171 : * the index's parent table and proceed, but in the interest of
172 : * avoiding complexity, we just error.
173 : */
174 4 : if (table_oid == relId && OidIsValid(*locked_oid))
175 0 : ereport(ERROR,
176 : (errcode(ERRCODE_UNDEFINED_OBJECT),
177 : errmsg("index \"%s\" was concurrently dropped",
178 : relation->relname)));
179 :
180 : /*
181 : * If the current lookup found an index but a previous lookup either
182 : * did not find an index or found one with a different parent
183 : * relation, the relation was dropped and the OID was reused for an
184 : * index between lookups. RangeVarGetRelidExtended() will have
185 : * already locked the index at this point, so we can't just lock the
186 : * newly discovered parent table OID without risking deadlock. As
187 : * above, we just error in this case.
188 : */
189 4 : if (table_oid != relId && table_oid != *locked_oid)
190 0 : ereport(ERROR,
191 : (errcode(ERRCODE_UNDEFINED_OBJECT),
192 : errmsg("index \"%s\" was concurrently created",
193 : relation->relname)));
194 : }
195 :
196 3582 : tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(table_oid));
197 3582 : if (!HeapTupleIsValid(tuple))
198 0 : elog(ERROR, "cache lookup failed for OID %u", table_oid);
199 3582 : form = (Form_pg_class) GETSTRUCT(tuple);
200 :
201 : /* the relkinds that can be used with ANALYZE */
202 3582 : switch (form->relkind)
203 : {
204 3564 : case RELKIND_RELATION:
205 : case RELKIND_MATVIEW:
206 : case RELKIND_FOREIGN_TABLE:
207 : case RELKIND_PARTITIONED_TABLE:
208 3564 : break;
209 18 : default:
210 18 : ereport(ERROR,
211 : (errcode(ERRCODE_WRONG_OBJECT_TYPE),
212 : errmsg("cannot modify statistics for relation \"%s\"",
213 : NameStr(form->relname)),
214 : errdetail_relkind_not_supported(form->relkind)));
215 : }
216 :
217 3564 : if (form->relisshared)
218 0 : ereport(ERROR,
219 : (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
220 : errmsg("cannot modify statistics for shared relation")));
221 :
222 : /* Check permissions */
223 3564 : if (!object_ownercheck(DatabaseRelationId, MyDatabaseId, GetUserId()))
224 : {
225 0 : AclResult aclresult = pg_class_aclcheck(table_oid,
226 : GetUserId(),
227 : ACL_MAINTAIN);
228 :
229 0 : if (aclresult != ACLCHECK_OK)
230 0 : aclcheck_error(aclresult,
231 0 : get_relkind_objtype(form->relkind),
232 0 : NameStr(form->relname));
233 : }
234 :
235 3564 : ReleaseSysCache(tuple);
236 :
237 : /* Lock heap before index to avoid deadlock. */
238 3564 : if (relId != oldRelId && table_oid != relId)
239 : {
240 588 : LockRelationOid(table_oid, ShareUpdateExclusiveLock);
241 588 : *locked_oid = table_oid;
242 : }
243 : }
244 :
245 :
246 : /*
247 : * Find the argument number for the given argument name, returning -1 if not
248 : * found.
249 : */
250 : static int
251 25670 : get_arg_by_name(const char *argname, struct StatsArgInfo *arginfo)
252 : {
253 : int argnum;
254 :
255 123454 : for (argnum = 0; arginfo[argnum].argname != NULL; argnum++)
256 123442 : if (pg_strcasecmp(argname, arginfo[argnum].argname) == 0)
257 25658 : return argnum;
258 :
259 12 : ereport(WARNING,
260 : (errmsg("unrecognized argument name: \"%s\"", argname)));
261 :
262 12 : return -1;
263 : }
264 :
265 : /*
266 : * Ensure that a given argument matched the expected type.
267 : */
268 : static bool
269 25658 : stats_check_arg_type(const char *argname, Oid argtype, Oid expectedtype)
270 : {
271 25658 : if (argtype != expectedtype)
272 : {
273 24 : ereport(WARNING,
274 : (errmsg("argument \"%s\" has type %s, expected type %s",
275 : argname, format_type_be(argtype),
276 : format_type_be(expectedtype))));
277 24 : return false;
278 : }
279 :
280 25634 : return true;
281 : }
282 :
283 : /*
284 : * Translate variadic argument pairs from 'pairs_fcinfo' into a
285 : * 'positional_fcinfo' appropriate for calling relation_statistics_update() or
286 : * attribute_statistics_update() with positional arguments.
287 : *
288 : * Caller should have already initialized positional_fcinfo with a size
289 : * appropriate for calling the intended positional function, and arginfo
290 : * should also match the intended positional function.
291 : */
292 : bool
293 3620 : stats_fill_fcinfo_from_arg_pairs(FunctionCallInfo pairs_fcinfo,
294 : FunctionCallInfo positional_fcinfo,
295 : struct StatsArgInfo *arginfo)
296 : {
297 : Datum *args;
298 : bool *argnulls;
299 : Oid *types;
300 : int nargs;
301 3620 : bool result = true;
302 :
303 : /* clear positional args */
304 42572 : for (int i = 0; arginfo[i].argname != NULL; i++)
305 : {
306 38952 : positional_fcinfo->args[i].value = (Datum) 0;
307 38952 : positional_fcinfo->args[i].isnull = true;
308 : }
309 :
310 3620 : nargs = extract_variadic_args(pairs_fcinfo, 0, true,
311 : &args, &types, &argnulls);
312 :
313 3620 : if (nargs % 2 != 0)
314 6 : ereport(ERROR,
315 : errmsg("variadic arguments must be name/value pairs"),
316 : errhint("Provide an even number of variadic arguments that can be divided into pairs."));
317 :
318 : /*
319 : * For each argument name/value pair, find corresponding positional
320 : * argument for the argument name, and assign the argument value to
321 : * positional_fcinfo.
322 : */
323 32868 : for (int i = 0; i < nargs; i += 2)
324 : {
325 : int argnum;
326 : char *argname;
327 :
328 29260 : if (argnulls[i])
329 6 : ereport(ERROR,
330 : (errmsg("name at variadic position %d is null", i + 1)));
331 :
332 29254 : if (types[i] != TEXTOID)
333 0 : ereport(ERROR,
334 : (errmsg("name at variadic position %d has type %s, expected type %s",
335 : i + 1, format_type_be(types[i]),
336 : format_type_be(TEXTOID))));
337 :
338 29254 : if (argnulls[i + 1])
339 276 : continue;
340 :
341 28978 : argname = TextDatumGetCString(args[i]);
342 :
343 : /*
344 : * The 'version' argument is a special case, not handled by arginfo
345 : * because it's not a valid positional argument.
346 : *
347 : * For now, 'version' is accepted but ignored. In the future it can be
348 : * used to interpret older statistics properly.
349 : */
350 28978 : if (pg_strcasecmp(argname, "version") == 0)
351 3308 : continue;
352 :
353 25670 : argnum = get_arg_by_name(argname, arginfo);
354 :
355 51328 : if (argnum < 0 || !stats_check_arg_type(argname, types[i + 1],
356 25658 : arginfo[argnum].argtype))
357 : {
358 36 : result = false;
359 36 : continue;
360 : }
361 :
362 25634 : positional_fcinfo->args[argnum].value = args[i + 1];
363 25634 : positional_fcinfo->args[argnum].isnull = false;
364 : }
365 :
366 3608 : return result;
367 : }
|