Skip to content

Commit 71104b3

Browse files
committed
Add decompress_batch SQL function
Introduce _timescaledb_functions.decompress_batch(record), which expands a single compressed-chunk row back into the individual rows it represents. The function is intended for use from a custom logical decoding plugin that needs to materialize the original tuples from a compressed batch. The input descriptor is recovered from the record's own type info (typeId/typmod in the HeapTupleHeader) via lookup_rowtype_tupdesc, so callers can pass a row from a compressed chunk directly. The output descriptor is taken from the call site's column definition list through get_call_result_type, letting the caller specify the shape of the decompressed rows in the AS t(...) clause. Internally the function is a value-per-call SRF that calls build_decompressor, deforms the input record into the decompressor's compressed_datums/_is_nulls arrays, runs decompress_batch once, and yields each tuple from decompressed_slots. row_decompressor_close runs on SRF_RETURN_DONE. Example: SELECT x.* FROM _timescaledb_internal.compress_hyper_2_2_chunk t LIMIT 1 CROSS JOIN LATERAL _timescaledb_functions.decompress_batch(t) AS x(time timestamptz, device_id int, value float);
1 parent ba6bc78 commit 71104b3

12 files changed

Lines changed: 414 additions & 11 deletions

File tree

.unreleased/pr_9684

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Implements: #9684 Add `decompress_batch` SQL function

sql/compression.sql

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,8 @@ CREATE OR REPLACE FUNCTION _timescaledb_functions.compressed_data_column_size(_t
1212
AS '@MODULE_PATHNAME@', 'ts_compressed_data_column_size'
1313
LANGUAGE C IMMUTABLE PARALLEL SAFE;
1414

15+
CREATE OR REPLACE FUNCTION _timescaledb_functions.decompress_batch(record)
16+
RETURNS SETOF record
17+
AS '@MODULE_PATHNAME@', 'ts_decompress_batch'
18+
LANGUAGE C STRICT;
19+

sql/updates/reverse-dev.sql

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,3 +293,4 @@ CREATE TABLE _timescaledb_catalog.telemetry_event (
293293
);
294294
GRANT SELECT ON _timescaledb_catalog.telemetry_event TO PUBLIC;
295295

296+
DROP FUNCTION IF EXISTS _timescaledb_functions.decompress_batch(record);

src/cross_module_fn.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ CROSSMODULE_WRAPPER(compressed_data_decompress_forward);
6060
CROSSMODULE_WRAPPER(compressed_data_decompress_reverse);
6161
CROSSMODULE_WRAPPER(compressed_data_column_size);
6262
CROSSMODULE_WRAPPER(compressed_data_to_array);
63+
CROSSMODULE_WRAPPER(decompress_batch);
6364
CROSSMODULE_WRAPPER(compressed_data_send);
6465
CROSSMODULE_WRAPPER(compressed_data_recv);
6566
CROSSMODULE_WRAPPER(compressed_data_in);
@@ -367,6 +368,7 @@ TSDLLEXPORT CrossModuleFunctions ts_cm_functions_default = {
367368
.compressed_data_decompress_reverse = error_no_default_fn_pg_community,
368369
.compressed_data_column_size = error_no_default_fn_pg_community,
369370
.compressed_data_to_array = error_no_default_fn_pg_community,
371+
.decompress_batch = error_no_default_fn_pg_community,
370372
.deltadelta_compressor_append = error_no_default_fn_pg_community,
371373
.deltadelta_compressor_finish = error_no_default_fn_pg_community,
372374
.gorilla_compressor_append = 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
@@ -149,6 +149,7 @@ typedef struct CrossModuleFunctions
149149
PGFunction compressed_data_decompress_reverse;
150150
PGFunction compressed_data_column_size;
151151
PGFunction compressed_data_to_array;
152+
PGFunction decompress_batch;
152153
PGFunction deltadelta_compressor_append;
153154
PGFunction deltadelta_compressor_finish;
154155
PGFunction gorilla_compressor_append;

tsl/src/compression/compression.c

Lines changed: 145 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include <access/attmap.h>
88
#include <access/attnum.h>
99
#include <access/detoast.h>
10+
#include <access/htup_details.h>
1011
#include <access/skey.h>
1112
#include <access/tupdesc.h>
1213
#include <catalog/heap.h>
@@ -146,7 +147,7 @@ static void row_compressor_process_ordered_slot(RowCompressor *row_compressor, T
146147
static void row_compressor_update_group(RowCompressor *row_compressor, TupleTableSlot *row);
147148
static bool row_compressor_new_row_is_in_new_group(RowCompressor *row_compressor,
148149
TupleTableSlot *row);
149-
static void create_per_compressed_column(RowDecompressor *decompressor);
150+
static void create_per_compressed_column(RowDecompressor *decompressor, bool internal_error);
150151
static void row_compressor_append_row(RowCompressor *row_compressor, TupleTableSlot *row);
151152
static void row_compressor_flush(RowCompressor *row_compressor, BulkWriter *writer,
152153
bool changed_groups);
@@ -2022,8 +2023,9 @@ bulk_writer_close(BulkWriter *writer)
20222023
** decompress_chunk **
20232024
**********************/
20242025

2025-
RowDecompressor
2026-
build_decompressor(const TupleDesc in_desc, const TupleDesc out_desc, Oid in_oid, Oid out_oid)
2026+
static inline RowDecompressor
2027+
build_decompressor_common(const TupleDesc in_desc, const TupleDesc out_desc, Oid in_oid,
2028+
Oid out_oid, bool internal_error)
20272029
{
20282030
AttrNumber count_meta_attnum = InvalidAttrNumber;
20292031
AttrMap *attrmap = build_decompress_attrmap(out_desc, in_desc, &count_meta_attnum);
@@ -2054,7 +2056,7 @@ build_decompressor(const TupleDesc in_desc, const TupleDesc out_desc, Oid in_oid
20542056
.attrmap = attrmap,
20552057
};
20562058

2057-
create_per_compressed_column(&decompressor);
2059+
create_per_compressed_column(&decompressor, internal_error);
20582060

20592061
/*
20602062
* We need to make sure decompressed_is_nulls is in a defined state. While this
@@ -2071,6 +2073,12 @@ build_decompressor(const TupleDesc in_desc, const TupleDesc out_desc, Oid in_oid
20712073
return decompressor;
20722074
}
20732075

2076+
RowDecompressor
2077+
build_decompressor(const TupleDesc in_desc, const TupleDesc out_desc, Oid in_oid, Oid out_oid)
2078+
{
2079+
return build_decompressor_common(in_desc, out_desc, in_oid, out_oid, true);
2080+
}
2081+
20742082
void
20752083
row_decompressor_init_stats(RowDecompressor *decompressor, Oid compressed_relid,
20762084
Oid uncompressed_relid, CmdType cmd_type)
@@ -2190,7 +2198,7 @@ decompress_chunk(Oid in_table, Oid out_table)
21902198
}
21912199

21922200
static void
2193-
create_per_compressed_column(RowDecompressor *decompressor)
2201+
create_per_compressed_column(RowDecompressor *decompressor, bool internal_error)
21942202
{
21952203
Oid compressed_data_type_oid = ts_custom_type_cache_get(CUSTOM_TYPE_COMPRESSED_DATA)->type_oid;
21962204
Assert(OidIsValid(compressed_data_type_oid));
@@ -2234,12 +2242,14 @@ create_per_compressed_column(RowDecompressor *decompressor)
22342242
is_compressed = compressed_attr->atttypid == compressed_data_type_oid;
22352243
if (!is_compressed && compressed_attr->atttypid != decompressed_type)
22362244
{
2237-
elog(ERROR,
2238-
"compressed table type '%s' does not match decompressed table type '%s' for "
2239-
"segment-by column \"%s\"",
2240-
format_type_be(compressed_attr->atttypid),
2241-
format_type_be(decompressed_type),
2242-
col_name);
2245+
ereport(ERROR,
2246+
(errcode(internal_error ? ERRCODE_INTERNAL_ERROR :
2247+
ERRCODE_INVALID_PARAMETER_VALUE),
2248+
errmsg("compressed table type '%s' does not match decompressed "
2249+
"table type '%s' for segment-by column \"%s\"",
2250+
format_type_be(compressed_attr->atttypid),
2251+
format_type_be(decompressed_type),
2252+
col_name)));
22432253
}
22442254

22452255
*per_compressed_col = (PerCompressedColumn){
@@ -2767,6 +2777,130 @@ tsl_compressed_data_decompress_reverse(PG_FUNCTION_ARGS)
27672777
;
27682778
}
27692779

2780+
/*
2781+
* decompress_batch(compressed_tuple record) RETURNS SETOF record
2782+
*
2783+
* Decompresses a single compressed batch (one row of a compressed chunk) into
2784+
* the individual rows it represents. The shape of the input compressed tuple
2785+
* is taken from the record's own type info (typeId/typmod in the header).
2786+
* The shape of the output rows is taken from the call site's column
2787+
* definition list (the AS t(...) clause).
2788+
*/
2789+
typedef struct DecompressBatchSRFContext
2790+
{
2791+
RowDecompressor decompressor;
2792+
int next_row;
2793+
int total_rows;
2794+
} DecompressBatchSRFContext;
2795+
2796+
Datum
2797+
tsl_decompress_batch(PG_FUNCTION_ARGS)
2798+
{
2799+
FuncCallContext *funcctx;
2800+
DecompressBatchSRFContext *decompress_ctx;
2801+
2802+
if (SRF_IS_FIRSTCALL())
2803+
{
2804+
funcctx = SRF_FIRSTCALL_INIT();
2805+
MemoryContext oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
2806+
2807+
TupleDesc out_desc;
2808+
if (get_call_result_type(fcinfo, NULL, &out_desc) != TYPEFUNC_COMPOSITE)
2809+
{
2810+
ereport(ERROR,
2811+
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
2812+
errmsg("function returning record called in context "
2813+
"that cannot accept type record")));
2814+
}
2815+
BlessTupleDesc(out_desc);
2816+
2817+
HeapTupleHeader td = PG_GETARG_HEAPTUPLEHEADER(0);
2818+
TupleDesc in_desc =
2819+
lookup_rowtype_tupdesc(HeapTupleHeaderGetTypeId(td), HeapTupleHeaderGetTypMod(td));
2820+
2821+
/*
2822+
* Verify that the input record actually looks like a compressed-chunk
2823+
* row before handing it to the decompressor. We are looking for the
2824+
* "_ts_meta_count" metadata column which the decompressor uses to
2825+
* identify the number of rows in the batch.
2826+
*/
2827+
bool has_count_column = false;
2828+
for (int i = 0; i < in_desc->natts; i++)
2829+
{
2830+
Form_pg_attribute attr = TupleDescAttr(in_desc, i);
2831+
2832+
if (attr->attisdropped)
2833+
{
2834+
continue;
2835+
}
2836+
2837+
if (strcmp(NameStr(attr->attname), COMPRESSION_COLUMN_METADATA_COUNT_NAME) == 0)
2838+
{
2839+
if (attr->atttypid != INT4OID)
2840+
{
2841+
ereport(ERROR,
2842+
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
2843+
errmsg("input record is not a compressed batch"),
2844+
errdetail("Column \"%s\" must have type integer.",
2845+
COMPRESSION_COLUMN_METADATA_COUNT_NAME)));
2846+
}
2847+
2848+
has_count_column = true;
2849+
break;
2850+
}
2851+
}
2852+
2853+
if (!has_count_column)
2854+
{
2855+
ReleaseTupleDesc(in_desc);
2856+
ereport(ERROR,
2857+
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
2858+
errmsg("input record is not a compressed batch"),
2859+
errdetail("A compressed batch must have a \"%s\" metadata column.",
2860+
COMPRESSION_COLUMN_METADATA_COUNT_NAME)));
2861+
}
2862+
2863+
decompress_ctx = palloc0(sizeof(DecompressBatchSRFContext));
2864+
decompress_ctx->decompressor =
2865+
build_decompressor_common(in_desc, out_desc, InvalidOid, InvalidOid, false);
2866+
2867+
HeapTupleData compressed_tuple;
2868+
compressed_tuple.t_len = HeapTupleHeaderGetDatumLength(td);
2869+
ItemPointerSetInvalid(&compressed_tuple.t_self);
2870+
compressed_tuple.t_tableOid = InvalidOid;
2871+
compressed_tuple.t_data = td;
2872+
2873+
heap_deform_tuple(&compressed_tuple,
2874+
in_desc,
2875+
decompress_ctx->decompressor.compressed_datums,
2876+
decompress_ctx->decompressor.compressed_is_nulls);
2877+
2878+
ReleaseTupleDesc(in_desc);
2879+
2880+
decompress_ctx->total_rows = decompress_batch(&decompress_ctx->decompressor);
2881+
decompress_ctx->next_row = 0;
2882+
2883+
funcctx->user_fctx = decompress_ctx;
2884+
funcctx->tuple_desc = decompress_ctx->decompressor.out_desc;
2885+
MemoryContextSwitchTo(oldcontext);
2886+
}
2887+
2888+
funcctx = SRF_PERCALL_SETUP();
2889+
decompress_ctx = (DecompressBatchSRFContext *) funcctx->user_fctx;
2890+
2891+
if (decompress_ctx->next_row >= decompress_ctx->total_rows)
2892+
{
2893+
row_decompressor_close(&decompress_ctx->decompressor);
2894+
SRF_RETURN_DONE(funcctx);
2895+
}
2896+
2897+
TupleTableSlot *slot =
2898+
decompress_ctx->decompressor.decompressed_slots[decompress_ctx->next_row++];
2899+
bool should_free;
2900+
HeapTuple tuple = ExecFetchSlotHeapTuple(slot, false, &should_free);
2901+
SRF_RETURN_NEXT(funcctx, HeapTupleGetDatum(tuple));
2902+
}
2903+
27702904
/*
27712905
* compressed_data_to_array(compressed_data, element_type) -> anyarray
27722906
*/

tsl/src/compression/compression.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,7 @@ extern Datum tsl_compressed_data_info(PG_FUNCTION_ARGS);
351351
extern Datum tsl_compressed_data_has_nulls(PG_FUNCTION_ARGS);
352352
extern Datum tsl_compressed_data_column_size(PG_FUNCTION_ARGS);
353353
extern Datum tsl_compressed_data_to_array(PG_FUNCTION_ARGS);
354+
extern Datum tsl_decompress_batch(PG_FUNCTION_ARGS);
354355

355356
static void
356357
pg_attribute_unused() assert_num_compression_algorithms_sane(void)

tsl/src/init.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ CrossModuleFunctions tsl_cm_functions = {
142142
.compressed_data_decompress_reverse = tsl_compressed_data_decompress_reverse,
143143
.compressed_data_column_size = tsl_compressed_data_column_size,
144144
.compressed_data_to_array = tsl_compressed_data_to_array,
145+
.decompress_batch = tsl_decompress_batch,
145146
.compressed_data_send = tsl_compressed_data_send,
146147
.compressed_data_recv = tsl_compressed_data_recv,
147148
.compressed_data_in = tsl_compressed_data_in,

0 commit comments

Comments
 (0)