Skip to content

Feat/yugabyte#453

Open
wdroste wants to merge 30 commits into
duckdb:mainfrom
brinqa:feat/yugabyte
Open

Feat/yugabyte#453
wdroste wants to merge 30 commits into
duckdb:mainfrom
brinqa:feat/yugabyte

Conversation

@wdroste
Copy link
Copy Markdown
Contributor

@wdroste wdroste commented Apr 29, 2026

No description provided.

wdroste and others added 30 commits April 25, 2026 15:20
Three-phase design for making duckdb-postgres YugabyteDB-aware:
Phase A (correctness), Phase B (tablet-aware parallelism),
Phase C (COPY optimization).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
12-task plan across 3 phases: correctness (5 tasks), parallelism
(4 tasks), COPY optimization (1 task), plus verification.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
PROMPT.md drives the loop: one task per iteration, retroactive
verification of previous work, functional smoke tests against
real YugabyteDB via $YB_CONN, and completion gates with build +
diff + grep + integration + smoke test checks.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Detect YugabyteDB via '-YB-' in the PostgreSQL version string.
Extract the YB version (e.g., 2025.2.0.0) for future feature gating.
Store instance_type in OwnedPostgresConnection for connection-level gating.
YugabyteDB uses LSM storage, not heap pages. CTID page ranges
are meaningless and would produce incorrect parallel scan plans.
YugabyteDB uses hybrid logical clocks for MVCC. Snapshot
export/import is unnecessary and may not behave correctly.
DISCARD ALL clobbers session GUCs like statement_timeout and
search_path. For YugabyteDB, use RESET ALL + DEALLOCATE ALL +
CLOSE ALL + UNLISTEN * instead.
Query num_tablets, num_hash_key_columns, and PK column names from
YugabyteDB. Use tablet count for parallel thread count instead of
meaningless relpages.
Query yb_servers() to get all tservers with host/port/region/zone.
Probe each tserver for direct connectivity (2s timeout).
Cache topology for the ATTACH lifetime.
Split YugabyteDB hash space (0-65535) into N ranges and push
yb_hash_code(pk_cols) BETWEEN X AND Y as parallel scan tasks.
Force REPEATABLE READ on all parallel connections for consistent
reads via HLC-based MVCC.
When tservers are directly reachable, route parallel scan
connections round-robin to individual tservers for data locality.
Falls back to connection pool when tservers are not reachable.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Register pg_yb_rows_per_transaction and pg_yb_disable_transactional_writes.
Push yb_disable_transactional_writes GUC for bulk loads.
Add CommitAndRestartCopy helper for batched transaction COPY FROM.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Retroactive verification confirmed all 10 tasks are committed and
passing smoke tests against the dev YugabyteDB cluster. Sync plan
checkboxes with git reality.
Build clean, 12 YUGABYTE checks across 4 files, DISCARD ALL gated
in else branch, 10-commit log confirmed, attach/detach/re-attach
smoke test passes with matching row counts.
Remove PROMPT.md, docs/superpowers/ planning and spec docs that were
used during YugabyteDB integration development. Add .cocoindex_code/
to .gitignore.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Auto-formatted with clang-format 11.0.1 to pass CI format-check.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add a full YugabyteDB test suite that exercises every code path
introduced by the YugabyteDB integration:

CI Pipeline:
- YugabyteDB service container (2024.2.3.0) running in parallel
  with existing linux-tests job
- Health-check with 120s startup grace period and 20 retries
- Cluster readiness verification (yb_servers() must respond)
- Retry-with-backoff in test data setup script
- Container logs captured on failure for debugging

Test Coverage (6 test files, 892 lines):
- attach_yugabyte_basic: 100k+ row scans across hash/range/wide
  tables, pg_catalog system table access, NULL handling
- attach_yugabyte_parallel: hash-code parallel scan with 100k rows,
  filter pushdown, cross-table joins, varied thread counts (1/4/8),
  compound partition keys, range table fallback
- attach_yugabyte_reconnect: 7 attach/detach cycles testing
  connection pool Reset path (DISCARD ALL replacement), rapid
  cycling, pool limit changes
- attach_yugabyte_concurrent: concurrent reads under contention,
  concurrent cross-table reads, concurrent table creation/writes
- attach_yugabyte_write: bulk COPY insert (10k rows), cross-table
  INSERT INTO, UPDATE, DELETE, multi-type columns, data checksums
- attach_yugabyte_settings: pg_yb_* settings registration, read-back,
  reset, interaction with attach/query flow

Test Data (create-yugabyte-tables.sh):
- hash_test: 100k rows, single hash key
- wide_test: 50k rows, 7 column types
- multi_hash: 10k rows, compound partition key
- range_test: 20k rows, range-partitioned (no hash scan)
- test/nulltest: small tables for basic/null coverage

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Expand test suite from 6 to 9 test files covering every YugabyteDB
code path:

New test files:
- attach_yugabyte_range: dedicated range-partitioned table tests
  (ASC single key with SPLIT AT, compound ASC timeseries, DESC
  ordering), verifies NO hash-code scan fallback, mixed range+hash
  joins, write to range tables
- attach_yugabyte_colocated: colocated database (WITH colocation=true),
  colocated tables (0 tablets → single-thread scan), non-colocated
  opt-out in same db, cross-table joins, write/delete, reconnect
- attach_yugabyte_errors: schema mismatch resilience — non-existent
  table/schema errors, create-drop-recreate with type change, invalid
  connection string, empty table scan, no-PK table, connection health
  after errors

Updated test files:
- attach_yugabyte_basic: replace old range_test with range_single/
  range_ts/range_desc, deterministic filter values
- attach_yugabyte_parallel: range table references updated, verify
  range tables don't use hash-code scan while hash tables do
- attach_yugabyte_write: add yb_disable_transactional_writes COPY
  path test, verify normal COPY works after flag reset

Test data (create-yugabyte-tables.sh):
- Fix colocated DB creation (CREATE DATABASE WITH colocation=true)
- Add 3 range-partitioned tables with explicit ASC/DESC/SPLIT AT
- 9 tables across 2 databases, 215k+ total rows

Code path coverage audit: 23/23 YugabyteDB code paths now have
test coverage (CommitAndRestartCopy is declared but has no callers).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The services block failed because YugabyteDB needs an explicit
startup command (bin/yugabyted start --daemon=false). Switch to
docker run in a step for full control over startup. Also fix
image tag to 2025.1.4.0-b103 (2024.x tags don't exist on Docker
Hub). Increase readiness timeout to 400s with 40 retries.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Security fix (high):
- Escape single quotes in regclass literals in LoadYugabyteTableProperties
  to prevent SQL injection via crafted table/schema names
  (src/storage/postgres_table_set.cpp)

Snapshot safety fix (high):
- Add pg_yb_parallel_scan setting (default false) to gate hash-code
  parallel scanning. Without a shared snapshot mechanism (no HLC
  exposure in YSQL), parallel workers use separate REPEATABLE READ
  transactions. Users must explicitly opt in, acknowledging the
  trade-off for read-only/append-only workloads.
  (src/postgres_scanner.cpp, src/postgres_extension.cpp)

COPY batching fix (medium):
- Wire pg_yb_rows_per_transaction into the insert sink. Track rows
  per batch and call CommitAndRestartCopy when the threshold is
  reached on YugabyteDB instances. Previously the setting was
  registered but never consumed.
  (src/storage/postgres_insert.cpp)

CI test fixes:
- Fix generate_series alias: use t(g) not g to get scalar column
- Remove SPLIT AT VALUES from range_single (caused 4x row count)
- Fix coloc_wide sum expected value (1250250000000 not 1250025000000)
- Add pg_yb_parallel_scan=true to parallel test
- Add pg_yb_parallel_scan to settings test coverage

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Test fixes:
- Remove DELETE/UPDATE from YB tests — extension uses ctid which
  YugabyteDB doesn't support (needs separate PK-based delete fix)
- Fix empty_hash SELECT * column count (query II not query I)
- Use sequential loop for CREATE TABLE, concurrentloop for writes
- Add ext commit hash to CI cache key to prevent stale ccache

Code quality (from /simplify review agents):
- Remove dead yb_version field from PostgresVersion (never read)
- Remove dead HasTopology/ReachableCount from YugabyteTopology
- Eliminate redundant StringUtil::Contains + find for -YB- detection
- Fix catch(...) break -> continue to try next tserver on failure
- Fix probe DSN: use full connection_string (with auth credentials)
  instead of bare host/port that fails on secured clusters

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…_timeout

Replace hardcoded connect_timeout=2 in tserver reachability probes
with a configurable pg_yb_tserver_probe_timeout setting (default 2s).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The ninja package is intermittently unavailable from the Chocolatey
community repo. Fall back to pip install ninja which pulls from PyPI.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Coverage audit identified 37 YB-specific code paths with 43% missing
coverage. These tests close the critical gaps:

attach_yugabyte_copy_batch (141 lines):
- CommitAndRestartCopy with pg_yb_rows_per_transaction=100/500/1000
- Batch disabled (pg_yb_rows_per_transaction=0)
- Batch + yb_disable_transactional_writes combined
- Data integrity verified across multiple commit cycles

attach_yugabyte_scan_modes (244 lines):
- 8 scan mode combinations: default serial, parallel+1/2/4/16 threads,
  filter pushdown+parallel, write txn fallback to serial,
  colocated+parallel (0 tablets → serial), tserver probe timeout
- Verifies pg_yb_parallel_scan=false forces single-thread
- Verifies INSERT...SELECT scan stays serial despite parallel enabled
- Verifies colocated tables correctly fall back with parallel on

attach_yugabyte_catalog (198 lines):
- pg_catalog system table access (pg_class, pg_tables, pg_namespace,
  pg_attribute, pg_type)
- information_schema access (tables, columns)
- Catalog entry verification for test tables
- CREATE TABLE with 10 column types, boundary values
- CREATE OR REPLACE TABLE
- Cross-table INSERT...SELECT (read one YB table → write another)
- Multi-schema support (CREATE SCHEMA, cross-schema table ops)

attach_yugabyte_pool (98 lines):
- Connection pool with limit=1 (serial access)
- Connection pool with limit=1000 (high concurrency)
- Connection pool disabled (pg_connection_cache=false)
- Multiple queries exercising Reset path without pool

attach_yugabyte_edge_cases (193 lines):
- Empty table scan (0 rows)
- Single row table
- All-NULL columns (except PK)
- Large text values (100k chars)
- Special characters (quotes, backslash, newlines, unicode, emoji)
- Integer boundary values (BIGINT/SMALLINT min/max)
- Wide table (20 columns)
- Concurrent attach to colocated + non-colocated databases
- Cross-database join

attach_yugabyte_stress (140 lines, slow):
- Full table scan across all 6 pre-loaded tables (205k total rows)
- 50k row bulk write and read-back with checksums
- Cross-table JOINs with parallel scan
- Subquery with hash-code parallel scan
- UNION across hash + range tables
- Window functions over parallel scan

Total YB test suite: 15 files, ~2800 lines

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- ATTACH before SET pg_yb_* settings to ensure extension is fully
  loaded and settings are registered (parallel, stress, settings tests)
- Fix PRIMARY KEY (ts ASC, id ASC) → (ts, id) — ASC syntax not
  supported by DuckDB's DDL parser
- Fix DATE + INTEGER → DATE + INTERVAL for DuckDB compatibility
- Fix LIKE 'row_1%' count: 11112 not 11111 (includes row_1 and
  row_100000)
- Fix compound filter count: 10000 not 11111 (id>50000 constraint)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- attach_yugabyte_scan_modes.test line 175: Mode 6 was missing ATTACH
  after DETACH yb, causing 'Schema with name yb does not exist'
- linux-relassert: reduce CMAKE_BUILD_PARALLEL_LEVEL 2→1 and add 8GB
  swap to prevent OOM during sanitizer-instrumented DuckDB build

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
`fallocate` fails with "Text file busy" when /swapfile already exists
and is mounted as swap. Guard the setup block so it only runs when
the swapfile is not already active.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
GitHub runners already have a /swapfile pre-configured. The step was
added to prevent OOM during sanitizer builds, but CMAKE_BUILD_PARALLEL_LEVEL=1
already handles memory pressure. The explicit fallocate was failing with
"Text file busy" on newer runner images.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Member

@staticlibs staticlibs left a comment

Choose a reason for hiding this comment

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

Hi, thanks for the PR! I think it is fine to have additional logic for Yugabyte as long as it does not affect vanilla Postgres logic (AFAICS current version is properly guarded by the instance type checks).

Though the list of commits in this PR is a bit too long, would you mind squashing them into a single commit and removing Co-Authored-By: Claude to not violate the the org-wide LLM policy?


linux-yugabyte:
name: YugabyteDB Tests
needs: format-check
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 better to make this dependent on linux-tests.


env:
CMAKE_BUILD_PARALLEL_LEVEL: 2
CMAKE_BUILD_PARALLEL_LEVEL: 1
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.

I think this change is not needed, the CI passes with 2.

ninja \
-y --force --no-progress
choco install ccache make -y --force --no-progress
choco install ninja -y --force --no-progress || pip install ninja
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.

I think this change is not needed, if adding pip fallback - this should be done starting from the main duckdb/duckdb repo.

static std::string AcquireModeToString(PostgresPoolAcquireMode mode);
static void ValidatePoolAcquireMode(ClientContext &context, SetScope scope, Value &parameter);

std::unique_ptr<PostgresConnection> CreateConnectionToHost(const string &host, int32_t port);
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.

M, this is kinda confusing. Are this direct-to-host connections pooled/cached in any way? If not, this logic should go outside of the connection pool.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes you can directly connect to x number of the tservers in the cluster.

if (!keep_copy_alive) {
// if we are can't keep the copy alive we need to restart the copy during every sink
gstate.rows_in_batch += chunk.size();
if (gstate.yb_rows_per_transaction > 0 && gstate.rows_in_batch >= gstate.yb_rows_per_transaction) {
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.

Can we explicitly check for PostgresInstanceType::YUGABYTE here for clarity? If the instance type check is not convenient here - lets add a comment that this branch is Yugabyte only.

@@ -0,0 +1,120 @@
# name: test/sql/storage/attach_yugabyte_basic.test
# description: Basic YugabyteDB attach, scan, and detection tests
# group: [storage]
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.

Please move all Yugabyte tests into a separate yugabyte directory.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'll see how/if I can merge them back, I created it separating because there were so many different additional tests and runtime.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants