Sprint C16b: catalog DDL builders — new_for_ddl + chainable setters#33
Conversation
Adds DDL-friendly constructors and chainable setters to catalog::TableDefinition / catalog::schema::FieldDefinition / catalog::schema::IndexDefinition so external codegen tools can build typed canonical forms purely for DDL emission via ToSql, without needing in-DB allocation or pub(crate) access to internal fields. Background and downstream consumer The first consumer is AdaWorldAPI/openproject-nexgen-rs C16a, which landed a mirror-layout op-surreal-ast crate (PR #20, merged). With this PR landed here, the planned nexgen C16c sprint switches from mirror-layout to canonical: From<op_surreal_ast::*> for catalog::* impls + render through the canonical catalog::* path. Until then, nexgen's mirror keeps the two layouts in lockstep. See .claude/op-codegen-bridge/README.md for the initiative context, scope of this sprint, and what was deliberately deferred. API added TableDefinition (catalog/table.rs): pub fn new_for_ddl(name: impl Into<TableName>) -> Self pub fn with_schemafull(self, v: bool) -> Self pub fn with_drop(self, v: bool) -> Self pub fn with_comment(self, v: Option<String>) -> Self pub fn with_table_type(self, v: TableType) -> Self pub fn with_view(self, v: Option<ViewDefinition>) -> Self pub fn with_permissions(self, v: Permissions) -> Self FieldDefinition (catalog/schema/field.rs): pub fn new_for_ddl(name: Idiom, table: TableName) -> Self pub fn with_kind(self, v: Option<Kind>) -> Self pub fn with_flexible(self, v: bool) -> Self pub fn with_readonly(self, v: bool) -> Self pub fn with_value(self, v: Option<Expr>) -> Self pub fn with_assert(self, v: Option<Expr>) -> Self pub fn with_computed(self, v: Option<Expr>) -> Self pub fn with_comment(self, v: Option<String>) -> Self pub fn with_reference(self, v: Option<Reference>) -> Self IndexDefinition (catalog/schema/index.rs): pub fn new_for_ddl(name, table_name, cols: Vec<Idiom>) -> Self pub fn with_comment(self, v: Option<String>) -> Self Each impl block is annotated #[allow(dead_code)] with a rationale comment: pub items have no in-crate caller (they exist as the public ergonomic surface for external surrealdb-core consumers); the #[cfg(test)] ddl_builder_tests modules exercise them but dead_code lints the non-test build target. Deliberately deferred Setters whose parameter types are pub(crate) (Permission, the four Permission slots on FieldDefinition, DefineDefault, ChangeFeed, Index variants beyond Idx) require a public-API decision and follow in dedicated sprints when downstream actually needs them. For typical Rails-mapped schema codegen the slots covered here are sufficient. The single warning the original draft triggered (with_changefeed exposed pub(crate) ChangeFeed) was dropped — see the rationale comment in catalog/table.rs. Tests cargo test -p surrealdb-core --lib ddl_builder_tests → 18 passed; 0 failed (1825 other tests filtered out) TableDefinition (8): new_for_ddl_does_not_leak_table_id_into_render builder_output_equals_struct_literal_output with_schemafull_round_trips with_drop_round_trips with_comment_round_trips with_table_type_round_trips new_for_ddl_supplies_dummy_ids_and_now_caches chained_builders_preserve_all_set_values FieldDefinition (6): new_for_ddl_defaults_to_no_kind_and_no_constraints new_for_ddl_carries_name_and_table_through_to_sql with_kind_round_trips with_flexible_and_readonly_round_trip with_comment_round_trips builder_output_equals_struct_literal_output IndexDefinition (4): new_for_ddl_defaults_to_non_unique_idx new_for_ddl_carries_name_table_and_cols_to_sql multi_column_index_renders_all_columns with_comment_round_trips The "builder_output_equals_struct_literal_output" tests are the strongest invariant: the builder is a pure ergonomic wrapper, semantically identical to a raw struct construction modulo cache UUIDs (which don't appear in DDL output). Preflight cargo check -p surrealdb-core --no-default-features → exit 0 25 warnings, all pre-existing (verified by filtering on my files — no warnings reference my edits). My code adds no new warnings. cargo fmt skipped on the tree level — stable rustfmt reformats 20+ files due to upstream's nightly-only rustfmt.toml options (same caveat ndarray fork's AGENTS.md documents). My 3 files are hand-formatted in upstream tab style. cargo audit / cargo deny / cargo clippy not run in this container (cargo-make not installed). Recommend running the full canonical battery before merge. Files changed .claude/op-codegen-bridge/README.md new — initiative notes .claude/board/AGENT_LOG.md +1 entry (append-only) surrealdb/core/src/catalog/table.rs +DDL-builder impl block + 8 tests surrealdb/core/src/catalog/schema/field.rs +DDL-builder impl block + 6 tests surrealdb/core/src/catalog/schema/index.rs +DDL-builder impl block + 4 tests No edits to: kvs/lance/*, sql/statements/*, runtime/transaction layers, existing impl TableDefinition / FieldDefinition / IndexDefinition blocks (new methods live in dedicated impl blocks beside the existing ones). No new dependencies. No Cargo.lock changes. https://claude.ai/code/session_01PBTGaPCSnnt6u3pjXpbLwY
📝 WalkthroughWalkthroughThis PR introduces DDL-builder APIs across three catalog type definitions. ChangesDDL Builder API and Tests
Sequence DiagramsequenceDiagram
participant Caller
participant TableDefinition
participant FieldDefinition
participant IndexDefinition
participant ToSql
Caller->>TableDefinition: new_for_ddl("my_table")
TableDefinition-->>Caller: TableDef with dummy IDs
Caller->>FieldDefinition: new_for_ddl(field_name, table_name)
FieldDefinition-->>Caller: FieldDef with defaults
Caller->>FieldDefinition: with_kind(Kind::String)
FieldDefinition-->>Caller: updated FieldDef
Caller->>IndexDefinition: new_for_ddl(index_name, table_name, cols)
IndexDefinition-->>Caller: IndexDef with defaults
Caller->>IndexDefinition: with_comment("indexed on id")
IndexDefinition-->>Caller: updated IndexDef
Caller->>ToSql: to_sql() on all definitions
ToSql-->>Caller: rendered DDL without dummy IDs
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~22 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8ffe4088c4
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| pub fn new_for_ddl(name: impl Into<TableName>) -> Self { | ||
| Self::new(NamespaceId(0), DatabaseId(0), TableId(0), name.into()) |
There was a problem hiding this comment.
Export the new DDL builders from catalog
For downstream codegen users, this constructor is still unreachable: catalog::table and catalog::schema are private modules, and catalog/mod.rs only re-exports these definitions with pub(crate) use schema::{..., *} and pub(crate) use table::* (checked lines 17-19 and 33-37). As a result, external crates cannot name surrealdb_core::catalog::TableDefinition/FieldDefinition/IndexDefinition, so the newly added public builder API cannot be used outside this crate despite being added for that purpose.
Useful? React with 👍 / 👎.
| pub fn with_reference(mut self, v: Option<Reference>) -> Self { | ||
| self.reference = v; |
There was a problem hiding this comment.
Avoid a private type in the reference setter
If the field builders are exported for the intended external consumers, this setter remains unusable because Reference is imported from crate::expr::reference::Reference, whose definition is pub(crate) (checked surrealdb/core/src/expr/reference.rs:10). Downstream crates therefore cannot construct or even name the Option<Reference> argument needed to set references, so this part of the advertised DDL API needs a public wrapper/type decision or should be deferred like the other private-parameter setters.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.claude/op-codegen-bridge/README.md:
- Line 48: The README lists `changefeed` as in-scope but the implementation
intentionally omits support for ChangeFeed (it is pub(crate) and omitted in
TableDefinition's DDL-builder and AGENT_LOG at line 307), so remove `changefeed`
from the in-scope list; update the backtick-delimited list (`comment`,
`table_type`, `view`, `permissions`, `changefeed`) to exclude `changefeed` so it
reads `comment`, `table_type`, `view`, `permissions`.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: a282448a-9e2e-499c-b163-026419f1f242
📒 Files selected for processing (5)
.claude/board/AGENT_LOG.md.claude/op-codegen-bridge/README.mdsurrealdb/core/src/catalog/schema/field.rssurrealdb/core/src/catalog/schema/index.rssurrealdb/core/src/catalog/table.rs
|
|
||
| In-scope: | ||
| - `TableDefinition`: `new_for_ddl` + setters for `schemafull`, `drop`, | ||
| `comment`, `table_type`, `view`, `permissions`, `changefeed` |
There was a problem hiding this comment.
Remove changefeed from the in-scope list.
The implementation deliberately omits with_changefeed because ChangeFeed is pub(crate) (see the rationale comment at the end of the TableDefinition DDL-builder impl block in table.rs). The AGENT_LOG entry at line 307 also confirms changefeed was skipped alongside other pub(crate) types.
📝 Proposed fix
- `TableDefinition`: `new_for_ddl` + setters for `schemafull`, `drop`,
- `comment`, `table_type`, `view`, `permissions`, `changefeed`
+ `comment`, `table_type`, `view`, `permissions`📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| `comment`, `table_type`, `view`, `permissions`, `changefeed` | |
| `comment`, `table_type`, `view`, `permissions` |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.claude/op-codegen-bridge/README.md at line 48, The README lists
`changefeed` as in-scope but the implementation intentionally omits support for
ChangeFeed (it is pub(crate) and omitted in TableDefinition's DDL-builder and
AGENT_LOG at line 307), so remove `changefeed` from the in-scope list; update
the backtick-delimited list (`comment`, `table_type`, `view`, `permissions`,
`changefeed`) to exclude `changefeed` so it reads `comment`, `table_type`,
`view`, `permissions`.
Makes nexgen's rust-version explicit so it matches the AdaWorldAPI
ecosystem-wide pin. Until now, nexgen relied on the container's
default rustc — recently 1.94.1, silently diverging from
AdaWorldAPI/surrealdb (1.95) and AdaWorldAPI/ndarray (1.95.0).
What landed:
rust-toolchain.toml (new):
channel = "1.95"
components = ["rustfmt", "clippy"]
Comment explains the no-patch-in-channel rule (mirrors the same
caveat surrealdb fork's rust-toolchain.toml carries: "1.95.1" etc.
is not a valid toolchain identifier and breaks rust-analyzer).
Cargo.toml [workspace.package]:
rust-version = "1.95"
Cargo-canonical place to declare MSRV; per-crate opt-in via
`rust-version.workspace = true` (none use it yet).
Why now: planned C16c sprint (the consumer-side bridge to surrealdb's
catalog DDL builders, depending on AdaWorldAPI/surrealdb#33 being
merged) adds a surrealdb-core path-dep, which requires rust 1.95.
Pinning nexgen first means C16c lands without a toolchain surprise.
Verification:
rustup auto-installed 1.95 when the toolchain pin landed:
$ rustc --version
rustc 1.95.0 (59807616e 2026-04-14)
All 72 codegen-family tests green on 1.95:
op-codegen-bucket 7
op-codegen-projection 26
op-codegen-pipeline 15 (9 lib + 6 integration)
op-cli 7
op-surreal-ast 17
No code changes — purely the toolchain config. The 72 tests landing
green prove the change is non-breaking.
Cross-ecosystem pin status (AdaWorldAPI):
surrealdb 1.95 rust-toolchain.toml ✓
ndarray 1.95.0 rust-toolchain.toml ✓
openproject-nexgen-rs 1.95 rust-toolchain.toml + Cargo ✓ (this sprint)
lance-graph presumed 1.95, not verified this sprint ?
Minor patch-version difference between surrealdb (no patch) and
ndarray (patch) flagged for a future ecosystem-pin sweep — not
in-scope here.
Lance / arrow / datafusion / lancedb: nexgen has zero direct deps on
these today. They enter via surrealdb-core if/when C16c adds the dep.
Verified the surrealdb fork's pins (rust 1.95, lance =7.0.0,
lance-index =7.0.0, lancedb =0.30.0, arrow 58, datafusion 53
transitive via lance, ndarray hpc-extras for crate::simd::*) are
already on-spec in
AdaWorldAPI/surrealdb claude/beautiful-gates-dJo0u commit 07b1e2a.
Files changed:
rust-toolchain.toml new
Cargo.toml +1 line (rust-version)
.claude/sprints/c16d-toolchain-pin/STATE new (sprint notes)
https://claude.ai/code/session_01PBTGaPCSnnt6u3pjXpbLwY
Pin verification — full tableVerifying the fork is on-spec for the AdaWorldAPI ecosystem pinning rule. Concrete evidence for each constraint: Rust toolchainLance + LanceDB (exact pins via
|
| Line | Kernel | Type |
|---|---|---|
| 267 | (design note) HNSW vector index built on F64x8 |
comment |
| 414 | CPU detection cached once | dispatch glue |
| 421 | L2 (Euclidean) distance | use ndarray::simd::F64x8; |
| 444, 450 | L1 (Manhattan) distance | use ndarray::simd::F64x8; |
| 469, 475 | L∞ (Chebyshev) distance | use ndarray::simd::F64x8; |
| 496 | Pearson correlation | use ndarray::simd::F64x8; |
Gated by the vector-hpc feature flag. Compile-time route to the SIMD code paths; no extra deps pulled.
Cross-repo consistency
| Repo | Rust pin | Source |
|---|---|---|
AdaWorldAPI/surrealdb |
1.95 |
rust-toolchain.toml |
AdaWorldAPI/ndarray |
1.95.0 (with patch) |
rust-toolchain.toml (minor inconsistency with this fork's no-patch rule) |
AdaWorldAPI/openproject-nexgen-rs |
1.95 (just landed in PR #21) |
rust-toolchain.toml + [workspace.package].rust-version |
The full matrix + verification recipe is captured for future sessions at .claude/knowledge/adaworldapi-pinning.md on this branch (commit 2186d6d).
Posted as a pin-verification audit trail so reviewers can confirm the spec-match without re-grepping. Happy to tighten further (e.g. arrow-array = "=58.3.0" exact, or direct datafusion = "=53.1.0") if those would be preferred — the current state matches the stated AdaWorldAPI constraints ("lance 7.0.0, lanceDb 0.30, datafusion 53, arrow 58") literally.
Follow-up to PR #33: AdaWorldAPI pin docs + ndarray rev pin
The bridge has been pinned at the stranded `claude/op-surreal-ast-field-types-stacked` branch since the D-AR-5.2 work first went up as nexgen-rs#29. That PR landed on a stacked branch instead of main and never made it back, so the nexgen-rs `main` diverged from the bridge's view for several sprints. The D-AR-5.2 work has now re-landed via nexgen-rs#36 (Kind variants + field_type consumer), and nexgen-rs `main` carries significant downstream improvements: - FK direction gating (#33) — only `belongs_to` emits a column - Polymorphic FK fallback (#30) — `option<any>` for unknown targets - Singularization tightening (#34, #35, #37) — fixes corpus quirks like `phases→Phase`, `children→Child`, `job_status→JobStatus`, `:big_integer→Decimal` - Phantom-table guard + order-independent ASSERTs (#27/#28/#26) - Rails type aliases (#37) The bridge's `From<ast::Kind> for catalog::Kind` impl already matches the 7 scalar variants the new main carries, so the bump is mechanical — `cargo check --features op-bridge` passes with no source changes needed. Updates Cargo.lock to the new rev (44550a87 — the merge commit of nexgen-rs#37 on main).
The 16 existing tests cover individual mapping primitives (`Kind::String → CatalogKind::String`, `Option<T> → Either(None, T)`, ASSERT lowering in isolation, etc.). What was missing: a single test that exercises every nexgen-rs-producer feature together through `bridge_schema`, which is the actual ingress point for downstream ingestors. Build one `ast::Schema` containing a `WorkPackage` table with all four kinds of fields the producer side now emits: - `subject` : `option<string>` + `ASSERT $value != NONE` (typed scalar with validates_presence — both Kind + assert flow through D-AR-6.2/6.3) - `position` : `option<int>` (typed scalar, no assert) - `project_id` : `option<record<Project>>` (belongs_to FK after the #33 direction-gating) - `assignable_id` : `option<any>` (polymorphic FK after the #30 fallback) - A companion `idx_WorkPackage_project_id` index - A table comment with AR-shape annotations Assertions probe the catalog SQL render (the internal field accessors are `pub(crate)`) and check each feature is preserved: typed kinds present, ASSERT clauses render for validated fields only, record-link semantics carry the target table, polymorphic fallback renders as `any`, index name preserved. This locks the bridge contract against drift as the producer side keeps evolving — a single broken arm in `From<ast::*>` would fail this test loudly. 17 tests pass.
Summary
Adds DDL-friendly constructors and chainable setters to
catalog::TableDefinition/catalog::schema::FieldDefinition/catalog::schema::IndexDefinitionso external codegen tools can build typed canonical forms purely for DDL emission viaToSql, without needing in-DB allocation orpub(crate)access to internal fields.18/18 new tests green. No new dependencies. No
Cargo.lockchanges. No edits tokvs/lance/*,sql/statements/*, runtime layers, or existing impl blocks (new methods live in dedicated impl blocks beside the existing ones). Single commit, atomic.Downstream consumer and motivation
This is the upstream-side foundation for AdaWorldAPI/openproject-nexgen-rs's codegen pipeline. Nexgen's C16a (already merged: AdaWorldAPI/openproject-nexgen-rs#20) landed a mirror-layout
op-surreal-astcrate that duplicated catalog's struct shapes because:surrealdb-core::sql::statements::DefineTableStatementispub(crate)— not importablecatalog::TableDefinitionISpubbut fields arepub(crate)and the constructor demands runtime IDs (namespace_id,database_id,table_id) + fills cache UUIDsWith this PR landed, nexgen's planned C16c sprint switches mirror→canonical:
From<op_surreal_ast::TableDefinition> for catalog::TableDefinitionimpls + render through the canonicalcatalog::*path. The mirror crate is then deletable.See
.claude/op-codegen-bridge/README.mdfor the full initiative context, scope, and what was deliberately deferred.API added
new_for_ddlconstructors supply dummy zero IDs (NamespaceId(0),DatabaseId(0),TableId(0)) andUuid::now_v7()cache timestamps. The dummy IDs do not appear in rendered SurrealQL — pinned bynew_for_ddl_does_not_leak_table_id_into_render.Each new impl block is annotated
#[allow(dead_code)]with a rationale comment: pub items have no in-crate caller (they exist as the public ergonomic surface for externalsurrealdb-coreconsumers); the#[cfg(test)] ddl_builder_testsmodules exercise every method, but thedead_codelint runs on the non-test build target where no caller is visible.Deliberately deferred
Setters whose parameter types are
pub(crate)and need a public-API decision:PermissionFieldDefinition::{select,create,update}_permissionDefineDefaultFieldDefinition::defaultIndexvariantsUniq/Hnsw/FullText/CountIndexDefinition::indexhas_one) + vector-index sprintChangeFeedTableDefinition::changefeedFor typical Rails-mapped schema codegen the slots covered here are sufficient. The
with_changefeedsetter that was in the original draft triggered a private-in-public warning and was dropped — see the rationale comment incatalog/table.rs.Tests — 18/18 green
TableDefinition (8):
new_for_ddl_does_not_leak_table_id_into_render— dummy IDs invisible in DDLbuilder_output_equals_struct_literal_output— strongest invariant: builder = literal modulo cache UUIDswith_schemafull_round_trips,with_drop_round_trips,with_comment_round_trips,with_table_type_round_tripsnew_for_ddl_supplies_dummy_ids_and_now_caches— verifies IDs at zero, cache UUIDs non-nilchained_builders_preserve_all_set_values— multi-builder chain integrityFieldDefinition (6):
new_for_ddl_defaults_to_no_kind_and_no_constraintsnew_for_ddl_carries_name_and_table_through_to_sqlwith_kind_round_trips,with_flexible_and_readonly_round_trip,with_comment_round_tripsbuilder_output_equals_struct_literal_outputIndexDefinition (4):
new_for_ddl_defaults_to_non_unique_idxnew_for_ddl_carries_name_table_and_cols_to_sqlmulti_column_index_renders_all_columnswith_comment_round_tripsPreflight
cargo check -p surrealdb-core --no-default-featuresgrep -E 'catalog/(table|schema/field|schema/index).rs'on the warnings)cargo test -p surrealdb-core --lib ddl_builder_testscargo fmtrustfmt.tomloptions (same caveat that ndarray fork'sAGENTS.mddocuments). My 3 files are hand-formatted in upstream tab style.cargo audit/cargo deny/cargo clippy -- -D warningsFiles changed
.claude/op-codegen-bridge/README.md.claude/board/AGENT_LOG.mdtee -a)surrealdb/core/src/catalog/table.rssurrealdb/core/src/catalog/schema/field.rssurrealdb/core/src/catalog/schema/index.rsZero edits to:
kvs/lance/*,sql/statements/*,kvs/,iam/, any runtime/transaction layer, or any existingimpl TableDefinition/impl FieldDefinition/impl IndexDefinitionblock. New methods live in dedicated impl blocks beside the originals so identifying / reverting / extending the surface is mechanical.Verified pin-compatibility (AdaWorldAPI ecosystem)
While preparing this PR I verified the fork's existing pins against the AdaWorldAPI ecosystem-wide constraints:
1.95(rust-toolchain.toml)1.95✓lance=7.0.0(exact)7.0.0✓lance-index=7.0.0(exact)7.0.0✓lancedb=0.30.0(exact)0.30.0✓arrow-array5858✓arrow-schema5858✓ndarrayhpc-extrasfeaturedatafusionNo version-pin changes needed in this PR.
Summary by CodeRabbit
New Features
Tests