-
Notifications
You must be signed in to change notification settings - Fork 94
Timescale hypertable BM25 scans fail when parent/chunk indexed-column attnums drift #288
Description
Bug Description
On a TimescaleDB hypertable, parent-table BM25 queries can fail with:
ERROR: tpquery index mismatch
HINT: Query specifies index OID <parent_oid> but scan is on index "<chunk_index>" (OID <child_oid>)The failure happens when the hypertable parent and its chunks have different physical attnum values for the indexed column, even though the column name is the same and the chunk inherits from the parent.
Direct chunk queries still work. Parent-table queries fail.
This appears to come from pg_textsearch matching parent/chunk BM25 indexes by raw indkey.values[0] / attnum rather than by column identity/name in the Timescale fallback paths.
Environment
- PostgreSQL: 18.3
- TimescaleDB: 2.25.2
- pg_textsearch extension version: 0.6.1
- pg_textsearch loaded library version: 0.6.1
- OS: Ubuntu 22.04
Observed Catalog State
On the affected database:
-- Parent hypertable
SELECT attnum, attname, attisdropped
FROM pg_attribute
WHERE attrelid = 'public.episodes'::regclass
AND attnum > 0
AND (attisdropped OR attname IN ('search_metadata', 'trigram_text'))
ORDER BY attnum;
-- Result
14 | ........pg.dropped.14........ | t
15 | search_metadata | f
16 | trigram_text | f-- Child chunk
SELECT attnum, attname, attisdropped
FROM pg_attribute
WHERE attrelid = '_timescaledb_internal._hyper_1_20_chunk'::regclass
AND attnum > 0
AND (attisdropped OR attname IN ('search_metadata', 'trigram_text'))
ORDER BY attnum;
-- Result
14 | search_metadata | f
15 | trigram_text | f-- Parent BM25 index uses attnum 16
SELECT c.relname, ix.indkey::text
FROM pg_index ix
JOIN pg_class c ON c.oid = ix.indexrelid
WHERE c.relname = 'idx_episodes_trigram_text_bm25';
-- Result
idx_episodes_trigram_text_bm25 | 16-- Chunk BM25 index uses attnum 15
SELECT c.relname, ix.indkey::text
FROM pg_index ix
JOIN pg_class c ON c.oid = ix.indexrelid
WHERE c.relname = '_hyper_1_20_chunk_idx_episodes_trigram_text_bm25';
-- Result
_hyper_1_20_chunk_idx_episodes_trigram_text_bm25 | 15The chunk table still inherits from the parent table:
SELECT c.oid, c.relname, p.oid AS parent_oid, p.relname AS parent_relname
FROM pg_class c
LEFT JOIN pg_inherits i ON i.inhrelid = c.oid
LEFT JOIN pg_class p ON p.oid = i.inhparent
WHERE c.relname IN ('episodes', '_hyper_1_20_chunk')
ORDER BY c.oid;Queries
This parent-table query fails:
SELECT id, trigram_text <@> 'memory store'
FROM public.episodes
WHERE store_id = 4
AND created_at >= '2026-02-25 00:00:00+00'
AND created_at < '2026-02-26 00:00:00+00'
AND COALESCE(trigram_text, '') <> ''
ORDER BY trigram_text <@> 'memory store' ASC
LIMIT 5;EXPLAIN shows a chunk-local BM25 index scan:
Limit
-> Result
-> Index Scan using _hyper_1_20_chunk_idx_episodes_trigram_text_bm25 on _hyper_1_20_chunk
Order By: (trigram_text <@> 'idx_episodes_trigram_text_bm25:memory store'::bm25query)
Filter: ...But the query errors with tpquery index mismatch.
The same query directly against the chunk table works and returns non-zero BM25 scores:
SELECT id, trigram_text <@> 'memory store'
FROM _timescaledb_internal._hyper_1_20_chunk
WHERE store_id = 4
AND COALESCE(trigram_text, '') <> ''
ORDER BY trigram_text <@> 'memory store' ASC
LIMIT 5;Relevant Upstream Code Paths
This looks like the attnum comparison in two places:
src/am/scan.c::indexes_match_by_attribute()- compares
scan_attnum == query_attnum
- compares
src/types/query.c- child-index selection for a parent index also compares
child_idx_form->indkey.values[0] != indexed_attnum
- child-index selection for a parent index also compares
In the affected database, those checks fail even though the chunk index is the correct BM25 index for the same inherited column name.
Expected Behavior
For Timescale hypertables, if the child table inherits from the parent and both indexes are BM25 indexes on the same logical column name, the scan should be allowed even if raw physical attnum values differ.
Actual Behavior
Parent-table BM25 scans fail with tpquery index mismatch.
Notes
- This does not reproduce on another environment with the same PostgreSQL / TimescaleDB / pg_textsearch versions where the parent and chunk
attnumvalues still match. - So this does not look like a generic hypertable failure in 0.6.1; it looks like a schema-drift edge case that the current parent/chunk matching logic does not tolerate.
- I do not have a clean SQL-only reproduction for creating the parent/chunk attnum drift on a fresh cluster. The drift appears to be the result of historical schema evolution on the hypertable. But once the drift exists, the failure is deterministic.
Suggested Fix
For the Timescale fallback paths, match parent/chunk BM25 indexes by:
- BM25 access method
- child table inherits from parent table
- indexed column name (or a more robust logical-column identity), not raw
attnum
Using raw attnum seems too brittle for historical hypertables where parent and chunk physical layouts can diverge.