Skip to content

Commit 0e79df4

Browse files
committed
Fix handling of custom SQL-based partitioning functions
Previously, when the FmgrInfo was initialized for partitioning functions, the type information was cached in the fn_extra field. However, the contract for fn_extra is that it is to be set by the called function and not prior to its invokation. Setting fn_extra prior to function invokation is an issue for custom partitioning functions implemented in SQL, as the SQL call manager expects to use fn_extra for its own cache. This change avoids setting fn_extra prior to function invokation, and instead information is cached in fn_extra within our provided partitioning functions. The native partitioning functions now also support nested functions, i.e., the input to the function is the output of another function.
1 parent 22112e3 commit 0e79df4

File tree

3 files changed

+155
-32
lines changed

3 files changed

+155
-32
lines changed

src/partitioning.c

Lines changed: 75 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
#include <utils/jsonb.h>
1313
#include <utils/acl.h>
1414
#include <utils/rangetypes.h>
15+
#include <utils/memutils.h>
1516
#include <catalog/namespace.h>
1617
#include <catalog/pg_type.h>
1718
#include <access/hash.h>
@@ -158,7 +159,7 @@ partitioning_info_create(const char *schema,
158159
pinfo->typcache_entry = lookup_type_cache(columntype, TYPECACHE_HASH_FLAGS);
159160

160161
if (pinfo->typcache_entry->hash_proc == InvalidOid)
161-
elog(ERROR, "No hash function for type %u", columntype);
162+
elog(ERROR, "could not find hash function for type %u", columntype);
162163

163164
partitioning_func_set_func_fmgr(&pinfo->partfunc);
164165

@@ -181,20 +182,13 @@ partitioning_info_create(const char *schema,
181182

182183
fmgr_info_set_expr((Node *) expr, &pinfo->partfunc.func_fmgr);
183184

184-
/*
185-
* Set the type cache entry in fn_extra to avoid an extry lookup in the
186-
* partition hash function
187-
*/
188-
pinfo->partfunc.func_fmgr.fn_extra = pinfo->typcache_entry;
189-
190185
return pinfo;
191186
}
192187

193188
/*
194189
* Apply the partitioning function of a hypertable to a value.
195190
*
196-
* We support both partitioning functions with the signature int
197-
* func(anyelement).
191+
* We support partitioning functions with the signature (anyelement) -> int.
198192
*/
199193
int32
200194
partitioning_func_apply(PartitioningInfo *pinfo, Datum value)
@@ -232,10 +226,10 @@ resolve_function_argtype(FunctionCallInfo fcinfo)
232226
fe = (FuncExpr *) fcinfo->flinfo->fn_expr;
233227

234228
if (NULL == fe || !IsA(fe, FuncExpr))
235-
elog(ERROR, "No function expression set when invoking partitioning function");
229+
elog(ERROR, "no function expression set when invoking partitioning function");
236230

237231
if (list_length(fe->args) != 1)
238-
elog(ERROR, "Unexpected number of arguments in function expression");
232+
elog(ERROR, "unexpected number of arguments in function expression");
239233

240234
node = linitial(fe->args);
241235

@@ -250,13 +244,50 @@ resolve_function_argtype(FunctionCallInfo fcinfo)
250244
case T_CoerceViaIO:
251245
argtype = ((CoerceViaIO *) node)->resulttype;
252246
break;
247+
case T_FuncExpr:
248+
/* Argument is function, so our input is its result type */
249+
argtype = ((FuncExpr *) node)->funcresulttype;
250+
break;
253251
default:
254-
elog(ERROR, "Unsupported expression argument node type %u", nodeTag(node));
252+
elog(ERROR, "unsupported expression argument node type %u", nodeTag(node));
255253
}
256254

257255
return argtype;
258256
}
259257

258+
/*
259+
* Partitioning function cache.
260+
*
261+
* Holds type information to avoid repeated lookups. The cache is allocated on a
262+
* child memory context of the context that created the associated FmgrInfo
263+
* struct. For partitioning functions invoked on the insert path, this is
264+
* typically the Hypertable cache's memory context. Hence, the type cache lives
265+
* for the duration of the hypertable cache and can be reused across multiple
266+
* invokations of the partitioning function, even across transactions.
267+
*
268+
* If the partitioning function is invoked outside the insert path, the FmgrInfo
269+
* and its memory context has a lifetime corresponding to that invokation.
270+
*/
271+
typedef struct PartFuncCache
272+
{
273+
Oid argtype;
274+
Oid coerce_funcid;
275+
TypeCacheEntry *tce;
276+
} PartFuncCache;
277+
278+
static PartFuncCache *
279+
part_func_cache_create(Oid argtype, TypeCacheEntry *tce, Oid coerce_funcid, MemoryContext mcxt)
280+
{
281+
PartFuncCache *pfc;
282+
283+
pfc = MemoryContextAlloc(mcxt, sizeof(PartFuncCache));
284+
pfc->argtype = argtype;
285+
pfc->tce = tce;
286+
pfc->coerce_funcid = coerce_funcid;
287+
288+
return pfc;
289+
}
290+
260291
/* _timescaledb_catalog.get_partition_for_key(key anyelement) RETURNS INT */
261292
PGDLLEXPORT Datum get_partition_for_key(PG_FUNCTION_ARGS);
262293

@@ -270,26 +301,35 @@ Datum
270301
get_partition_for_key(PG_FUNCTION_ARGS)
271302
{
272303
Datum arg = PG_GETARG_DATUM(0);
304+
PartFuncCache *pfc = fcinfo->flinfo->fn_extra;
273305
struct varlena *data;
274-
Oid argtype;
275306
uint32 hash_u;
276307
int32 res;
277308

278309
if (PG_NARGS() != 1)
279-
elog(ERROR, "Unexpected number of arguments to partitioning function");
280-
281-
argtype = resolve_function_argtype(fcinfo);
310+
elog(ERROR, "unexpected number of arguments to partitioning function");
282311

283-
if (argtype != TEXTOID)
312+
if (NULL == pfc)
284313
{
285-
/* Not TEXT input -> need to convert to text */
286-
Oid funcid = find_text_coercion_func(argtype);
314+
Oid funcid = InvalidOid;
315+
Oid argtype = resolve_function_argtype(fcinfo);
287316

288-
if (!OidIsValid(funcid))
289-
elog(ERROR, "Could not coerce type %u to text",
290-
argtype);
317+
if (argtype != TEXTOID)
318+
{
319+
/* Not TEXT input -> need to convert to text */
320+
funcid = find_text_coercion_func(argtype);
321+
322+
if (!OidIsValid(funcid))
323+
elog(ERROR, "could not coerce type %u to text", argtype);
324+
}
291325

292-
arg = OidFunctionCall1(funcid, arg);
326+
pfc = part_func_cache_create(argtype, NULL, funcid, fcinfo->flinfo->fn_mcxt);
327+
fcinfo->flinfo->fn_extra = pfc;
328+
}
329+
330+
if (pfc->argtype != TEXTOID)
331+
{
332+
arg = OidFunctionCall1(pfc->coerce_funcid, arg);
293333
arg = CStringGetTextDatum(DatumGetCString(arg));
294334
}
295335

@@ -320,23 +360,26 @@ Datum
320360
get_partition_hash(PG_FUNCTION_ARGS)
321361
{
322362
Datum arg = PG_GETARG_DATUM(0);
323-
TypeCacheEntry *tce = fcinfo->flinfo->fn_extra;
324-
Oid argtype;
363+
PartFuncCache *pfc = fcinfo->flinfo->fn_extra;
325364
Datum hash;
326365
int32 res;
327366

328367
if (PG_NARGS() != 1)
329-
elog(ERROR, "Unexpected number of arguments to partitioning function");
368+
elog(ERROR, "unexpected number of arguments to partitioning function");
330369

331-
argtype = resolve_function_argtype(fcinfo);
370+
if (NULL == pfc)
371+
{
372+
Oid argtype = resolve_function_argtype(fcinfo);
373+
TypeCacheEntry *tce = lookup_type_cache(argtype, TYPECACHE_HASH_FLAGS);
332374

333-
if (tce == NULL)
334-
tce = lookup_type_cache(argtype, TYPECACHE_HASH_FLAGS);
375+
pfc = part_func_cache_create(argtype, tce, InvalidOid, fcinfo->flinfo->fn_mcxt);
376+
fcinfo->flinfo->fn_extra = pfc;
377+
}
335378

336-
if (tce->hash_proc == InvalidOid)
337-
elog(ERROR, "No hash function for type %u", argtype);
379+
if (pfc->tce->hash_proc == InvalidOid)
380+
elog(ERROR, "could not find hash function for type %u", pfc->argtype);
338381

339-
hash = FunctionCall1(&tce->hash_proc_finfo, arg);
382+
hash = FunctionCall1(&pfc->tce->hash_proc_finfo, arg);
340383

341384
/* Only positive numbers */
342385
res = (int32) (DatumGetUInt32(hash) & 0x7fffffff);

test/expected/partitioning.out

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,3 +141,56 @@ SELECT * FROM _timescaledb_catalog.dimension;
141141
9 | 4 | location | integer | f | 2 | _timescaledb_internal | get_partition_for_key |
142142
(9 rows)
143143

144+
-- Test that we support custom SQL-based partitioning functions and
145+
-- that our native partitioning function handles function expressions
146+
-- as argument
147+
CREATE OR REPLACE FUNCTION custom_partfunc(source anyelement)
148+
RETURNS INTEGER LANGUAGE PLPGSQL AS
149+
$BODY$
150+
DECLARE
151+
retval INTEGER;
152+
BEGIN
153+
retval = _timescaledb_internal.get_partition_hash(substring(source::text FROM '[A-za-z0-9 ]+'));
154+
RAISE NOTICE 'hash value for % is %', source, retval;
155+
RETURN retval;
156+
END
157+
$BODY$;
158+
CREATE TABLE part_custom_func(time timestamptz, temp float8, device text);
159+
SELECT create_hypertable('part_custom_func', 'time', 'device', 2, partitioning_func => 'custom_partfunc');
160+
NOTICE: adding NOT NULL constraint to column "time"
161+
create_hypertable
162+
-------------------
163+
164+
(1 row)
165+
166+
SELECT _timescaledb_internal.get_partition_hash(substring('dev1' FROM '[A-za-z0-9 ]+'));
167+
get_partition_hash
168+
--------------------
169+
1129986420
170+
(1 row)
171+
172+
SELECT _timescaledb_internal.get_partition_hash('dev1'::text);
173+
get_partition_hash
174+
--------------------
175+
1129986420
176+
(1 row)
177+
178+
SELECT _timescaledb_internal.get_partition_hash('dev7'::text);
179+
get_partition_hash
180+
--------------------
181+
449729092
182+
(1 row)
183+
184+
INSERT INTO part_custom_func VALUES ('2017-03-22T09:18:23', 23.4, 'dev1'),
185+
('2017-03-22T09:18:23', 23.4, 'dev7');
186+
NOTICE: hash value for dev1 is 1129986420
187+
NOTICE: hash value for dev1 is 1129986420
188+
NOTICE: hash value for dev7 is 449729092
189+
NOTICE: hash value for dev7 is 449729092
190+
SELECT * FROM test.show_subtables('part_custom_func');
191+
Child | Tablespace
192+
----------------------------------------+------------
193+
_timescaledb_internal._hyper_5_6_chunk |
194+
_timescaledb_internal._hyper_5_7_chunk |
195+
(2 rows)
196+

test/sql/partitioning.sql

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,30 @@ SELECT add_dimension('part_add_dim', 'location', 2, partitioning_func => 'bad_fu
5656

5757
SELECT add_dimension('part_add_dim', 'location', 2, partitioning_func => '_timescaledb_internal.get_partition_for_key');
5858
SELECT * FROM _timescaledb_catalog.dimension;
59+
60+
-- Test that we support custom SQL-based partitioning functions and
61+
-- that our native partitioning function handles function expressions
62+
-- as argument
63+
CREATE OR REPLACE FUNCTION custom_partfunc(source anyelement)
64+
RETURNS INTEGER LANGUAGE PLPGSQL AS
65+
$BODY$
66+
DECLARE
67+
retval INTEGER;
68+
BEGIN
69+
retval = _timescaledb_internal.get_partition_hash(substring(source::text FROM '[A-za-z0-9 ]+'));
70+
RAISE NOTICE 'hash value for % is %', source, retval;
71+
RETURN retval;
72+
END
73+
$BODY$;
74+
75+
CREATE TABLE part_custom_func(time timestamptz, temp float8, device text);
76+
SELECT create_hypertable('part_custom_func', 'time', 'device', 2, partitioning_func => 'custom_partfunc');
77+
78+
SELECT _timescaledb_internal.get_partition_hash(substring('dev1' FROM '[A-za-z0-9 ]+'));
79+
SELECT _timescaledb_internal.get_partition_hash('dev1'::text);
80+
SELECT _timescaledb_internal.get_partition_hash('dev7'::text);
81+
82+
INSERT INTO part_custom_func VALUES ('2017-03-22T09:18:23', 23.4, 'dev1'),
83+
('2017-03-22T09:18:23', 23.4, 'dev7');
84+
85+
SELECT * FROM test.show_subtables('part_custom_func');

0 commit comments

Comments
 (0)