// Package cypher provides Cypher query parsing and execution for NornicDB.
package cypher
import (
"context"
"fmt"
"strings"
)
// QueryType represents the type of Cypher query.
type QueryType int
const (
QueryMatch QueryType = iota
QueryCreate
QueryMerge
QueryDelete
QuerySet
QueryReturn
QueryWith
)
// Parser parses Cypher queries into AST.
type Parser struct{}
// NewParser creates a new Cypher parser.
func NewParser() *Parser {
return &Parser{}
}
// Query represents a parsed Cypher query.
type Query struct {
Type QueryType
Clauses []Clause
Parameters map[string]any
}
// Clause represents a query clause.
type Clause interface {
clauseMarker()
}
// MatchClause represents a MATCH clause.
type MatchClause struct {
Pattern Pattern
Optional bool
Where *WhereClause
}
func (c *MatchClause) clauseMarker() {}
// CreateClause represents a CREATE clause.
type CreateClause struct {
Pattern Pattern
}
func (c *CreateClause) clauseMarker() {}
// ReturnClause represents a RETURN clause.
type ReturnClause struct {
Items []ReturnItem
OrderBy []OrderItem
Skip *int
Limit *int
}
func (c *ReturnClause) clauseMarker() {}
// WhereClause represents a WHERE clause.
type WhereClause struct {
Expression Expression
}
func (c *WhereClause) clauseMarker() {}
// SetClause represents a SET clause.
type SetClause struct {
Items []SetItem
}
func (c *SetClause) clauseMarker() {}
// DeleteClause represents a DELETE clause.
type DeleteClause struct {
Variables []string
Detach bool
}
func (c *DeleteClause) clauseMarker() {}
// Pattern represents a graph pattern.
type Pattern struct {
Nodes []NodePattern
Edges []EdgePattern
}
// NodePattern represents a node in a pattern.
type NodePattern struct {
Variable string
Labels []string
Properties map[string]any
}
// EdgePattern represents an edge in a pattern.
type EdgePattern struct {
Variable string
Type string
Direction EdgeDirection
Properties map[string]any
MinHops *int
MaxHops *int
}
// EdgeDirection represents edge direction.
type EdgeDirection int
const (
EdgeBoth EdgeDirection = iota
EdgeOutgoing
EdgeIncoming
)
// Expression represents a Cypher expression.
type Expression interface {
exprMarker()
}
// PropertyAccess represents property access (e.g., n.name).
type PropertyAccess struct {
Variable string
Property string
}
func (e *PropertyAccess) exprMarker() {}
// Comparison represents a comparison expression.
type Comparison struct {
Left Expression
Operator string
Right Expression
}
func (e *Comparison) exprMarker() {}
// Literal represents a literal value.
type Literal struct {
Value any
}
func (e *Literal) exprMarker() {}
// Parameter represents a query parameter ($name).
type Parameter struct {
Name string
}
func (e *Parameter) exprMarker() {}
// FunctionCall represents a function call.
type FunctionCall struct {
Name string
Args []Expression
}
func (e *FunctionCall) exprMarker() {}
// ReturnItem represents an item in a RETURN clause.
type ReturnItem struct {
Expression Expression
Alias string
}
// OrderItem represents an ORDER BY item.
type OrderItem struct {
Expression Expression
Descending bool
}
// SetItem represents a SET operation.
type SetItem struct {
Variable string
Property string
Value Expression
}
// Parse parses a Cypher query string into a Query AST.
func (p *Parser) Parse(cypher string) (*Query, error) {
// Normalize whitespace
cypher = strings.TrimSpace(cypher)
query := &Query{
Clauses: make([]Clause, 0),
Parameters: make(map[string]any),
}
// Tokenize and parse
tokens := tokenize(cypher)
if len(tokens) == 0 {
return nil, fmt.Errorf("empty query")
}
// Simple top-down parser
pos := 0
for pos < len(tokens) {
token := strings.ToUpper(tokens[pos])
switch token {
case "MATCH":
clause, newPos, err := p.parseMatch(tokens, pos)
if err != nil {
return nil, err
}
query.Clauses = append(query.Clauses, clause)
pos = newPos
query.Type = QueryMatch
case "OPTIONAL":
if pos+1 < len(tokens) && strings.EqualFold(tokens[pos+1], "MATCH") {
clause, newPos, err := p.parseMatch(tokens, pos+1)
if err != nil {
return nil, err
}
clause.Optional = true
query.Clauses = append(query.Clauses, clause)
pos = newPos
}
case "CREATE":
clause, newPos, err := p.parseCreate(tokens, pos)
if err != nil {
return nil, err
}
query.Clauses = append(query.Clauses, clause)
pos = newPos
query.Type = QueryCreate
case "RETURN":
clause, newPos, err := p.parseReturn(tokens, pos)
if err != nil {
return nil, err
}
query.Clauses = append(query.Clauses, clause)
pos = newPos
case "WHERE":
clause, newPos, err := p.parseWhere(tokens, pos)
if err != nil {
return nil, err
}
query.Clauses = append(query.Clauses, clause)
pos = newPos
case "SET":
clause, newPos, err := p.parseSet(tokens, pos)
if err != nil {
return nil, err
}
query.Clauses = append(query.Clauses, clause)
pos = newPos
query.Type = QuerySet
case "DELETE", "DETACH":
clause, newPos, err := p.parseDelete(tokens, pos)
if err != nil {
return nil, err
}
query.Clauses = append(query.Clauses, clause)
pos = newPos
query.Type = QueryDelete
default:
pos++
}
}
return query, nil
}
// parseMatch parses a MATCH clause.
func (p *Parser) parseMatch(tokens []string, pos int) (*MatchClause, int, error) {
// Skip MATCH keyword
pos++
clause := &MatchClause{}
// Parse pattern (simplified)
// TODO: Full pattern parsing
return clause, pos, nil
}
// parseCreate parses a CREATE clause.
func (p *Parser) parseCreate(tokens []string, pos int) (*CreateClause, int, error) {
pos++
clause := &CreateClause{}
return clause, pos, nil
}
// parseReturn parses a RETURN clause.
func (p *Parser) parseReturn(tokens []string, pos int) (*ReturnClause, int, error) {
pos++
clause := &ReturnClause{
Items: make([]ReturnItem, 0),
}
return clause, pos, nil
}
// parseWhere parses a WHERE clause.
func (p *Parser) parseWhere(tokens []string, pos int) (*WhereClause, int, error) {
pos++
clause := &WhereClause{}
return clause, pos, nil
}
// parseSet parses a SET clause.
func (p *Parser) parseSet(tokens []string, pos int) (*SetClause, int, error) {
pos++
clause := &SetClause{
Items: make([]SetItem, 0),
}
return clause, pos, nil
}
// parseDelete parses a DELETE clause.
func (p *Parser) parseDelete(tokens []string, pos int) (*DeleteClause, int, error) {
clause := &DeleteClause{}
if strings.EqualFold(tokens[pos], "DETACH") {
clause.Detach = true
pos++
}
pos++ // Skip DELETE
return clause, pos, nil
}
// tokenize splits Cypher into tokens.
func tokenize(cypher string) []string {
// Simple tokenizer - will need improvement
tokens := make([]string, 0)
current := strings.Builder{}
inString := false
stringChar := byte(0)
for i := 0; i < len(cypher); i++ {
c := cypher[i]
if inString {
current.WriteByte(c)
if c == stringChar {
inString = false
tokens = append(tokens, current.String())
current.Reset()
}
continue
}
switch c {
case '"', '\'':
inString = true
stringChar = c
current.WriteByte(c)
case ' ', '\t', '\n', '\r':
if current.Len() > 0 {
tokens = append(tokens, current.String())
current.Reset()
}
case '(', ')', '[', ']', '{', '}', ':', ',', '.', '=', '<', '>', '-', '+', '*', '/':
if current.Len() > 0 {
tokens = append(tokens, current.String())
current.Reset()
}
tokens = append(tokens, string(c))
default:
current.WriteByte(c)
}
}
if current.Len() > 0 {
tokens = append(tokens, current.String())
}
return tokens
}
// Executor executes Cypher queries.
type Executor struct {
parser *Parser
// storage reference will be added
}
// NewExecutor creates a new Cypher executor.
func NewExecutor() *Executor {
return &Executor{
parser: NewParser(),
}
}
// Execute executes a Cypher query.
func (e *Executor) Execute(ctx context.Context, cypher string, params map[string]any) (*Result, error) {
// Parse query
query, err := e.parser.Parse(cypher)
if err != nil {
return nil, fmt.Errorf("parse error: %w", err)
}
// Add parameters
for k, v := range params {
query.Parameters[k] = v
}
// Execute based on query type
result := &Result{
Columns: make([]string, 0),
Rows: make([]map[string]any, 0),
}
// TODO: Implement execution
_ = query
return result, nil
}
// Result holds query results.
type Result struct {
Columns []string
Rows []map[string]any
}
// RowCount returns the number of rows.
func (r *Result) RowCount() int {
return len(r.Rows)
}