Skip to content

Implement SQL Planner for Logical Plans and Statement Validation - #38

Open
rahulc0dy wants to merge 14 commits into
mainfrom
feat/planner
Open

Implement SQL Planner for Logical Plans and Statement Validation#38
rahulc0dy wants to merge 14 commits into
mainfrom
feat/planner

Conversation

@rahulc0dy

@rahulc0dy rahulc0dy commented Jul 10, 2026

Copy link
Copy Markdown
Member

Issue Reference

Summary by CodeRabbit

  • New Features
    • Added SQL planning for database and table management, data changes, and SELECT queries.
    • Added joins, filtering, grouping, aggregates, DISTINCT, sorting, pagination, aliases, ordinals, and wildcard expansion.
    • Improved type checking, name resolution, foreign-key validation, and semantic diagnostics.
  • Bug Fixes
    • Added validation for invalid grouping, duplicate assignments, unsupported comparisons, and incompatible expressions.
  • Tests
    • Added comprehensive planner coverage.
  • Documentation
    • Updated the README coverage badge.

@rahulc0dy
rahulc0dy requested a review from theMr17 July 10, 2026 09:08
@rahulc0dy rahulc0dy self-assigned this Jul 10, 2026
@rahulc0dy rahulc0dy added the feature New feature or request label Jul 10, 2026
@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The PR adds a SQL planner that resolves catalog identifiers, expressions, conditions, and types, then produces logical plans for SELECT, DDL, INSERT, UPDATE, and DELETE statements with diagnostics and unit tests.

Changes

SQL planner implementation

Layer / File(s) Summary
Planner contracts and plan IR
internal/sql/planner/planner.go, internal/sql/planner/errors.go, internal/sql/planner/statements.go, internal/sql/planner/operators.go, internal/sql/planner/expressions.go, internal/sql/planner/conditions.go, internal/sql/planner/op.go, internal/sql/ast/clauses.go
Defines planner entry points, diagnostic codes, resolved expression and condition nodes, planner operators, relational nodes, statement plans, and the internal NULL type sentinel.
Identifier, expression, and condition resolution
internal/sql/planner/resolve.go, internal/sql/planner/typecheck.go, internal/sql/planner/plan_expressions.go, internal/sql/planner/plan_conditions.go, internal/sql/planner/op_convert.go, internal/sql/planner/resolve_test.go, internal/sql/planner/plan_expressions_test.go, internal/sql/planner/plan_conditions_test.go
Resolves catalog names, scopes, typed expressions, aggregates, and predicates. It validates operator compatibility, orderability, nullability, and function arguments.
SELECT relational planning and grouping
internal/sql/planner/aggregate.go, internal/sql/planner/plan_queries.go, internal/sql/planner/plan_queries_test.go
Builds scan, join, filter, projection, aggregation, distinct, sort, and limit trees. It validates wildcard expansion, aliases, ORDER BY, HAVING, aggregates, and GROUP BY rules.
DDL and DML planning
internal/sql/planner/plan_statements.go, internal/sql/planner/plan_statements_test.go
Plans database and table operations plus INSERT, UPDATE, and DELETE statements. It validates schemas, defaults, foreign keys, assignments, predicates, row sources, and cancellation.
Coverage reporting support
README.md
Updates the coverage badge endpoint.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Planner
  participant Catalog
  participant PlanContext
  participant LogicalPlan
  Client->>Planner: Submit AST statement
  Planner->>PlanContext: Create planning context
  PlanContext->>Catalog: Resolve databases, tables, and columns
  Catalog-->>PlanContext: Return catalog metadata
  PlanContext->>LogicalPlan: Construct resolved plan tree
  LogicalPlan-->>Planner: Return plan and diagnostics
  Planner-->>Client: Return planning result
Loading

Assessment against linked issues

Objective Addressed Explanation
Translate SELECT AST nodes into resolved relational operator trees [#36]
Translate INSERT, UPDATE, and DELETE statements with scope and type validation [#36]
Translate DDL statements into catalog-ready plans and schema metadata [#36]
Provide self-contained plans with semantic diagnostics [#36]

Out-of-scope changes

Code Change Explanation
Coverage badge endpoint update (README.md:13) The linked issue specifies planner implementation and validation, not README coverage reporting.

Merge Risk: 🟡 Moderate · up to 68d06

The planner can currently accept invalid grouped queries involving self-joins, while related validation paths inconsistently handle NULL values and internal NULL types. These are bounded but concrete SQL correctness issues, so the PR is not merge-ready until they are fixed or explicitly accepted.

Warning

Your free Security trial is over. An organization admin can activate billing to continue.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@rahulc0dy rahulc0dy changed the title Implement SQL name resolution logic @coderabbitai Jul 10, 2026
@Souvik606
Souvik606 self-requested a review July 13, 2026 15:47
@rahulc0dy
rahulc0dy marked this pull request as ready for review July 15, 2026 14:44
@coderabbitai coderabbitai Bot changed the title @coderabbitai Implement SQL Planner for Logical Plans and Statement Validation Jul 15, 2026

@coderabbitai coderabbitai 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.

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/sql/planner/statements.go (1)

167-168: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Run goimports before merging.

The committed file fails the required lint check at Line 167. Apply goimports to remove the formatting discrepancy.

goimports -w internal/sql/planner/statements.go

Sources: Linters/SAST tools, Pipeline failures

🧹 Nitpick comments (1)
internal/sql/planner/plan_statements_test.go (1)

267-306: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing coverage for ADD COLUMN ... UNIQUE / ... PRIMARY KEY rejection.

planAlterSchema's AlterAdd case has dedicated "not supported in v1" diagnostics for Unique and PrimaryKey columns, but no test here exercises either path (only the NOT-NULL-without-default and duplicate-column paths are covered).

func TestPlanAlterTable_AddUniqueColumn_Errors(t *testing.T) {
	pc := newPlanContext(testCatalog(), Session{ActiveDatabase: "shop"}, nil)
	stmt := &ast.AlterTableStmt{
		Table:  ident("users"),
		Action: &ast.AlterAction{Kind: ast.AlterAdd, Column: colDef("email", ast.TypeText, &ast.UniqueConstraint{})},
	}
	_, err := pc.planAlterTable(stmt)
	if err == nil || pc.diag[len(pc.diag)-1].Code != CodeUnsupportedAlter {
		t.Errorf("expected CodeUnsupportedAlter, got err=%v diag=%+v", err, pc.diag)
	}
}

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 8dbf5033-632c-4ebb-8109-c14f73ba7f10

📥 Commits

Reviewing files that changed from the base of the PR and between 3f01808 and 5e3ba6c.

📒 Files selected for processing (19)
  • internal/sql/planner/.gitkeep
  • internal/sql/planner/aggregate.go
  • internal/sql/planner/conditions.go
  • internal/sql/planner/errors.go
  • internal/sql/planner/expressions.go
  • internal/sql/planner/operators.go
  • internal/sql/planner/plan_conditions.go
  • internal/sql/planner/plan_conditions_test.go
  • internal/sql/planner/plan_expressions.go
  • internal/sql/planner/plan_expressions_test.go
  • internal/sql/planner/plan_queries.go
  • internal/sql/planner/plan_queries_test.go
  • internal/sql/planner/plan_statements.go
  • internal/sql/planner/plan_statements_test.go
  • internal/sql/planner/planner.go
  • internal/sql/planner/resolve.go
  • internal/sql/planner/resolve_test.go
  • internal/sql/planner/statements.go
  • internal/sql/planner/typecheck.go

Comment thread internal/sql/planner/aggregate.go
Comment thread internal/sql/planner/plan_expressions.go
Comment thread internal/sql/planner/plan_queries.go
Comment thread internal/sql/planner/plan_queries.go Outdated
Comment thread internal/sql/planner/plan_statements.go Outdated
Comment thread internal/sql/planner/plan_statements.go
Comment thread internal/sql/planner/plan_statements.go Outdated
Comment thread internal/sql/planner/plan_statements.go
Comment thread internal/sql/planner/typecheck.go
@rahulc0dy

Copy link
Copy Markdown
Member Author

/coverage

@github-actions

Copy link
Copy Markdown

Coverage report (a0a677a)

Packages changed in this PR:

Package Coverage Statements
internal/sql/planner 81.4% 717/881
TOTAL (PR-affected) 81.4% 717/881
Treemap — by file

coverage treemap by file

Box size = statements per file. Color = coverage heat, red (low) → green (high).

Treemap — by package (with %)

coverage treemap by package

Box size = statements per package. Color = coverage heat, red (low) → green (high).

@rahulc0dy

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai 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.

Actionable comments posted: 2


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d0f230f9-5090-44b1-85dc-1f0848b98004

📥 Commits

Reviewing files that changed from the base of the PR and between d0aaa4c and a0a677a.

📒 Files selected for processing (11)
  • internal/sql/planner/aggregate.go
  • internal/sql/planner/errors.go
  • internal/sql/planner/plan_conditions.go
  • internal/sql/planner/plan_conditions_test.go
  • internal/sql/planner/plan_expressions.go
  • internal/sql/planner/plan_expressions_test.go
  • internal/sql/planner/plan_queries.go
  • internal/sql/planner/plan_queries_test.go
  • internal/sql/planner/plan_statements.go
  • internal/sql/planner/plan_statements_test.go
  • internal/sql/planner/typecheck.go
🚧 Files skipped from review as they are similar to previous changes (5)
  • internal/sql/planner/aggregate.go
  • internal/sql/planner/plan_expressions.go
  • internal/sql/planner/typecheck.go
  • internal/sql/planner/plan_queries.go
  • internal/sql/planner/plan_expressions_test.go

Comment thread internal/sql/planner/plan_conditions.go Outdated
Comment thread internal/sql/planner/plan_statements.go

@Souvik606 Souvik606 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Please check following reviews and do the needful.

Comment thread internal/sql/planner/expressions.go Outdated
Comment on lines +82 to +84
type ResolvedColumnRef struct {
Column *ResolvedColumn
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

If any pass in the planner or executor mutates a ResolvedColumn in place (e.g. changing Index, Name, or Type), it mutates all references across the plan simultaneously without thread/memory safety.

Store ResolvedColumn by value or mark it immutable or think of some brilliant idea

Comment thread internal/sql/planner/conditions.go Outdated
Comment thread internal/sql/planner/operators.go Outdated
Comment thread internal/sql/planner/operators.go Outdated
Comment thread internal/sql/planner/statements.go Outdated
type DropDatabasePlan struct {
PlanBase
Name string
Tables []*catalog.TableMeta

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The executor only needs table names to delete keys. Holding full *catalog.TableMeta pointers needlessly pins large catalog schema trees in memory.

Store a slice of table names Tables []string instead of full metadata pointers.

Comment on lines +19 to +21
type Session struct {
ActiveDatabase string
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Session struct contains only ActiveDatabase string.It cannot pass connection context (context.Context), query execution timeouts etc

Expand Session struct:

type Session struct {
ActiveDatabase string
Ctx context.Context
}

Need to check context cancellation stuffs in the concrete planner implementation codes too.Do it comfortably.

@Souvik606
Souvik606 self-requested a review August 15, 2026 20:37
coderabbitai[bot]
coderabbitai Bot previously approved these changes Aug 18, 2026

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/sql/planner/plan_statements.go (1)

406-414: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Allow NULL results from INSERT ... SELECT.

When a SELECT output has the NULL type, typesCompatible rejects it against a typed target column. The VALUES path uses exprsCompatible, which accepts NULL expressions. Apply the same NULL rule here. Add coverage for INSERT INTO ... SELECT NULL.

♻️ Duplicate comments (1)
internal/sql/planner/aggregate.go (1)

8-9: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Preserve relation-instance identity in GROUP BY comparisons.

Line 9 compares only physical-column fields. In a self-join, u1.id and u2.id have the same compared values. Therefore, GROUP BY u1.id incorrectly permits SELECT u2.id.

Store a relation binding or immutable relation-instance key in the resolved column identity. Compare that key here. Add a self-join regression test.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 6fb4b531-f7b6-4880-a0ef-68de851315ac

📥 Commits

Reviewing files that changed from the base of the PR and between 16760f5 and 68d064f.

📒 Files selected for processing (17)
  • internal/sql/ast/clauses.go
  • internal/sql/planner/aggregate.go
  • internal/sql/planner/conditions.go
  • internal/sql/planner/errors.go
  • internal/sql/planner/expressions.go
  • internal/sql/planner/op.go
  • internal/sql/planner/op_convert.go
  • internal/sql/planner/operators.go
  • internal/sql/planner/plan_conditions.go
  • internal/sql/planner/plan_expressions.go
  • internal/sql/planner/plan_expressions_test.go
  • internal/sql/planner/plan_queries.go
  • internal/sql/planner/plan_statements.go
  • internal/sql/planner/plan_statements_test.go
  • internal/sql/planner/planner.go
  • internal/sql/planner/statements.go
  • internal/sql/planner/typecheck.go

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.

TypeFloat // FLOAT
TypeDouble // DOUBLE
TypeDecimal // DECIMAL
TypeNull // NULL (sentinel; not a user-facing SQL type)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reject TypeNull in DataType.Validate.

TypeNull is an internal sentinel. DataType.Validate accepts it because no validation branch rejects it. A programmatic DDL AST can then define a column with NULL as its declared type.

Reject TypeNull during AST validation. Use it only for resolved expressions.

@Souvik606 Souvik606 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Please review these comments and fix as desirable and correct.

Comment thread internal/sql/planner/aggregate.go Outdated
// same physical column — used to check whether a SELECT-list or HAVING
// column reference matches one of the query's GROUP BY keys.
func columnRefsEqual(a, b *ResolvedColumn) bool {
return a == b

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Compare by value identity instead

func columnRefsEqual(a, b *ResolvedColumn) bool {
return a.Database == b.Database && a.Table == b.Table && a.Name == b.Name
}

Comment on lines +138 to +140
if qualifier != "" {
return qualifier, nil
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

When qualifier != "", the function returns it without verifying that the database actually exists in the catalog.Add a check

Name: col.Name,
Index: index,
Type: col.Type,
VarcharLen: col.VarcharLen,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

VarcharLen *int is a pointer. This copies the pointer from catalog.ColumnMeta into ResolvedColumn. Both the catalog and the plan now share the same *int. If either side ever mutates the integer value through the pointer, it silently changes the other.So better deep-copy the pointer:

Comment on lines +90 to +91
case *ResolvedFunctionCall:
return nil

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

validateGroupedExpr stops recursion at ResolvedFunctionCall. This correctly allows SUM(salary) in a GROUP BY query. But it also silently allows nested aggregates like SUM(COUNT(*)), which is illegal in standard SQL.Add a nested-aggregate check.

return &ResolvedBinaryExpr{ResolvedExprBase: newExprBase(resultType), Left: left, Op: be.Op, Right: right}, nil
}

func (pc *planContext) resolveUnaryExpr(scope *Scope, ue *ast.UnaryExpr) (ResolvedExpr, error) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Negative float literals carry an extra unary operator node during execution, requiring extra CPU dispatch per row. Add float literal folding like integer

Comment on lines +55 to +59
func (pc *planContext) resolveSelectExpression(scope *Scope, se *ast.SelectExpression) (ResolvedExpr, error) {
if se.Expr != nil {
return pc.resolveExpr(scope, se.Expr)
}
cond, err := pc.resolveCond(scope, se.Cond)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

If both se.Expr and se.Cond are nil (a malformed AST node), line 59 calls pc.resolveCond(scope, nil), which will hit the type switch in resolveCond with a nil condition and panic on cond.Span() .A malformed AST from a buggy parser causes a panic crash instead of a clean error.Add a nil guard:

return nil, err
}

resultType, ok := aggregateResultType(name, arg.ResolvedType())

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

If the function argument is NULL (e.g. SUM(NULL)), arg is a *ResolvedNullLiteral, and arg.ResolvedType() returns the dummy placeholder ast.TypeInt.

So aggregateResultType("SUM", ast.TypeInt) returns (TypeBigInt, true) — meaning SUM(NULL) is typed as BIGINT. This is technically harmless at runtime (the result is NULL regardless), but the reported output column type is wrong. The executor or client will see BIGINT as the column type when it should arguably be indeterminate or NULL.

For AVG(NULL), the placeholder TypeInt flows through aggregateResultType("AVG", TypeInt) → returns TypeDouble. So AVG(NULL) reports type DOUBLE. Neither is truly correct — the type should be unknown/null.

return &ResolvedFloatLiteral{ResolvedExprBase: newExprBase(ast.TypeDouble), Value: v}, nil
}

func (pc *planContext) resolveBinaryExpr(scope *Scope, be *ast.BinaryExpr) (ResolvedExpr, error) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The planner validates that operand types are numeric, but does not check for constant division by zero
The planner accepts this query. The division-by-zero error is only discovered at execution time when evaluating the expression per row.

Please consider whether this should be stopped at planning phase or be passed and catched at executor phase

Comment on lines +86 to +92
} else if !exprsCompatible(left, right) {
return nil, pc.errorf(
cp.Span(), CodeTypeMismatch,
"cannot compare %s and %s", exprTypeName(left), exprTypeName(right),
)
}
return &ResolvedComparison{Left: left, Op: op, Right: right}, nil

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

In SQL, NULL = NULL always evaluates to NULL (unknown), never TRUE. Any WHERE NULL = NULL condition filters out all rows — it is almost certainly a user mistake.

Emit a diagnostic warning (not an error) when comparing with NULL using = or !=.

// mismatches, so a query with several bad values reports all of them in one
// pass. It still returns the first error encountered, since a partially
// resolved ResolvedIn cannot be used by the caller.
func (pc *planContext) resolveIn(scope *Scope, ip *ast.InPredicate) (ResolvedCond, error) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

exprsCompatible(expr, NULL) returns true (NULL is compatible with anything), so NULL is silently added to the ResolvedIn.Values list.

In SQL, x IN (1, NULL, 3) never returns TRUE when x is the NULL value — it returns NULL. A NULL inside an IN list is almost always a user mistake and has no useful filtering effect (it can only change a FALSE to NULL, but NULL is still filtered out by WHERE).

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

Labels

feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement Planner to Translate ASTs into Logical Plan Trees

3 participants