Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions docs/plans/2026-04-24-pg-completion-scope.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# PostgreSQL Completion Scope Implementation Plan

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

**Goal:** Add a PostgreSQL completion-only API that returns parser candidates plus the visible FROM scope at the cursor.

**Architecture:** Keep ordinary `Parse` strict and avoid exposing partial ASTs. Add `parser.CollectCompletion(sql, cursorOffset)` as a completion context API that wraps existing `Collect` candidates and computes a best-effort `ScopeSnapshot` from grammar/token facts and parseable prefixes. ByteBase remains responsible for metadata and final candidate formatting.

**Tech Stack:** Go, `github.com/bytebase/omni/pg/parser`, `github.com/bytebase/omni/pg/ast`.

### Task 1: Scope API Tests

**Files:**
- Create: `pg/parser/complete_context_test.go`

**Steps:**
- Write failing tests for `CollectCompletion`.
- Cover incomplete `JOIN ... USING (`, aliases with alias columns, incomplete `JOIN`, subquery aliases, and CTE references.
- Run `go test ./pg/parser -run 'TestCollectCompletion' -count=1`; expected failure is missing API/types.

### Task 2: Minimal API and Scope Extraction

**Files:**
- Create: `pg/parser/complete_context.go`

**Steps:**
- Define `CompletionContext`, `ScopeSnapshot`, `RangeReference`, and `RangeReferenceKind`.
- Implement `CollectCompletion` by reusing `Collect` and extracting visible references for the cursor's SELECT scope.
- Preserve strict `Parse`; completion-only recovery may tolerate incomplete join qualifiers.
- Run focused tests until green.

### Task 3: Public Package Wrapper

**Files:**
- Create: `pg/completion.go`

**Steps:**
- Add a small `pg.CollectCompletion` wrapper if downstream users prefer the top-level pg package.
- Keep the parser package API as the primary implementation.
- Run `go test ./pg ./pg/parser -run 'TestCollectCompletion|TestParse' -count=1`.

### Task 4: Verification

**Steps:**
- Run `go test ./pg/parser ./pg/completion`.
- Confirm ordinary strict parse behavior remains covered by existing parser tests.
29 changes: 29 additions & 0 deletions pg/completion.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package pg

import "github.com/bytebase/omni/pg/parser"

// CompletionContext is the parser-native context for SQL completion.
type CompletionContext = parser.CompletionContext

// ScopeSnapshot describes the SELECT-level relation scope visible at a cursor.
type ScopeSnapshot = parser.ScopeSnapshot

// RangeReference is a syntax-level FROM/JOIN reference.
type RangeReference = parser.RangeReference

// RangeReferenceKind classifies a range-table entry visible to completion.
type RangeReferenceKind = parser.RangeReferenceKind

const (
RangeReferenceRelation = parser.RangeReferenceRelation
RangeReferenceSubquery = parser.RangeReferenceSubquery
RangeReferenceFunction = parser.RangeReferenceFunction
RangeReferenceJoinAlias = parser.RangeReferenceJoinAlias
RangeReferenceCTE = parser.RangeReferenceCTE
)

// CollectCompletion returns completion candidates plus a best-effort visible
// relation scope at cursorOffset. Ordinary Parse remains strict.
func CollectCompletion(sql string, cursorOffset int) *CompletionContext {
return parser.CollectCompletion(sql, cursorOffset)
}
24 changes: 24 additions & 0 deletions pg/completion_context_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package pg

import (
"testing"

"github.com/bytebase/omni/pg/parser"
)

func TestCollectCompletionWrapper(t *testing.T) {
sql := "SELECT * FROM t1 JOIN t2 USING ("
ctx := CollectCompletion(sql, len(sql))
if ctx == nil || ctx.Candidates == nil || ctx.Scope == nil {
t.Fatal("expected completion context")
}
if !ctx.Candidates.HasRule("columnref") {
t.Fatal("expected columnref candidate")
}
if len(ctx.Scope.References) != 2 {
t.Fatalf("reference count = %d, want 2", len(ctx.Scope.References))
}
if ctx.Scope.References[0].Kind != parser.RangeReferenceRelation {
t.Fatalf("first reference kind = %v, want relation", ctx.Scope.References[0].Kind)
}
}
Loading
Loading