Add decompress_batch SQL function - #9684
Conversation
| * The shape of the output rows is taken from the call site's column | ||
| * definition list (the AS t(...) clause). | ||
| */ | ||
| typedef struct DecompressBatchState |
There was a problem hiding this comment.
Let's name it something else, we already have a struct named like this and it will be confusing.
There was a problem hiding this comment.
Renamed to DecompressBatchSRFContext.
5985824 to
4e24375
Compare
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
4e24375 to
6c87475
Compare
|
@pnthao, @melihmutlu: please review this pull request.
|
d5e354e to
bff9fb7
Compare
| SELECT decomp.time, decomp.device_id, decomp.value | ||
| FROM (SELECT t FROM :compressed_chunk t WHERE t.device_id = 1) comp, | ||
| LATERAL _timescaledb_functions.decompress_batch(comp.t) | ||
| AS decomp(time timestamptz, device_id int, value float) |
There was a problem hiding this comment.
If you specify a wrong type, how does it handle it? It must be relatively robust since this is a user-callable function.
There was a problem hiding this comment.
Looks like it's easy to crash: https://github.com/timescale/timescaledb/actions/runs/26507892002
There was a problem hiding this comment.
I added a check for the _ts_meta_count column in the input record and some more tests.
Bwt how do you run LLM fuzzer? The CI in the PR seems to skip this workflow and I can't run it manually for the branch that is not in the official repo (my branch is in my own fork)
There was a problem hiding this comment.
Force push to main repo to branch trigger/llm-fuzzer. I think you should have permissions.
There was a problem hiding this comment.
XX000 is reserved for bugs. If it's triggered by invalid user input, it must use some other error code. If you add this fuzzer-generated test to the regression test, you'll see that it also complains about XX000 which is supposed to be impossible to hit.
There was a problem hiding this comment.
It's like an assertion failure basically.
There was a problem hiding this comment.
If in this particular case I could write a custom implementation of create_per_compressed_column that throws user-level error, there are still lot's of locations in the code that can throw XX001 (ERRCODE_DATA_CORRUPTED which is another internal error) throughout the compression code -- a couple of examples of these are in decompress_batch.sql (i.e. user can trigger them by passing specially crafted input record and the output format).
It seems to me that the only realistic way I can go around this is to catch ERRORs and re-wrap it into another "user-level" error. It's not great but this is supposed to be an internal function.
There was a problem hiding this comment.
XX001 is OK to return for corrupt data, we only treat XX000 as bug.
There was a problem hiding this comment.
I fixed this one error in create_per_compressed_column (it chooses different errcodes depending whether the function is called by an internal caller of from the user-facing decompress_batch). But there are functions deep in the algorithms still that can throw internal errors (XX000). In this sense decompress_batch is no different from existing compressed_data_to_array:
\set VERBOSITY sqlstate
SELECT _timescaledb_functions.compressed_data_to_array('BAAAAs2YDGOgAAAAAAAKupUAAAAACgAAAAMAAAAAAAAB7gAFmy9XqMYAAAWbL0Izm/8AAAAAAAAAAA==', NULL::float8);
ERROR: XX000
SELECT decomp.*
FROM (VALUES (1, 2, 'BAAAAs2YDGOgAAAAAAAKupUAAAAACgAAAAMAAAAAAAAB7gAFmy9XqMYAAAWbL0Izm/8AAAAAAAAAAA=='::_timescaledb_internal.compressed_data)) AS r(_ts_meta_count, a, b),
LATERAL _timescaledb_functions.decompress_batch(r)
AS decomp(a int, b float8);
ERROR: XX000(here a compressed timestamptz array is decompressed as float8).
9436962 to
742fabd
Compare
dbeck
left a comment
There was a problem hiding this comment.
It would be great to have at least some smoke tests with invalid compressed blobs:
- garbage
- truncated
| CREATE OR REPLACE FUNCTION _timescaledb_functions.decompress_batch(record) | ||
| RETURNS SETOF record | ||
| AS '@MODULE_PATHNAME@', 'ts_update_placeholder' | ||
| LANGUAGE C STRICT; | ||
|
|
There was a problem hiding this comment.
| CREATE OR REPLACE FUNCTION _timescaledb_functions.decompress_batch(record) | |
| RETURNS SETOF record | |
| AS '@MODULE_PATHNAME@', 'ts_update_placeholder' | |
| LANGUAGE C STRICT; |
New functions dont need to be added to latest-dev
There was a problem hiding this comment.
I removed it from the latest-dev.sql. What about reverse-dev.sql, do we drop functions there?
3ec3bfb to
a224651
Compare
a224651 to
7739c54
Compare
65ee8ce to
82a9413
Compare
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);
82a9413 to
71104b3
Compare
## 2.29.0 (2026-07-28) This release contains performance improvements and bug fixes since the 2.28.3 release. We recommend that you upgrade at the next available opportunity. **Release Highlights** * **Chunk exclusion for DML operations** drastically improves the performance of `UPDATE` and `DELETE` statements on hypertables. By acquiring exclusive locks only on the specific chunks being modified rather than the entire hypertable, this enhancement eliminates massive lock contention and keeps high-concurrency workloads running smoothly without unnecessary slowdowns. * Intelligent **row-by-row decompression** enables the query planner to decompress data row-by-row rather than in large batches when an operation prioritizes a fast initial response (such as queries with `LIMIT` clauses). This dramatically reduces memory overhead and query latency, ensuring lightning-fast performance when you only need to retrieve a small subset of records from your compressed hypertables. **Important: PostgreSQL 15 Support Removed** TimescaleDB 2.29.0 removes support for PostgreSQL 15. This release supports PostgreSQL 16, 17, and 18. If you are still running PostgreSQL 15, upgrade PostgreSQL before upgrading to TimescaleDB 2.29.0. **Backward-Incompatible Changes** * [#10041](#10041) Remove support for PostgreSQL 15 **Features** * [#9315](#9315) Speed up `DML` operations on hypertables by using the optimized TimescaleDB hypertable expansion code instead of the generic PostgreSQL inheritance hierarchy expansion * [#9534](#9534) Speed up expression evaluation in the columnar pipeline by caching common subexpressions * [#9684](#9684) Add `_timescaledb_functions.decompress_batch()` SQL function * [#9732](#9732) Speed up some queries with small `LIMIT` by switching to row-by-row query execution pipeline * [#9917](#9917) Decompress less data in `DML` on compressed hypertables by accounting for prepared statement parameters * [#9957](#9957) Add `compact_chunk()` function * [#10048](#10048) Support concurrent refresh policies on hierarchical continuous aggregates * [#10081](#10081) Add `samplerate` argument to `_timescaledb_functions.estimate_uncompressed_size()` * [#10100](#10100) Skip classifying compressed relations to speed up planning * [#10118](#10118) Don't track compressed relations as separate chunk * [#10119](#10119) Reduce memory usage of `INSERT` queries using direct compress and spanning multiple chunks * [#10163](#10163) Add a compaction policy for unordered chunks * [#10204](#10204) Don't create separate hypertable catalog entry for hypertables with compression * [#10217](#10217) Initial placeholder version of granular refresh API * [#10225](#10225) Add `config_merge` parameter to `alter_job()` for merging `jsonb` into the existing job configuration * [#10226](#10226) Add `recompress_unordered` columnstore policy option * [#10231](#10231) Use `regclass` for storing relation reference in chunk table * [#10237](#10237) Add helper functions for decoding hypertable status * [#10240](#10240) Add the `tsdb.direct_compress` storage parameter that allows enabling direct compress for a given hypertable independent of global settings * [#10266](#10266) Add `max_batches` to `compact_chunk()` * [#10299](#10299) Add `continuous_aggs_tenant_tracking` and `hypertable_cagg_settings` catalogs **Bugfixes** * [#10013](#10013) Make ownership error messages on continuous aggregates consistent * [#10052](#10052) Result of `MIN` / `MAX` aggregate functions in columnar aggregation pipeline possibly inconsistent with plain PostgreSQL result * [#10071](#10071) Prune the real-time branch of hierarchical continuous aggregates at any nesting depth * [#10143](#10143) Fix division by zero when planning `time_bucket` with zero width * [#10199](#10199) Fix `initial_start` handling in `build_job_info` * [#10213](#10213) Cache sort pathkeys per hypertable * [#10221](#10221) Fix incremental refresh skipping the last bucket * [#10278](#10278) Drop `job_errors` view in `bgw_job_stat_history` migration * [#10280](#10280) `RETURNING` clause returned no rows for `INSERT` using direct compress * [#10281](#10281) Disable direct compress when the destination table has an exclusion constraint so the constraint is still enforced * [#10282](#10282) Only count directly compressed rows toward the command tag when the `INSERT` sets it * [#10286](#10286) Propagate `VACUUM` on a chunk to the compressed relation when running on the chunk directly * [#10302](#10302) Fix useless-join removal and self-join elimination for hypertables * [#10313](#10313) Allow running `ALTER EXTENSION timescaledb UPDATE` inside a transaction block * [#10315](#10315) Fix overlap detection with running max * [#10324](#10324) Fix stale index entries after `rebuild_sparse_index()` on compressed chunks **GUCs** * `timescaledb.enable_hypertable_expansion_for_dml`: allow using the optimized TimescaleDB hypertable expansion code for `UPDATE` and `DELETE` instead of the generic PostgreSQL inheritance hierarchy expansion. On by default. **Thanks** * @FrancescEthon and @ManuelEthon for reporting the issue * @h0rn3t for reporting a problem with `VACUUM` not propagating to the compressed relation * @MaximeEthon for reporting an issue with prepared statement parameters in DML decompression * @proddata for reporting a problem when upgrading from 2.15.3 to 2.28.2 * @tureba for reporting and fixing stale sparse-index entries after rebuild * @viniciusrsouza for reporting an issue with hierarchical continuous aggregates
This release contains performance improvements and bug fixes since the 2.28.3 release. We recommend that you upgrade at the next available opportunity. **Release Highlights** * **Chunk exclusion for DML operations** drastically improves the performance of `UPDATE` and `DELETE` statements on hypertables. By acquiring exclusive locks only on the specific chunks being modified rather than the entire hypertable, this enhancement eliminates massive lock contention and keeps high-concurrency workloads running smoothly without unnecessary slowdowns. * Intelligent **row-by-row decompression** enables the query planner to decompress data row-by-row rather than in large batches when an operation prioritizes a fast initial response (such as queries with `LIMIT` clauses). This dramatically reduces memory overhead and query latency, ensuring lightning-fast performance when you only need to retrieve a small subset of records from your compressed hypertables. **Important: PostgreSQL 15 Support Removed** TimescaleDB 2.29.0 removes support for PostgreSQL 15. This release supports PostgreSQL 16, 17, and 18. If you are still running PostgreSQL 15, upgrade PostgreSQL before upgrading to TimescaleDB 2.29.0. **Backward-Incompatible Changes** * [#10041](#10041) Remove support for PostgreSQL 15 **Features** * [#9315](#9315) Speed up `DML` operations on hypertables by using the optimized TimescaleDB hypertable expansion code instead of the generic PostgreSQL inheritance hierarchy expansion * [#9534](#9534) Speed up expression evaluation in the columnar pipeline by caching common subexpressions * [#9684](#9684) Add `_timescaledb_functions.decompress_batch()` SQL function * [#9732](#9732) Speed up some queries with small `LIMIT` by switching to row-by-row query execution pipeline * [#9917](#9917) Decompress less data in `DML` on compressed hypertables by accounting for prepared statement parameters * [#9957](#9957) Add `compact_chunk()` function * [#10048](#10048) Support concurrent refresh policies on hierarchical continuous aggregates * [#10081](#10081) Add `samplerate` argument to `_timescaledb_functions.estimate_uncompressed_size()` * [#10100](#10100) Skip classifying compressed relations to speed up planning * [#10118](#10118) Don't track compressed relations as separate chunk * [#10119](#10119) Reduce memory usage of `INSERT` queries using direct compress and spanning multiple chunks * [#10163](#10163) Add a compaction policy for unordered chunks * [#10204](#10204) Don't create separate hypertable catalog entry for hypertables with compression * [#10217](#10217) Initial placeholder version of granular refresh API * [#10225](#10225) Add `config_merge` parameter to `alter_job()` for merging `jsonb` into the existing job configuration * [#10226](#10226) Add `recompress_unordered` columnstore policy option * [#10231](#10231) Use `regclass` for storing relation reference in chunk table * [#10237](#10237) Add helper functions for decoding hypertable status * [#10240](#10240) Add the `tsdb.direct_compress` storage parameter that allows enabling direct compress for a given hypertable independent of global settings * [#10266](#10266) Add `max_batches` to `compact_chunk()` * [#10299](#10299) Add `continuous_aggs_tenant_tracking` and `hypertable_cagg_settings` catalogs **Bugfixes** * [#10013](#10013) Make ownership error messages on continuous aggregates consistent * [#10052](#10052) Result of `MIN` / `MAX` aggregate functions in columnar aggregation pipeline possibly inconsistent with plain PostgreSQL result * [#10071](#10071) Prune the real-time branch of hierarchical continuous aggregates at any nesting depth * [#10143](#10143) Fix division by zero when planning `time_bucket` with zero width * [#10199](#10199) Fix `initial_start` handling in `build_job_info` * [#10213](#10213) Cache sort pathkeys per hypertable * [#10221](#10221) Fix incremental refresh skipping the last bucket * [#10278](#10278) Drop `job_errors` view in `bgw_job_stat_history` migration * [#10280](#10280) `RETURNING` clause returned no rows for `INSERT` using direct compress * [#10281](#10281) Disable direct compress when the destination table has an exclusion constraint so the constraint is still enforced * [#10282](#10282) Only count directly compressed rows toward the command tag when the `INSERT` sets it * [#10286](#10286) Propagate `VACUUM` on a chunk to the compressed relation when running on the chunk directly * [#10302](#10302) Fix useless-join removal and self-join elimination for hypertables * [#10313](#10313) Allow running `ALTER EXTENSION timescaledb UPDATE` inside a transaction block * [#10315](#10315) Fix overlap detection with running max * [#10324](#10324) Fix stale index entries after `rebuild_sparse_index()` on compressed chunks **GUCs** * `timescaledb.enable_hypertable_expansion_for_dml`: allow using the optimized TimescaleDB hypertable expansion code for `UPDATE` and `DELETE` instead of the generic PostgreSQL inheritance hierarchy expansion. On by default. **Thanks** * @FrancescEthon and @ManuelEthon for reporting an issue with incremental refresh skipping the last bucket * @h0rn3t for reporting a problem with `VACUUM` not propagating to the * @igor2x for reporting an issue with locking during DML statements on hypertables compressed relation * @MaximeEthon for reporting an issue with prepared statement parameters in DML decompression * @proddata for reporting a problem when upgrading from 2.15.3 to 2.28.2 * @tureba for reporting and fixing stale sparse-index entries after rebuild * @viniciusrsouza for reporting an issue with hierarchical continuous aggregates
This release contains performance improvements and bug fixes since the 2.28.3 release. We recommend that you upgrade at the next available opportunity. **Release Highlights** * **Chunk exclusion for DML operations** drastically improves the performance of `UPDATE` and `DELETE` statements on hypertables. By acquiring exclusive locks only on the specific chunks being modified rather than the entire hypertable, this enhancement eliminates massive lock contention and keeps high-concurrency workloads running smoothly without unnecessary slowdowns. * Intelligent **row-by-row decompression** enables the query planner to decompress data row-by-row rather than in large batches when an operation prioritizes a fast initial response (such as queries with `LIMIT` clauses). This dramatically reduces memory overhead and query latency, ensuring lightning-fast performance when you only need to retrieve a small subset of records from your compressed hypertables. **Important: PostgreSQL 15 Support Removed** TimescaleDB 2.29.0 removes support for PostgreSQL 15. This release supports PostgreSQL 16, 17, and 18. If you are still running PostgreSQL 15, upgrade PostgreSQL before upgrading to TimescaleDB 2.29.0. **Backward-Incompatible Changes** * [timescale#10041](timescale#10041) Remove support for PostgreSQL 15 **Features** * [timescale#9315](timescale#9315) Speed up `DML` operations on hypertables by using the optimized TimescaleDB hypertable expansion code instead of the generic PostgreSQL inheritance hierarchy expansion * [timescale#9534](timescale#9534) Speed up expression evaluation in the columnar pipeline by caching common subexpressions * [timescale#9684](timescale#9684) Add `_timescaledb_functions.decompress_batch()` SQL function * [timescale#9732](timescale#9732) Speed up some queries with small `LIMIT` by switching to row-by-row query execution pipeline * [timescale#9917](timescale#9917) Decompress less data in `DML` on compressed hypertables by accounting for prepared statement parameters * [timescale#9957](timescale#9957) Add `compact_chunk()` function * [timescale#10048](timescale#10048) Support concurrent refresh policies on hierarchical continuous aggregates * [timescale#10081](timescale#10081) Add `samplerate` argument to `_timescaledb_functions.estimate_uncompressed_size()` * [timescale#10100](timescale#10100) Skip classifying compressed relations to speed up planning * [timescale#10118](timescale#10118) Don't track compressed relations as separate chunk * [timescale#10119](timescale#10119) Reduce memory usage of `INSERT` queries using direct compress and spanning multiple chunks * [timescale#10163](timescale#10163) Add a compaction policy for unordered chunks * [timescale#10204](timescale#10204) Don't create separate hypertable catalog entry for hypertables with compression * [timescale#10217](timescale#10217) Initial placeholder version of granular refresh API * [timescale#10225](timescale#10225) Add `config_merge` parameter to `alter_job()` for merging `jsonb` into the existing job configuration * [timescale#10226](timescale#10226) Add `recompress_unordered` columnstore policy option * [timescale#10231](timescale#10231) Use `regclass` for storing relation reference in chunk table * [timescale#10237](timescale#10237) Add helper functions for decoding hypertable status * [timescale#10240](timescale#10240) Add the `tsdb.direct_compress` storage parameter that allows enabling direct compress for a given hypertable independent of global settings * [timescale#10266](timescale#10266) Add `max_batches` to `compact_chunk()` * [timescale#10299](timescale#10299) Add `continuous_aggs_tenant_tracking` and `hypertable_cagg_settings` catalogs **Bugfixes** * [timescale#10013](timescale#10013) Make ownership error messages on continuous aggregates consistent * [timescale#10052](timescale#10052) Result of `MIN` / `MAX` aggregate functions in columnar aggregation pipeline possibly inconsistent with plain PostgreSQL result * [timescale#10071](timescale#10071) Prune the real-time branch of hierarchical continuous aggregates at any nesting depth * [timescale#10143](timescale#10143) Fix division by zero when planning `time_bucket` with zero width * [timescale#10199](timescale#10199) Fix `initial_start` handling in `build_job_info` * [timescale#10213](timescale#10213) Cache sort pathkeys per hypertable * [timescale#10221](timescale#10221) Fix incremental refresh skipping the last bucket * [timescale#10278](timescale#10278) Drop `job_errors` view in `bgw_job_stat_history` migration * [timescale#10280](timescale#10280) `RETURNING` clause returned no rows for `INSERT` using direct compress * [timescale#10281](timescale#10281) Disable direct compress when the destination table has an exclusion constraint so the constraint is still enforced * [timescale#10282](timescale#10282) Only count directly compressed rows toward the command tag when the `INSERT` sets it * [timescale#10286](timescale#10286) Propagate `VACUUM` on a chunk to the compressed relation when running on the chunk directly * [timescale#10302](timescale#10302) Fix useless-join removal and self-join elimination for hypertables * [timescale#10313](timescale#10313) Allow running `ALTER EXTENSION timescaledb UPDATE` inside a transaction block * [timescale#10315](timescale#10315) Fix overlap detection with running max * [timescale#10324](timescale#10324) Fix stale index entries after `rebuild_sparse_index()` on compressed chunks **GUCs** * `timescaledb.enable_hypertable_expansion_for_dml`: allow using the optimized TimescaleDB hypertable expansion code for `UPDATE` and `DELETE` instead of the generic PostgreSQL inheritance hierarchy expansion. On by default. **Thanks** * @FrancescEthon and @ManuelEthon for reporting an issue with incremental refresh skipping the last bucket * @h0rn3t for reporting a problem with `VACUUM` not propagating to the * @igor2x for reporting an issue with locking during DML statements on hypertables compressed relation * @MaximeEthon for reporting an issue with prepared statement parameters in DML decompression * @proddata for reporting a problem when upgrading from 2.15.3 to 2.28.2 * @tureba for reporting and fixing stale sparse-index entries after rebuild * @viniciusrsouza for reporting an issue with hierarchical continuous aggregates
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: