Skip to content

Add decompress_batch SQL function - #9684

Merged
zilder merged 1 commit into
timescale:mainfrom
zilder:zilder/decompress-batch
Jun 12, 2026
Merged

Add decompress_batch SQL function#9684
zilder merged 1 commit into
timescale:mainfrom
zilder:zilder/decompress-batch

Conversation

@zilder

@zilder zilder commented Apr 29, 2026

Copy link
Copy Markdown
Member

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);

Comment thread tsl/src/compression/compression.c Outdated
* The shape of the output rows is taken from the call site's column
* definition list (the AS t(...) clause).
*/
typedef struct DecompressBatchState

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's name it something else, we already have a struct named like this and it will be confusing.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed to DecompressBatchSRFContext.

@zilder
zilder force-pushed the zilder/decompress-batch branch 5 times, most recently from 5985824 to 4e24375 Compare May 4, 2026 21:12
@codecov

codecov Bot commented May 4, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.15686% with 4 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
tsl/src/compression/compression.c 92.00% 2 Missing and 2 partials ⚠️

📢 Thoughts on this report? Let us know!

@zilder
zilder force-pushed the zilder/decompress-batch branch from 4e24375 to 6c87475 Compare May 5, 2026 14:14
@zilder
zilder requested a review from akuzm May 5, 2026 14:15
@zilder
zilder marked this pull request as ready for review May 5, 2026 14:15
@zilder
zilder requested a review from a team May 5, 2026 14:15
@github-actions
github-actions Bot requested review from melihmutlu and pnthao May 5, 2026 14:16
@github-actions

github-actions Bot commented May 5, 2026

Copy link
Copy Markdown

@pnthao, @melihmutlu: please review this pull request.

Powered by pull-review

@zilder
zilder force-pushed the zilder/decompress-batch branch 2 times, most recently from d5e354e to bff9fb7 Compare May 22, 2026 14:51
@dbeck
dbeck self-requested a review May 26, 2026 11:35
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)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you specify a wrong type, how does it handle it? It must be relatively robust since this is a user-callable function.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Force push to main repo to branch trigger/llm-fuzzer. I think you should have permissions.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, it works, thanks!

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's like an assertion failure basically.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

XX001 is OK to return for corrupt data, we only treat XX000 as bug.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

@zilder
zilder force-pushed the zilder/decompress-batch branch 3 times, most recently from 9436962 to 742fabd Compare May 29, 2026 15:42

@dbeck dbeck left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be great to have at least some smoke tests with invalid compressed blobs:

  • garbage
  • truncated

Comment thread sql/updates/latest-dev.sql Outdated
Comment on lines +53 to +57
CREATE OR REPLACE FUNCTION _timescaledb_functions.decompress_batch(record)
RETURNS SETOF record
AS '@MODULE_PATHNAME@', 'ts_update_placeholder'
LANGUAGE C STRICT;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed it from the latest-dev.sql. What about reverse-dev.sql, do we drop functions there?

@zilder
zilder force-pushed the zilder/decompress-batch branch 2 times, most recently from 3ec3bfb to a224651 Compare June 1, 2026 10:33
@zilder
zilder force-pushed the zilder/decompress-batch branch from a224651 to 7739c54 Compare June 1, 2026 12:30
@zilder
zilder requested review from akuzm and svenklemm June 1, 2026 13:12
@zilder
zilder force-pushed the zilder/decompress-batch branch 5 times, most recently from 65ee8ce to 82a9413 Compare June 11, 2026 14:37
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);
@zilder
zilder force-pushed the zilder/decompress-batch branch from 82a9413 to 71104b3 Compare June 12, 2026 07:25
@zilder
zilder merged commit 431ecc6 into timescale:main Jun 12, 2026
63 checks passed
@surister surister mentioned this pull request Jul 7, 2026
@surister surister mentioned this pull request Jul 21, 2026
surister pushed a commit that referenced this pull request Jul 28, 2026
## 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
svenklemm pushed a commit that referenced this pull request Jul 28, 2026
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
surister pushed a commit to surister/timescaledb that referenced this pull request Jul 28, 2026
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
@timescale-automation timescale-automation added the released-2.29.0 Released in 2.29.0 label Jul 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

released-2.29.0 Released in 2.29.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants