Skip to content

Partitioning: refuse RANGE until implemented; add dynamic LIST partitioning - #834

Open
adsharma wants to merge 8 commits into
mainfrom
fix/range-partitioning
Open

Partitioning: refuse RANGE until implemented; add dynamic LIST partitioning#834
adsharma wants to merge 8 commits into
mainfrom
fix/range-partitioning

Conversation

@adsharma

Copy link
Copy Markdown
Contributor

Summary

Two changes to table partitioning:

1. RANGE is refused at DDL time

RANGE previously fell back to hash routing while accepting PARTITION BY RANGE, silently violating the monotonic placement users expect. Real range partitioning needs bounds derived from the actual value distribution (declarative bounds or data-driven splitting); equal splits of the type domain were rejected because all realistic data lands in one middle bucket. The binder now refuses the DDL with an actionable message; the grammar keeps parsing it so enabling it later is a binder-only change.

2. LIST partitioning (one partition per distinct value)

PARTITION BY LIST (col) creates one partition per distinct partition-key value, dynamically:

  • Grammar: new iC_PartitionList rule + LIST keyword (no PARTITIONS clause); regenerated parser sources and shell keywords.
  • Catalog: LIST parents persist an encoded-key -> child-table-ID map on the parent entry (storage version 47; 0.20.0 repointed since v46 never shipped).
  • Dynamic creation: a ListPartitionRouter creates a partition at first sight of a new value inside the writing transaction, via the same machinery as CREATE NODE TABLE (catalog entry + serial sequence + subgraph + storage + WAL create record), so rollback, WAL replay, and checkpointing behave like ordinary DDL. Children carry their parent link through the create info so replay reconstructs them correctly.
  • Routing: single-row INSERT/MERGE resolve lazily in NodeInsertExecutor; COPY resolves under the router lock and atomically grows shared + per-worker target arrays (including index producer tokens) as workers discover values.
  • One initial unkeyed partition is created at DDL so reads/writes always see a non-empty child set; it stays empty by design.

HASH routing is unchanged; all previously merged invariants hold (no direct child drops/alters, cascade-drop guards, partition-column updates refused, rel FROM/TO expands over partitions, parent rename cascades).

Testing

  • New e2e case PartitionedListDynamic: per-value partition creation on inserts, parent-scan union, partition reuse for repeated values, persistence across reopen/WAL replay, post-reopen routing into an existing partition, cascade drop.
  • All 12 partitioned + storage_version e2e tests pass.
  • Verified manually in the shell across close/reopen cycles.

adsharma and others added 8 commits August 22, 2026 10:02
…ists

RANGE previously fell back to hash routing while accepting RANGE DDL,
which silently violated the monotonic placement users expect from range
partitioning. Real RANGE needs bounds derived from the actual value
distribution (declarative bounds or data-driven splitting); a static
stand-in - including equal splits of the type domain, which dump all
real-world data into one bucket - would misplace rows silently. The
binder now refuses PARTITION BY RANGE with an actionable message; the
grammar keeps parsing it so enabling it later is a binder-only change.

Tests: PartitionedCreateRange now asserts the refusal; PartitionedDropCascades
switched to HASH. Docs: status note, limitations, and a roadmap section
covering declarative bounds and dynamic distribution-aware splits.
…nct value)

PARTITION BY LIST (col) now creates one partition per distinct partition-key
value, dynamically:

- Grammar: iC_PartitionList (LIST keyword, no PARTITIONS clause); regenerated
  parser sources and shell keywords.
- Catalog: LIST parents persist an encoded-key -> child-table-ID map on the
  parent entry (storage version 47; 0.20.0 repointed since v46 never shipped).
  The first partition is created at DDL time so reads/writes always see a
  non-empty child set; it stays unkeyed and empty.
- Routing: ListPartitionRouter creates a partition on first sight of a new
  value inside the writing transaction via the CREATE NODE TABLE machinery
  (catalog entry + serial sequence + subgraph + storage + WAL create record),
  so rollback, WAL replay, and checkpoint behave like ordinary DDL. Children
  carry their parent link through the create info, keeping replay correct.
- Single-row INSERT/MERGE route in NodeInsertExecutor; COPY routes under the
  router lock, atomically growing shared + per-worker target arrays (and index
  producer tokens) as workers discover new values.
- RANGE stays refused at DDL; HASH routing unchanged.

Verified end to end in the shell: distinct values create distinct partitions,
same value reuses its partition, parent scans union all partitions, state
survives reopen/WAL replay, post-reopen inserts reuse persisted partitions.
All 11 partitioned + storage_version e2e tests pass.
Both ladybug's third_party/mbedtls and a statically-linked duckdb
(libduckdb_static.a) bundle mbedtls with plain, unprefixed mbedtls_*
symbols. When both end up on one link line (e.g. GEN=Ninja make shell
EXTENSION_STATIC_LINK_LIST='httpfs'), the linker reports ~19 duplicate
symbol errors for _mbedtls_cipher_*.

Fix by wrapping our vendored copy in namespace lbug_mbedtls:

- third_party/ports/mbedtls/do-patch.py (new): port patch script,
  following the zstd port pattern. It renames .c -> .cpp, strips the
  now-dead extern "C" guards (the copy is compiled as C++ everywhere),
  and wraps top-level code segments in balanced namespace pairs.
  Segmentation tracks preprocessor conditional context and brace depth
  per line, so sibling conditional branches (MBEDTLS_AES_ALT vs
  MBEDTLS_SELF_TEST) and mid-declaration conditionals (lone 'static'
  followed by a conditional attribute block) stay balanced. It also
  absorbs the previously-missing format_c_to_cpp.py step, making the
  port runnable again.
- ports/mbedtls/Makefile: invoke do-patch.py during build.
- third_party/mbedtls: vendored sources regenerated via the script
  (57 files; only namespace pairs and extern "C" removals).

Public headers re-expose names via 'using namespace lbug_mbedtls', so
in-tree consumers (src/common/sha256.cpp,
extension/httpfs/src/crypto.cpp) are unchanged.

Requires LadybugDB/extensions#63 (submodule bump included here).
Verified with GEN=Ninja make shell EXTENSION_STATIC_LINK_LIST='httpfs':
the link succeeds, libmbedtls.a exports zero plain _mbedtls_* symbols,
and the shell passes a smoke query.
SECURITY.md linked security/threat-model.md (hyphen); the file in the
repository is security/threat_model.md (underscore).

tools/shell/shell_development_guide.md: avaiable -> available.
PartitionedListDynamic: dynamic per-value partition creation on single-row
inserts (4 distinct cluster values -> L_p1..L_p3 alongside the initial
unkeyed L_p0), parent-scan union over all partitions, partition reuse for a
repeated value, persistence across reopen/WAL replay, post-reopen routing
into an existing partition, and cascade drop of the parent.

The case ends with an explicit DROP TABLE L: the FSM leak checker drops
tables in show_tables() order, which after reopen lists partition children
before their parent, and dropping a child directly is refused.
Adding the LIST keyword made 'list' a reserved word, breaking every query
that uses it as a variable/alias/column name (TCK list/map scenarios, cast
tests with (list STRING[]) columns). Add LIST to iC_NonReservedKeywords so it
remains usable as an identifier, matching HASH/RANGE/PARTITION(S), and
regenerate the parser. Also apply clang-format-18 to the touched src/ files
(third_party generated files are outside CI's lint scope).
…ng hooks (PR 829)

Encode the decision order at every shared seam: locate() first (claimed ->
wrapper owns it, no local state), then phase B's dedicated StorageManager for
unclaimed partitions. Claimed partitions never enter the file registry, so
checkpoint/rollback/drop iterate exactly the unclaimed set with no extra
filtering; bind-time scan substitution means plan-time resolution never sees
a remote child; drop notifications and file cleanup are disjoint by
construction. Child files are named by catalog name and renamed with the
rename cascade; PartitionRef stays ID-based as in 829.
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