Skip to content

oracle: support constraint_state clause on constraints (BYT-10010) - #399

Open
rebelice wants to merge 6 commits into
mainfrom
claude/byt-10010-analysis-55795a
Open

oracle: support constraint_state clause on constraints (BYT-10010)#399
rebelice wants to merge 6 commits into
mainfrom
claude/byt-10010-analysis-55795a

Conversation

@rebelice

Copy link
Copy Markdown
Collaborator

Fixes the customer-blocking parser gap from BYT-10010: SQL Review rejected valid Oracle DDL such as PRIMARY KEY (...) USING INDEX LOCAL, blocking MBBank's partitioned-table changes since 3.19.0 (when the ANTLR fallback was removed).

What was broken

The Oracle parser implemented only DEFERRABLE / INITIALLY out of Oracle's constraint_state grammar. Everything else failed with a syntax error:

  • every USING INDEX form (LOCAL, bare, TABLESPACE, index name, (CREATE INDEX ...))
  • ENABLE / DISABLE, VALIDATE / NOVALIDATE, RELY / NORELY
  • column-level equivalents, including NOT NULL ENABLE — the default shape of DBMS_METADATA.GET_DDL output
  • the same clauses on ALTER TABLE ADD/MODIFY CONSTRAINT

What this PR does

Commit 1 — shared constraint_state implementation. One parseConstraintState plus a parseUsingIndexClause whose whitelist-driven property parser terminates safely inside a CREATE TABLE column list (deliberately not shared with parseCreateIndexAttributes, whose collector runs to the statement terminator). Wired into table-level, column-level, and NOT NULL/NULL paths; collapses the three ad-hoc ALTER TABLE implementations (skipConstraintState is deleted). Also fixes adjacent gaps found while auditing the same family: COMPUTE STATISTICS in USING INDEX (DBMS_METADATA output), table-level MAXTRANS, IOT PCTTHRESHOLD, out-of-line view constraints, and CREATE/ALTER MATERIALIZED VIEW USING INDEX / USING NO INDEX / USING TRUSTED|ENFORCED CONSTRAINTS.

Commit 2 — view constraints in the AST. CreateViewStmt.Constraints (list of *TableConstraint), walker regenerated.

Commit 3 — slot order, verified against a real engine. Cross-validated against Oracle 23ai (gvenzl/oracle-free testcontainer, real DDL execution). Oracle is not order-free: out-of-order subclauses raise ORA-03075, so parseConstraintState enforces the documented slot sequence (DEFERRABLE/INITIALLY interchangeable within their group, then RELY|NORELY, using_index_clause, ENABLE|DISABLE, VALIDATE|NOVALIDATE). EXCEPTIONS INTO moved to a separate helper used only by ALTER contexts — Oracle rejects it inside CREATE TABLE constraints (ORA-00922).

AST changes

  • TableConstraint: new UsingIndexLocal; the previously-dead Tablespace field is now populated
  • ColumnConstraint: new Tablespace, UsingIndexLocal
  • CreateViewStmt: new Constraints

Verification

  • 27 repro cases from the issue investigation pass, including MBBank's full statement
  • BYT-9909's customer corpus replayed clean (every complete statement parses; the only failures are Excel's 32766-char cell truncation artifacts)
  • Real-engine comparison: 33/34 statements agree with Oracle 23ai (accept ⇔ accept, reject ⇔ reject). The one deliberate leniency is ALTER MATERIALIZED VIEW USING INDEX PCTFREE, which 23ai rejects (ORA-02243) while accepting INITRANS/STORAGE; noted in a comment
  • Full ./oracle/... suite passes, including the parser contract gates

Landing

After merge: go get -u github.com/bytebase/omni in bytebase, run parser/plsql / advisor/oracle / schema/oracle, cherry-pick to 3.21.x for MBBank. Wiring local-index semantics into IndexMetadata is deferred to a separate issue.

🤖 Generated with Claude Code

rebelice and others added 3 commits August 14, 2026 17:03
The Oracle parser only implemented DEFERRABLE / INITIALLY out of
Oracle's constraint_state grammar. Everything else — USING INDEX (all
forms), ENABLE/DISABLE, VALIDATE/NOVALIDATE, RELY/NORELY, EXCEPTIONS
INTO — failed with a syntax error, rejecting valid DDL such as
PRIMARY KEY (...) USING INDEX LOCAL and the NOT NULL ENABLE emitted
by DBMS_METADATA.

Add a shared parseConstraintState that accepts the subclauses in any
order, and a parseUsingIndexClause whose whitelist-driven property
parser terminates safely inside a CREATE TABLE column list. Wire them
into table-level constraints, column-level constraints, the implicit
NOT NULL/NULL paths, and collapse the three ad-hoc ALTER TABLE
implementations (skipConstraintState, the ENABLE/DISABLE USING INDEX
block, and the ADD CONSTRAINT path) onto the same code.

Also fix adjacent gaps found while auditing the same family:
- USING INDEX ... COMPUTE STATISTICS (DBMS_METADATA output)
- table-level MAXTRANS and IOT PCTTHRESHOLD options
- out-of-line view constraints (CONSTRAINT ... RELY DISABLE NOVALIDATE)
- CREATE/ALTER MATERIALIZED VIEW USING INDEX, USING NO INDEX,
  USING TRUSTED/ENFORCED CONSTRAINTS

TableConstraint gains UsingIndexLocal and now populates the
previously-dead Tablespace field; ColumnConstraint gains both.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Follow-up to the constraint_state change: CREATE VIEW's out-of-line
constraints (CONSTRAINT ... PRIMARY KEY/UNIQUE/FOREIGN KEY ... RELY
DISABLE NOVALIDATE) were parsed and discarded. Store them on the new
CreateViewStmt.Constraints list as *TableConstraint nodes so schema
extraction can consume them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…23ai

Cross-validated the constraint_state implementation against a real
Oracle 23ai instance (gvenzl/oracle-free testcontainer). Oracle is not
order-free: out-of-order subclauses raise ORA-03075 (e.g. ENABLE USING
INDEX, DISABLE RELY, NOVALIDATE ENABLE). Rewrite parseConstraintState
from a free-order loop to the documented slot sequence — [NOT]
DEFERRABLE / INITIALLY (interchangeable within the group), then
RELY|NORELY, using_index_clause, ENABLE|DISABLE, VALIDATE|NOVALIDATE —
so review verdicts match rollout behavior in both directions.

EXCEPTIONS INTO moves out of parseConstraintState into its own
parseExceptionsIntoClause: Oracle rejects it inside CREATE TABLE
constraints (ORA-00922) and accepts it only in ALTER TABLE enable /
modify-constraint contexts, which now share the helper.

Engine comparison result: 33/34 statements agree. The one deliberate
leniency is ALTER MATERIALIZED VIEW USING INDEX PCTFREE, which 23ai
rejects (ORA-02243) while INITRANS/STORAGE are accepted; noted in a
comment.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 026cde98d5

ℹ️ 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".

Comment thread oracle/parser/constraint_state.go
Comment thread oracle/parser/constraint_state.go Outdated
Comment thread oracle/parser/create_view.go Outdated
All three Codex review findings verified against Oracle 23ai before
fixing:

- Accept the remaining legal index_attributes in using_index_clause:
  PARALLEL [n], NOPARALLEL, INDEXING FULL|PARTIAL, and COMPRESS
  ADVANCED [LOW|HIGH]. All parse on 23ai (their failures — ORA-03001,
  ORA-12812, ORA-14227, ORA-25193 — are semantic, not syntax).
- Validate the parenthesized using_index form: require the CREATE
  [UNIQUE|BITMAP] INDEX prefix instead of skipping any balanced tokens.
  Oracle raises ORA-02000 "missing CREATE keyword" for anything else.
- Restrict view constraints to their actual grammar: [RELY|NORELY]
  DISABLE [NOVALIDATE], with DISABLE mandatory (ORA-02000) and
  VALIDATE rejected (ORA-03082). parseTableConstraint is split into
  parseTableConstraintBody + state so CREATE VIEW and ALTER VIEW ADD
  CONSTRAINT share the body but apply the view-specific state.

Engine comparison after the fixes: 39/40 agree; the sole remaining
deliberate leniency is ALTER MATERIALIZED VIEW USING INDEX PCTFREE.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8da4176a0f

ℹ️ 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".

Comment thread oracle/parser/constraint_state.go
Comment thread oracle/parser/constraint_state.go Outdated
Comment thread oracle/parser/alter_table.go
All three findings verified against Oracle 23ai before fixing:

- GLOBAL PARTITION BY HASH now accepts hash_partitions_by_quantity:
  PARTITIONS n [STORE IN (ts, ...)] in addition to explicit partition
  specs.
- The parenthesized using_index form validates the nested statement's
  required structure — CREATE [UNIQUE|BITMAP] INDEX name ON table (...)
  — matching Oracle's ORA-00953 (missing index name) and ORA-00969
  (missing ON). Trailing index attributes remain balanced-skipped.
- MODIFY PRIMARY KEY / MODIFY UNIQUE attach a populated TableConstraint
  (type, Deferrable, Initially, Tablespace, UsingIndexLocal) instead of
  discarding the parsed state, matching the MODIFY CONSTRAINT branch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9d742c94bd

ℹ️ 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".

Comment thread oracle/parser/constraint_state.go
Comment thread oracle/parser/constraint_state.go Outdated
Comment thread oracle/parser/create_view.go
Comment thread oracle/parser/constraint_state.go Outdated
Comment thread oracle/parser/create_view.go Outdated
Comment thread oracle/parser/create_table.go
Each finding was tested against Oracle 23ai before acting; three were
real, two were refuted by the engine, one was wrong but exposed a
different gap:

- PARTITIONS quantity is now HASH-only: GLOBAL PARTITION BY RANGE ...
  PARTITIONS n is rejected (Oracle: ORA-00906).
- LOCAL STORE IN (...) and explicit partition specs are now mutually
  exclusive, matching ORA-14153/ORA-03075 — the review's suggested
  "partition list then STORE IN" form is itself rejected by Oracle.
- USING TRUSTED|ENFORCED CONSTRAINTS now requires a preceding REFRESH
  clause (Oracle: ORA-00906 for the standalone form).
- PCTTHRESHOLD now requires ORGANIZATION INDEX (Oracle: ORA-00922 on
  heap tables).

Not changed, with engine evidence recorded in tests:

- IF NOT EXISTS inside the nested USING INDEX (CREATE INDEX ...) form
  is rejected by Oracle itself (ORA-00969), although valid standalone;
  the current rejection matches the engine.
- Column aliases after an out-of-line view constraint are accepted by
  Oracle despite the documented BNF ordering; the current acceptance
  matches the engine.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 98ba1ba62f

ℹ️ 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".

Comment on lines +274 to +276
isHash := p.cur.Type == kwHASH
if p.cur.Type == kwRANGE || p.cur.Type == kwHASH {
p.advance()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Require RANGE or HASH after PARTITION BY

When GLOBAL PARTITION BY is followed directly by (, this conditional simply skips the method instead of rejecting it, so invalid SQL such as CREATE TABLE t (a NUMBER, CONSTRAINT pk PRIMARY KEY (a) USING INDEX GLOBAL PARTITION BY (a) (PARTITION p1 VALUES LESS THAN (MAXVALUE))) parses successfully. global_partitioned_index in oracle/parser/bnf/CREATE-INDEX.bnf requires exactly RANGE or HASH here, so return a syntax error when neither token is present.

Useful? React with 👍 / 👎.

Comment on lines +202 to +205
if p.cur.Type != '(' {
return p.syntaxErrorAtCur()
}
depth := 1 // the outer '(' consumed above is still open

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Validate the nested index column list before skipping it

Fresh evidence beyond the resolved missing-name/ON cases is that this only checks for an opening parenthesis and then skips its contents, so USING INDEX (CREATE INDEX idx ON t ()) and malformed lists such as (a,) are accepted even though table_index_clause requires one or more valid index_expr entries. Parse the column list rather than treating any balanced group as sufficient.

Useful? React with 👍 / 👎.

Comment on lines +221 to +222
if p.cur.Type == tokQIDENT ||
(p.cur.Type == tokIDENT && !identLikeUsingIndexProperties[p.cur.Str]) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Parse ANNOTATIONS as a USING INDEX property

Fresh evidence beyond the four resolved index attributes is the ANNOTATIONS alternative in oracle/parser/bnf/CREATE-INDEX.bnf, which the standalone CREATE INDEX parser already handles. Here ANNOTATIONS is absent from the property-starter set, so USING INDEX ANNOTATIONS (classification 'pii') treats ANNOTATIONS as an index name and leaves ( unconsumed, rejecting valid constraint DDL; classify and consume this attribute with the other index properties.

Useful? React with 👍 / 👎.

Comment on lines +114 to +116
// parseExceptionsIntoClause parses [ EXCEPTIONS INTO [schema.]table ] if
// present. Oracle allows this only in ALTER TABLE enable/modify-constraint
// contexts, not inside CREATE TABLE constraints.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Accept EXCEPTIONS INTO when adding a constraint

This restriction omits ALTER TABLE ... ADD CONSTRAINT, although constraint_clauses and constraint_state in oracle/parser/bnf/ALTER-TABLE.bnf allow the trailing exceptions_clause on an added out-of-line constraint. Consequently, valid statements such as ALTER TABLE t ADD CONSTRAINT pk PRIMARY KEY (a) ENABLE VALIDATE EXCEPTIONS INTO bad_rows leave EXCEPTIONS unconsumed and fail; invoke this helper from the ADD-constraint path as well.

Useful? React with 👍 / 👎.

Comment on lines +1666 to +1670
case "MAXTRANS":
p.advance() // consume MAXTRANS
if p.cur.Type == tokICONST {
p.advance()
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Require an integer after table-level MAXTRANS

The newly added MAXTRANS branch makes its numeric operand optional, so CREATE TABLE t (a NUMBER) MAXTRANS reaches the statement terminator and parses successfully even though MAXTRANS requires an integer. The USING INDEX implementation correctly rejects the same missing operand; apply that validation here rather than silently accepting an incomplete physical attribute.

Useful? React with 👍 / 👎.

// UNIQUE | FOREIGN KEY } (cols) [RELY|NORELY] DISABLE [NOVALIDATE].
// View constraints are declarative only; the table-only state
// clauses are rejected — see parseViewConstraintState.
if p.isTableConstraintStart() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Recognize datatype-named view constraints

Using the table-specific start predicate here rejects valid out-of-line view constraints whose unquoted name is a nonreserved datatype token. For example, CREATE VIEW v (a, CONSTRAINT blob PRIMARY KEY (a) DISABLE) AS SELECT 1 FROM dual treats CONSTRAINT as an alias because BLOB satisfies isOracleTypeToken, then fails at the column list; unlike a CREATE TABLE column list, a view alias list has no datatype declaration ambiguity, so CONSTRAINT should always start a constraint here.

Useful? React with 👍 / 👎.

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.

1 participant