-
Notifications
You must be signed in to change notification settings - Fork 3
Harden exporter state and add follow-up TAP tests #53
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,168 @@ | ||||||||||||||
| #!/usr/bin/env perl | ||||||||||||||
| # Test: Squashed IN-list normalization (PG18+ only) | ||||||||||||||
| # | ||||||||||||||
| # PG18 introduced "squashed" constant lists: SELECT * FROM t WHERE id IN (1,2,3) | ||||||||||||||
| # gets a single LocationLen entry with squashed=true that covers the entire list. | ||||||||||||||
| # The normalized output should be: ... IN ($1 /*, ... */) | ||||||||||||||
| # | ||||||||||||||
| # This test verifies that FillInConstantLengths correctly computes the token | ||||||||||||||
| # length for squashed constants, producing clean normalized output. | ||||||||||||||
|
|
||||||||||||||
| use strict; | ||||||||||||||
| use warnings; | ||||||||||||||
| use lib 't'; | ||||||||||||||
|
|
||||||||||||||
| use PostgreSQL::Test::BackgroundPsql; | ||||||||||||||
| use PostgreSQL::Test::Cluster; | ||||||||||||||
| use PostgreSQL::Test::Utils; | ||||||||||||||
| use Test::More; | ||||||||||||||
|
|
||||||||||||||
| use psch; | ||||||||||||||
|
|
||||||||||||||
| # Skip if Docker/ClickHouse not available | ||||||||||||||
| if (!psch_clickhouse_available()) { | ||||||||||||||
| plan skip_all => 'Docker not available'; | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| my $ch_check = `curl -s 'http://localhost:18123/' --data 'SELECT 1' 2>/dev/null`; | ||||||||||||||
| if ($ch_check !~ /^1/) { | ||||||||||||||
| plan skip_all => 'ClickHouse container not running'; | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| psch_query_clickhouse("TRUNCATE TABLE IF EXISTS pg_stat_ch.events_raw"); | ||||||||||||||
|
|
||||||||||||||
| my $node = psch_init_node_with_clickhouse('squashed_in', | ||||||||||||||
| flush_interval_ms => 100, | ||||||||||||||
| batch_max => 100 | ||||||||||||||
| ); | ||||||||||||||
|
|
||||||||||||||
| # Detect PG version — squashed IN lists are PG18+ only | ||||||||||||||
| my $pg_version = $node->safe_psql('postgres', | ||||||||||||||
| 'SHOW server_version_num'); | ||||||||||||||
| chomp($pg_version); | ||||||||||||||
| if ($pg_version < 180000) { | ||||||||||||||
| plan skip_all => "Squashed IN-list normalization requires PG18+ (have $pg_version)"; | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| $node->safe_psql('postgres', 'CREATE TABLE test_squash(id int, val text)'); | ||||||||||||||
| $node->safe_psql('postgres', | ||||||||||||||
| "INSERT INTO test_squash SELECT g, 'v' || g FROM generate_series(1,20) g"); | ||||||||||||||
|
|
||||||||||||||
| # Helper: run a query, flush, and return the captured query text. | ||||||||||||||
| sub get_captured_query { | ||||||||||||||
| my ($node, $sql) = @_; | ||||||||||||||
|
|
||||||||||||||
| psch_query_clickhouse("TRUNCATE TABLE pg_stat_ch.events_raw"); | ||||||||||||||
| psch_reset_stats($node); | ||||||||||||||
|
|
||||||||||||||
| my $session = $node->background_psql('postgres', on_error_stop => 1); | ||||||||||||||
| my ($stdout, $ret) = $session->query('SELECT pg_backend_pid()'); | ||||||||||||||
| die "failed to get backend pid" unless $ret == 0; | ||||||||||||||
| my ($pid) = $stdout =~ /(\d+)/; | ||||||||||||||
| die "backend pid missing" unless defined $pid; | ||||||||||||||
|
|
||||||||||||||
| ($stdout, $ret) = $session->query($sql); | ||||||||||||||
| die "query failed: $sql" unless $ret == 0; | ||||||||||||||
|
|
||||||||||||||
| ($stdout, $ret) = $session->query('SELECT pg_stat_ch_flush()'); | ||||||||||||||
| die "flush failed" unless $ret == 0; | ||||||||||||||
| $session->quit(); | ||||||||||||||
|
|
||||||||||||||
| psch_wait_for_export($node, 1, 10); | ||||||||||||||
|
|
||||||||||||||
| return psch_wait_for_clickhouse_query( | ||||||||||||||
| "SELECT query FROM pg_stat_ch.events_raw " . | ||||||||||||||
| "WHERE pid = $pid " . | ||||||||||||||
| "AND query NOT LIKE '%pg_stat_ch%' " . | ||||||||||||||
| "AND query NOT LIKE '%pg_extension%' " . | ||||||||||||||
| "AND query != '' " . | ||||||||||||||
| "ORDER BY ts_start DESC LIMIT 1", | ||||||||||||||
| sub { $_[0] ne '' }, | ||||||||||||||
| 10 | ||||||||||||||
| ); | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| # --------------------------------------------------------------------------- | ||||||||||||||
| # Test 1: Basic IN list with numeric constants gets squashed | ||||||||||||||
| # --------------------------------------------------------------------------- | ||||||||||||||
| subtest 'IN list with numerics is squashed' => sub { | ||||||||||||||
| my $q = get_captured_query($node, | ||||||||||||||
| "SELECT * FROM test_squash WHERE id IN (1, 2, 3, 4, 5)"); | ||||||||||||||
|
|
||||||||||||||
| like($q, qr/\$\d/, 'IN list has placeholder'); | ||||||||||||||
| # The squashed form should collapse the entire list into one placeholder | ||||||||||||||
| like($q, qr{/\*, \.\.\. \*/}, 'Squashed comment marker present'); | ||||||||||||||
| # The original comma-separated list should be gone | ||||||||||||||
| unlike($q, qr/\d\s*,\s*\d/, 'No comma-separated raw numbers'); | ||||||||||||||
| like($q, qr/IN\s*\(/, 'IN keyword and opening paren preserved'); | ||||||||||||||
| # The full squashed form: IN ($N /*, ... */) with nothing else inside parens | ||||||||||||||
| like($q, qr/IN\s*\(\s*\$\d+\s+\/\*,\s*\.\.\.\s*\*\/\s*\)/, | ||||||||||||||
| 'IN list fully squashed to single placeholder with comment'); | ||||||||||||||
| }; | ||||||||||||||
|
|
||||||||||||||
| # --------------------------------------------------------------------------- | ||||||||||||||
| # Test 2: IN list with string constants gets squashed | ||||||||||||||
| # --------------------------------------------------------------------------- | ||||||||||||||
| subtest 'IN list with strings is squashed' => sub { | ||||||||||||||
| my $q = get_captured_query($node, | ||||||||||||||
| "SELECT * FROM test_squash WHERE val IN ('alpha', 'beta', 'gamma')"); | ||||||||||||||
|
|
||||||||||||||
| like($q, qr/\$\d/, 'IN list has placeholder'); | ||||||||||||||
| like($q, qr{/\*, \.\.\. \*/}, 'Squashed comment marker present'); | ||||||||||||||
| unlike($q, qr/alpha/, 'String constant alpha removed'); | ||||||||||||||
| unlike($q, qr/beta/, 'String constant beta removed'); | ||||||||||||||
| unlike($q, qr/gamma/, 'String constant gamma removed'); | ||||||||||||||
| }; | ||||||||||||||
|
|
||||||||||||||
| # --------------------------------------------------------------------------- | ||||||||||||||
| # Test 3: Mixed query — IN list squashed AND other constants normalized | ||||||||||||||
| # --------------------------------------------------------------------------- | ||||||||||||||
| subtest 'IN list squashed alongside regular constant' => sub { | ||||||||||||||
| my $q = get_captured_query($node, | ||||||||||||||
| "SELECT * FROM test_squash WHERE id IN (10, 20, 30) AND val = 'secret'"); | ||||||||||||||
|
|
||||||||||||||
| like($q, qr{/\*, \.\.\. \*/}, 'Squashed comment marker present for IN list'); | ||||||||||||||
| unlike($q, qr/\b10\b/, 'IN constant 10 removed'); | ||||||||||||||
| unlike($q, qr/\b20\b/, 'IN constant 20 removed'); | ||||||||||||||
| unlike($q, qr/\b30\b/, 'IN constant 30 removed'); | ||||||||||||||
| unlike($q, qr/secret/, 'Standalone string constant removed'); | ||||||||||||||
| # Should have at least two distinct placeholders ($N for IN, $M for val) | ||||||||||||||
| like($q, qr/\$1/, 'First placeholder present'); | ||||||||||||||
| like($q, qr/\$2/, 'Second placeholder present'); | ||||||||||||||
| }; | ||||||||||||||
|
|
||||||||||||||
| # --------------------------------------------------------------------------- | ||||||||||||||
| # Test 4: Large IN list to ensure full list coverage | ||||||||||||||
| # --------------------------------------------------------------------------- | ||||||||||||||
| subtest 'large IN list fully squashed' => sub { | ||||||||||||||
| my @nums = (100 .. 119); | ||||||||||||||
| my $in_list = join(', ', @nums); | ||||||||||||||
| my $q = get_captured_query($node, | ||||||||||||||
| "SELECT * FROM test_squash WHERE id IN ($in_list)"); | ||||||||||||||
|
|
||||||||||||||
| like($q, qr{/\*, \.\.\. \*/}, 'Squashed comment marker present'); | ||||||||||||||
| # Verify no raw numbers from the list survive | ||||||||||||||
| for my $n (@nums) { | ||||||||||||||
| unlike($q, qr/\b$n\b/, "IN constant $n removed"); | ||||||||||||||
| } | ||||||||||||||
| # The normalized form should be compact, not contain 20 separate placeholders | ||||||||||||||
| unlike($q, qr/\$5/, 'No excessive placeholders (list is squashed, not expanded)'); | ||||||||||||||
|
Comment on lines
+148
to
+149
|
||||||||||||||
| # The normalized form should be compact, not contain 20 separate placeholders | |
| unlike($q, qr/\$5/, 'No excessive placeholders (list is squashed, not expanded)'); | |
| # The normalized IN-list should contain exactly one placeholder plus | |
| # the squashed comment marker, not an expanded series of placeholders. | |
| like($q, qr/WHERE\s+id\s+IN\s*\(\s*\$\d+\s+\/\*, \.\.\. \*\/\s*\)/, | |
| 'IN list is squashed to a single placeholder'); |
Copilot
AI
Apr 5, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This regex can match placeholder numbers as well (e.g., "$7") because word boundaries exist between "$" and a digit. That can make the test fail even when normalization is correct if placeholder numbering ever changes. Consider tightening the pattern to specifically target raw constants from the original IN list (e.g., match digits that aren’t immediately preceded by '$') or assert the exact normalized NOT IN form instead.
| unlike($q, qr/\b[7-9]\b/, 'NOT IN constants removed'); | |
| unlike($q, qr/(?<!\$)\b[7-9]\b/, 'NOT IN constants removed'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the kill() failure path, only ESRCH is treated as a stale PID. If the bgworker PID is reused by a non-Postgres process owned by another user, kill() can fail with EPERM; in that case the shared bgworker_pid remains set and pg_stat_ch_flush() will keep warning and never recover. Consider additionally clearing the stored PID when the PID is no longer a Postgres backend (e.g., BackendPidGetProc/IsBackendPid check) and treating that as “background worker not running”, instead of leaving a permanently stale PID.