Skip to content

test(join): add broad FULL OUTER JOIN coverage across types/constraints/table kinds - #2

Open
Ariznawlll wants to merge 17 commits into
mainfrom
feat/add-full-outer-join-tests
Open

test(join): add broad FULL OUTER JOIN coverage across types/constraints/table kinds#2
Ariznawlll wants to merge 17 commits into
mainfrom
feat/add-full-outer-join-tests

Conversation

@Ariznawlll

Copy link
Copy Markdown
Owner

Summary

Adds broad FULL OUTER JOIN test coverage on top of the core phase1–4 suite
introduced in fullouterjoin.sql (matrixorigin#24230). The four new files
exercise the user-visible surface rather than the execution paths:

  • fulljoin.sql — core semantics: basic equi-join on int/varchar with
    NULLs on both sides, commutativity, empty corners, no-match / all-match,
    duplicates on both sides, multi-column ON, USING, non-equi ON, WHERE
    anti-left/anti-right/symmetric-diff/predicate-on-nullable-column,
    COALESCE merged key, aggregation, derived tables, 3-table chain,
    self FULL JOIN, mixed INNER/LEFT, per-type coverage (decimal/date/
    char-vs-varchar), NULL-safe <=>, EXPLAIN, large-dataset invariants.
  • fulljoin_types.sql — per-data-type join-key coverage: all integer
    widths (signed + unsigned), decimal, double, bool, bit, char, varchar,
    text, char-vs-varchar, binary, varbinary, blob, date, datetime,
    timestamp, time, uuid, enum, json, vector columns as payload, and
    = vs <=> NULL-safe semantics.
  • fulljoin_constraints.sql — column/table constraint coverage:
    PRIMARY KEY (single/composite), UNIQUE KEY (single/composite),
    NOT NULL + DEFAULT, AUTO_INCREMENT, CHECK, ON UPDATE CURRENT_TIMESTAMP,
    FOREIGN KEY with ON DELETE/ON UPDATE CASCADE, generated columns,
    secondary indexes, and a 3-table FK-network combo (user → order →
    item).
  • fulljoin_tables.sql — table-kind coverage: temporary tables on
    one/both sides, partitioned tables (HASH / RANGE / KEY / LIST),
    partition × partition, views × views, CTE and recursive CTE,
    derived tables, mo_catalog cluster table joined with a normal table
    (sys session), FULLTEXT-indexed tables, IVFFLAT and HNSW vector
    indexes, plus a non-sys tenant section covering basic FULL,
    multi-column ON, anti-left/right, HASH partition, IVFFLAT, fulltext,
    CTE, and a negative test that confirms a non-sys tenant cannot create
    a cluster table.

Validation

Cross-validated every case against MySQL 8.0.45 via
LEFT JOIN UNION ALL RIGHT JOIN WHERE left.key IS NULL rewrites wherever
MySQL can build an equivalent schema. For MO-only constructs (vector
indexes, cluster tables, temporary tables, partitioned tables, fulltext)
used MO-internal FULL vs LEFT-UNION-ALL self-check as the oracle.

The cross-check surfaced three FULL OUTER JOIN correctness defects, each
filed as a separate issue:

Results generated with mo-tester -m genrs -g, and replay verified with
mo-tester -m run -g:

[SUMMARY] COST : 1s, TOTAL :506, SUCCESS : 504, FAILED :0, IGNORED :2, ABNORAML :0, SUCCESS RATE : 100%
[fulljoin.sql]              TOTAL :128, SUCCESS :126, IGNORED :2   (USING under #24247)
[fulljoin_constraints.sql]  TOTAL :101, SUCCESS :101
[fulljoin_tables.sql]       TOTAL :174, SUCCESS :174
[fulljoin_types.sql]        TOTAL :103, SUCCESS :103

Test plan

🤖 Generated with Claude Code

Ariznawlll and others added 3 commits April 29, 2026 16:42
…ts/table kinds

Complements the core phase1-4 coverage in fullouterjoin.sql (matrixorigin#24230)
by exercising FULL OUTER JOIN across the surface area a typical user
hits, not just the execution paths.

Four new suites:

- fulljoin.sql              - core semantics, NULL handling, WHERE,
                              aggregation, CTE, self-join, mix with
                              INNER/LEFT, large-scale invariants
- fulljoin_types.sql        - per-data-type join-key coverage
                              (all int widths + unsigned, decimal,
                              double, bool, bit, char/varchar/text,
                              binary/varbinary/blob, date/datetime/
                              timestamp/time, uuid, enum, json,
                              vector payload, null-safe <=>)
- fulljoin_constraints.sql  - PK/UK/NOT NULL/DEFAULT/AUTO_INC/CHECK/
                              ON UPDATE/FK-cascade/generated/secondary
                              index, plus a 3-table FK-network combo
- fulljoin_tables.sql       - temporary / partitioned (HASH/RANGE/
                              KEY/LIST) / view / CTE / recursive CTE /
                              mo_catalog cluster table / fulltext-
                              indexed table / IVFFLAT / HNSW, plus a
                              non-sys tenant section (incl. negative
                              test that a non-sys tenant cannot create
                              a cluster table)

Cross-validated against MySQL 8.0 via LEFT UNION RIGHT rewrites
for every case where MySQL can build an equivalent schema; the rest
use MO-internal self-check as the oracle.

Known-buggy queries are wrapped in -- @bvt:issue#24247 (FULL OUTER JOIN
USING does not coalesce the merged column).

Results verified with mo-tester -m run -g: 506 SQL, 504 success,
2 ignored (USING under matrixorigin#24247), 0 failed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This backports matrixorigin#24201 to `main`.

For pessimistic `INSERT ... SELECT ...` statements that escalate to table locks, `Compile.compileLock` was still keeping the target table on the pre-pipeline lock path. That can make a large source scan hold the target-table lock before the write side starts running and block later writers for too long.

This backport keeps the same table-lock semantics, but lets `LockOp` acquire the table lock when the first batch reaches the target pipeline. It also preserves EOF-time table locking for the zero-row path so inserts that produce no rows still keep the intended table-lock behavior.

Also included:
- a compile unit test for the pre-pipeline lock routing
- a lockop unit test for the zero-row EOF table-lock path

Approved by: @aunjgr, @ouyuanning, @iamlinjunhong
gouhongshen and others added 8 commits April 29, 2026 10:08
- avoid reading and scanning the hidden commit-ts column on every normal block read when block-level commit-ts zonemap proves the whole block is visible to the snapshot
- keep row-level commit-ts filtering for snapshot/data-branch fallback reads when the commit-ts range overlaps the requested snapshot
- avoid treating a user-visible trailing `TS` column as hidden commit-ts metadata
- reuse already loaded object metadata while loading block columns so the read path does not fetch the same metadata twice
- add blockio coverage for row-level filtering, zonemap fast path, and visible TS-column behavior

Validation:

- `gofmt` applied
- `git diff --check` passed
- `go test ./pkg/vm/engine/tae/blockio ./pkg/objectio/ioutil` could not complete in this local environment because CGO headers `usearch.h` and `xxhash.h` are missing before package tests run

Approved by: @XuPeng-SH
…trixorigin#24202)

Upstream Sirius added a transparent GPU execution optimizer hook (sirius-db/sirius#518). With 'gpu_execution=true' (default), the optimizer captures the outer logical plan and tries to compile it on the GPU. For our wrapper 'SELECT * FROM gpu_execution(...)', the captured outer plan has 'gpu_execution' as a TableScan source, and the Sirius pipeline converter throws 'Unsupported scan function: gpu_execution', surfaced to MO as a 500 from the sidecar.

The 'CALL gpu_execution(...)' form is the documented invocation in upstream Sirius (docs/super-sirius/README.md) and is not intercepted by the transparent hook, so it routes correctly through the dedicated gpu_execution table function bind/execute path that compiles the inner SQL into a Sirius plan.

Verified end-to-end: all 22 TPC-H SF10 queries pass via the sidecar GPU path with the new wrapper (paired with the matching sirius bump in mo-sirius-sidecar).

Approved by: @XuPeng-SH
…MIT (Fixes matrixorigin#24243) (matrixorigin#24244)

`INSERT INTO ... SELECT ... ORDER BY col LIMIT 5000000` on a 100M row table causes OOM in CI nightly regression. The Top operator holds all LIMIT rows with ALL columns in the heap, consuming O(limit × row_width) memory — for 5M rows of wide data this reaches tens of GiB.

This PR makes three targeted changes:

### 1. Top operator: spill to disk for large LIMIT (`top/top.go`, `top/types.go`)

When LIMIT > 16384, the Top operator now:
- Keeps only **sort-key columns** in the in-memory heap (not all columns)
- Spills full rows to a temporary file via `batch.MarshalBinary`
- Tracks `rowRef{batchIdx, rowIdx}` per heap entry to locate spilled rows during eval

Heap memory drops from **O(limit × row_width)** to **O(limit × key_width)**.

### 2. Top operator: streaming eval in spill mode (`top/top.go`)

Instead of materializing all LIMIT rows into one giant batch during eval, spill mode now:
- Pops heap into sorted `orderedRefs`
- Frees the heap batch immediately
- Reads spilled batches and outputs **8192-row chunks** per `Call()` invocation

Eval peak memory drops from **O(limit × row_width)** to **O(chunk_size × row_width)** (~10 MiB per chunk).

### 3. MergeTop: fix memory leak from `defer` in loop (`mergetop/top.go`)

`defer bat.Clean(proc.Mp())` was placed inside a `for` loop in `build()`. Since `defer` only fires on function return, every duplicated batch from each iteration accumulated in memory. Replaced with explicit `bat.Clean()` after each `processBatch` call and on error paths.

Approved by: @ouyuanning, @aunjgr
…ixorigin#24258)

Fixes two related FULL OUTER JOIN bugs.

### matrixorigin#24247 — `FULL OUTER JOIN ... USING(col)` did not coalesce the merged column

Per the SQL standard, the merged USING column on a FULL OUTER JOIN is
`COALESCE(left.col, right.col)`. MO previously resolved it to the left
binding alone, which loses the right-side value on right-only rows
(the column comes back as `NULL` even though a value exists on the
right side).

Repro before this PR:

```sql
create table u1(id int); create table u2(id int);
insert into u1 values (1),(2); insert into u2 values (2),(3);
select id from u1 full outer join u2 using(id) order by id;
-- Wrong: 1, 2, NULL    (expected: 1, 2, 3)
```

Implementation:

- New `outerUsingCols map[string][]string` on `BindContext` — for each
USING column, the ordered list of contributing leaf tables. The list
has length ≥ 2 only when the column came from a FULL OUTER JOIN USING
arm (possibly nested through other FOJ-USING).
- `addUsingCol` reads the lists from the **child** contexts (not from
`bc`), so the merge-context overwrite during context propagation is
harmless. Branch logic for LEFT/RIGHT/INNER vs OUTER is collapsed
via `chosen`/`chosenCoalesce` aliases and a small switch on
`chosen`-list semantics.
- COALESCE emission at three sites where a bare/unqualified merged
column is exposed:
- SELECT-list star expansion (`doUnfoldStar`)
- Unqualified column references (`qualifyColumnNames` and
`baseBindColRef` fast-path)
- The join equality predicate itself is also coalesce-aware
(`buildUsingEqOperand` emits `COALESCE(arm1.col, arm2.col, ...)`
when there are ≥ 2 arms), so an enclosing FOJ-USING matches against
the inner merged column rather than just the inner-chosen side.
Without this, nested FOJ-USING produced a duplicate id row.
- Qualified references (`u1.id`, `u2.id`) keep current per-side
behavior (NULL on the padded side).

### matrixorigin#24250 — `NATURAL FULL [OUTER] JOIN` parsed but silently became NATURAL RIGHT

The grammar action for `NATURAL outer_join` was an `if/else` chain
that fell through to RIGHT for FULL. Fixes:

- New `JOIN_TYPE_NATURAL_FULL` constant in `tree/select.go`.
- `mysql_sql.y`: rewrite the `NATURAL outer_join` reduction as a
`switch` over LEFT/RIGHT/FULL. Regenerated `mysql_sql.go` via
`cd pkg/sql/parsers && make mysql`.
- `query_builder.go`: plumb `NATURAL_FULL` into the `Node_OUTER`
joinType mapping and the natural-cols discovery branch.
- Add `JOIN_TYPE_FULL` and `JOIN_TYPE_NATURAL_FULL` to the
recursive-CTE rejection list — an oversight from matrixorigin#24192 when FULL
was first added.

### Tests

`test/distributed/cases/join/fullouterjoin.sql` gets two new sections:

- §8 (matrixorigin#24247): `SELECT id`, `SELECT *`, `WHERE id = ...`,
`GROUP BY id`, nested `(u1 FOJ u2 USING(id)) FOJ u3 USING(id)`,
and qualified-reference behavior.
- §9 (matrixorigin#24250): `NATURAL FULL OUTER JOIN`, `NATURAL FULL JOIN`, and
`SELECT *` over a NATURAL FULL JOIN.

mo-tester run on the file: 69/69 SUCCESS, 0 FAILED.

Approved by: @iamlinjunhong, @ouyuanning, @heni02
…onst keys (matrixorigin#24259)

`partition.Partition` is invoked once per sort key from
`pkg/sql/colexec/order/order.go::sortAndSend` (and per partition spec
from `pkg/sql/colexec/window/window.go`), reusing the same `diffs`
slice across calls. The contract is that each call must **OR** new
boundaries onto `diffs` — never overwrite an existing `true`.

Two code paths in `pkg/partition/partition.go` violated that contract:

1. **Both-NULL overwrite.** In `genericPartition` / `bytesPartition`,
when both adjacent rows were NULL under the current key the code
did `diffs[i] = false`, erasing a boundary set by a prior key.
This made multi-key `ORDER BY` over a FULL OUTER JOIN produce rows
out of order: with the primary key NULL on the right-padded side,
the secondary key was never sub-sorted within the `t1.s = NULL`
partition. Repro from the issue:

```sql
create table t1(s int, v varchar(5));
create table t2(s int, v varchar(5));
insert into t1 values (1,'a'),(5,'b'),(NULL,'x');
insert into t2 values (13,'c'),(14,NULL);
select t1.s, t1.v, t2.s, t2.v
from t1 full outer join t2 on t1.s = t2.s
order by t1.s, t2.s, t1.v, t2.v;
```

Before this PR, `(NULL,NULL,14,NULL)` was emitted **before**
`(NULL,NULL,13,'c')`. After: 13 before 14, as expected.

2. **Const-vector boundary collapse.** When `vec.IsConst()` the code
returned `partitions = [0]` and, for `IsConstNull()`, even cleared
all of `diffs`. A const key appearing mid-list therefore collapsed
all previously found partitions in the same `Partition` call series.

### Fix

In both `genericPartition` and `bytesPartition`, treat every "rows are
equal under this key" outcome as a no-op on `diffs` (the both-NULL
case and the entire const branch). Always rebuild `partitions` from
the OR-accumulated `diffs` at the end. `diffs[0] = true` is set up
front, so the const-vector path now naturally yields `[0]` via the
final scan rather than via a special branch.

This is the same semantics the non-const non-null path already had
(`diffs[i] = diffs[i] || (v != w)`).

### Tests

- `pkg/partition/partition_test.go::TestPartitionAccumulatesDiffs`
exercises the reuse contract — multiple successive `Partition`
calls on the same `diffs`/`partitions` slices — across all four
problematic shapes:
- fixed-width second key with all NULLs preserves the first key's
boundaries
- fixed-width const non-null second key preserves boundaries
- fixed-width const NULL second key preserves boundaries
- bytes second key with all NULLs preserves boundaries
- bytes const second key preserves boundaries
- sanity check that two non-trivial keys union their boundaries
- `test/distributed/cases/join/fullouterjoin.sql` gets the issue's
exact repro and a `NULLS LAST` (DESC) secondary-key variant.
- `mo-tester -m run` on the test file: **45/45 SUCCESS, 0 FAILED**.
- Unit tests for `pkg/sql/colexec/order/...`,
`pkg/sql/colexec/window/...`, and `pkg/partition/...` all pass.

Approved by: @iamlinjunhong, @heni02
jiangxinmeng1 and others added 2 commits April 30, 2026 10:36
…ation (matrixorigin#24246)

- Add `UnpackNthElement` to decode only up to the target element in a serialized tuple, instead of deserializing the entire blob
- Use fast path in `serial_extract` when the index parameter is constant (the common case in GROUP BY on composite secondary index)
- Reduces `serial_extract` CPU overhead ~6x for DISTINCT queries on leading columns of composite secondary indexes

Approved by: @ouyuanning, @aunjgr, @XuPeng-SH
mergify Bot pushed a commit that referenced this pull request May 7, 2026
…origin#24117)

Move postEvict callbacks (value.Release + metrics updates) outside the global `queueLock` in the FIFO cache to eliminate lock convoy under memory pressure.

### Root Cause

When MemCache (12GB FIFO cache) is 100% full, every `Set()` triggers `Evict()` which holds the global `queueLock` while executing `postEvict` callbacks. Under GC pressure (STW pauses up to ~1s), this creates a lock convoy where all concurrent cache operations serialize through the single lock:

- Each `Set()` takes **300-444ms** (normal: <0.1ms, 3000-4400x slower)
- Queries with ~90 cache ops inflate from 1-5s to **60-107s**
- Exceeds client-side timeouts → **connection disconnections**

Evidence from fileservice slow event trace:
```
Single S3FS.Read (total: 798ms) breakdown:
disk cache read:            35ms
set memory cache entry #1: 318ms  ← queueLock wait + eviction
set memory cache entry #2: 444ms  ← queueLock wait + eviction
762ms (95%) spent in "set memory cache entry"
```

### Fix

1. Collect evicted items under `queueLock` into a pending list
2. Release `queueLock`
3. Execute `postEvict` callbacks (value.Release + metrics) outside the lock

The `item.valueOK` flag is still set to `false` under the **shard lock** (not queueLock) to prevent data races with concurrent `Get()` calls.

### Impact

In stability testing (commit 725b723):
- **TPCC**: 40 "Communications link failure" events
- **Fulltext**: 57 "Lost connection" events
- **Sysbench**: 0 errors (point queries <5s skip tombstone transfer)

Approved by: @gouhongshen, @XuPeng-SH, @fengttt
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.

5 participants