Files
zeekay f5aaeb712e Hanzo Graph — the engine, on zapdb, ZAP-native
Derived from Dgraph (github.com/dgraph-io/dgraph) at commit 64804bd2, Apache-2.0.
See NOTICE.

WHAT THIS IS. The graph engine as a Go library: posting lists, the DQL parser,
the type and tokenizer system, the index and schema layers, and the wire types.
39,170 lines of the upstream's 166,337.

THE STORE is github.com/luxfi/zapdb, a Badger fork that kept the package name,
the subpackages (options, pb, y) and the API — Open, OpenManaged, DB, Options —
so the 74 import sites are aliased rather than rewritten.

THE CODEC is ZAP. zapdb serializes with ZAP, not protobuf: its pb package builds
the ZAP types by default and the protobuf ones only under `-tags grpc`, a path
that does not compile (table/builder.go calls pb.Marshal, which that tag
removes). One call site in posting needed converting; every other type here is
Hanzo's own protobuf and is untouched.

NO TRANSPORT. An embedded engine dials nothing, so nothing here imports
google.golang.org/grpc. Removed: 20 transport functions in x, the generated
service stubs for Raft, Zero and Worker (1,657 lines), and the JWT helper —
identity belongs to IAM. The core called none of them.

NOT KEPT: worker, conn, raftwal, the cluster commands, the GraphQL layer and the
test harnesses. `query`, the DQL execution engine, went with them because it
depends on worker; parsing DQL is available here, executing it is not.

The history of the upstream project lives at github.com/dgraph-io/dgraph. This
repository begins at the derivation because three quarters of that history
describes code this fork does not carry.
2026-08-20 21:26:27 -07:00

712 lines
16 KiB
Go

/*
* SPDX-FileCopyrightText: © 2017-2026 Istari Digital, Inc.
* SPDX-License-Identifier: Apache-2.0
*/
// Package dql is responsible for lexing and parsing a GraphQL query/mutation.
package dql
import (
"github.com/hanzoai/graph/lex"
)
const (
leftCurl = '{'
rightCurl = '}'
leftRound = '('
rightRound = ')'
leftSquare = '['
rightSquare = ']'
period = '.'
comma = ','
slash = '/'
equal = '='
quote = '"'
at = '@'
colon = ':'
lsThan = '<'
star = '*'
)
// Constants representing type of different graphql lexed items.
const (
itemText lex.ItemType = 5 + iota // plain text
itemLeftCurl // left curly bracket
itemRightCurl // right curly bracket
itemEqual // equals to symbol
itemName // [9] names
itemOpType // operation type
itemLeftRound // left round bracket
itemRightRound // right round bracket
itemColon // Colon
itemAt // @
itemPeriod // .
itemDollar // $
itemRegex // /
itemMutationOp // mutation operation (set, delete)
itemMutationOpContent // mutation operation content
itemUpsertBlock // mutation upsert block
itemUpsertBlockOp // upsert block op (query, mutate)
itemUpsertBlockOpContent // upsert block operations' content
itemLeftSquare
itemRightSquare
itemComma
itemMathOp
itemStar
)
// lexUpsertBlock lexes the upsert block
func lexUpsertBlock(l *lex.Lexer) lex.StateFn {
l.Mode = lexUpsertBlock
for {
switch r := l.Next(); {
case r == rightCurl:
l.BlockDepth--
l.Emit(itemRightCurl)
if l.BlockDepth == 0 {
return lexTopLevel
}
case r == leftCurl:
l.BlockDepth++
l.Emit(itemLeftCurl)
case isSpace(r) || lex.IsEndOfLine(r):
l.Ignore()
case isNameBegin(r):
return lexNameUpsertOp
case r == '#':
return lexComment
case r == lex.EOF:
return l.Errorf("Unclosed upsert block")
default:
return l.Errorf("Unrecognized character in upsert block: %#U", r)
}
}
}
// lexNameUpsertOp parses the operation names inside upsert block
func lexNameUpsertOp(l *lex.Lexer) lex.StateFn {
// The caller already checked isNameBegin, and absorbed one rune.
l.AcceptRun(isNameSuffix)
word := l.Input[l.Start:l.Pos]
switch word {
case "query":
l.Emit(itemUpsertBlockOp)
return lexBlockContent
case "mutation":
l.Emit(itemUpsertBlockOp)
return lexInsideMutation
case "fragment":
l.Emit(itemUpsertBlockOp)
return lexBlockContent
default:
return l.Errorf("Invalid operation type: %s", word)
}
}
// lexBlockContent lexes and absorbs the text inside a block (covered by braces).
func lexBlockContent(l *lex.Lexer) lex.StateFn {
return lexContent(l, leftCurl, rightCurl, lexUpsertBlock)
}
// lexIfContent lexes the whole of @if directive in a mutation block (covered by small brackets)
func lexIfContent(l *lex.Lexer) lex.StateFn {
if r := l.Next(); r != at {
return l.Errorf("Expected [@], found; [%#U]", r)
}
l.AcceptRun(isNameSuffix)
word := l.Input[l.Start:l.Pos]
if word != "@if" {
return l.Errorf("Expected @if, found [%v]", word)
}
return lexContent(l, '(', ')', lexInsideMutation)
}
func lexContent(l *lex.Lexer, leftRune, rightRune rune, returnTo lex.StateFn) lex.StateFn {
depth := 0
for {
switch l.Next() {
case lex.EOF:
return l.Errorf("Matching brackets not found")
case quote:
if err := l.LexQuotedString(); err != nil {
return l.Errorf("%s", err.Error())
}
case leftRune:
depth++
case rightRune:
depth--
switch {
case depth < 0:
return l.Errorf("Unopened %c found", rightRune)
case depth == 0:
l.Emit(itemUpsertBlockOpContent)
return returnTo
}
}
}
}
func lexInsideMutation(l *lex.Lexer) lex.StateFn {
l.Mode = lexInsideMutation
for {
switch r := l.Next(); {
case r == at:
l.Backup()
return lexIfContent
case r == rightCurl:
l.Depth--
l.Emit(itemRightCurl)
if l.Depth == 0 {
return lexTopLevel
}
case r == leftCurl:
l.Depth++
l.Emit(itemLeftCurl)
if l.Depth >= 2 {
return lexTextMutation
}
case isSpace(r) || lex.IsEndOfLine(r):
l.Ignore()
case isNameBegin(r):
return lexNameMutation
case r == '#':
return lexComment
case r == lex.EOF:
return l.Errorf("Unclosed mutation action")
default:
return l.Errorf("Unrecognized character inside mutation: %#U", r)
}
}
}
func lexInsideSchema(l *lex.Lexer) lex.StateFn {
l.Mode = lexInsideSchema
for {
switch r := l.Next(); {
case r == rightRound:
l.Emit(itemRightRound)
case r == leftRound:
l.Emit(itemLeftRound)
case r == rightCurl:
l.Depth--
l.Emit(itemRightCurl)
if l.Depth == 0 {
return lexTopLevel
}
case r == leftCurl:
l.Depth++
l.Emit(itemLeftCurl)
case r == leftSquare:
l.Emit(itemLeftSquare)
case r == rightSquare:
l.Emit(itemRightSquare)
case isSpace(r) || lex.IsEndOfLine(r):
l.Ignore()
case r == lsThan:
return lexIRIRef
case isNameBegin(r):
return lexArgName
case r == '#':
return lexComment
case r == colon:
l.Emit(itemColon)
case r == comma:
l.Emit(itemComma)
case r == lex.EOF:
return l.Errorf("Unclosed schema action")
default:
return l.Errorf("Unrecognized character inside schema: %#U", r)
}
}
}
func lexFuncOrArg(l *lex.Lexer) lex.StateFn {
l.Mode = lexFuncOrArg
var empty bool
for {
switch r := l.Next(); {
case r == at:
l.Emit(itemAt)
return lexDirectiveOrLangList
case isNameBegin(r) || isNumber(r):
return lexArgName
case r == slash:
// if argument starts with '/' it's a regex, otherwise it's a division
if empty {
return lexRegex(l)
}
fallthrough
case isMathOp(r):
l.Emit(itemMathOp)
case isInequalityOp(r):
if r == equal {
if !isInequalityOp(l.Peek()) {
l.Emit(itemEqual)
continue
}
}
if r == lsThan {
if !isSpace(l.Peek()) && l.Peek() != '=' {
// as long as its not '=' or ' '
return lexIRIRef
}
}
if isInequalityOp(l.Peek()) {
l.Next()
}
l.Emit(itemMathOp)
case r == leftRound:
l.Emit(itemLeftRound)
l.ArgDepth++
case r == rightRound:
if l.ArgDepth == 0 {
return l.Errorf("Unexpected right round bracket")
}
l.ArgDepth--
l.Emit(itemRightRound)
if empty {
return l.Errorf("Empty Argument")
}
if l.ArgDepth == 0 {
return lexQuery // Filter directive is done.
}
case r == lex.EOF:
return l.Errorf("Unclosed Brackets")
case isSpace(r) || lex.IsEndOfLine(r):
l.Ignore()
case r == comma:
if empty {
return l.Errorf("Consecutive commas not allowed.")
}
empty = true
l.Emit(itemComma)
case isDollar(r):
l.Emit(itemDollar)
case r == colon:
l.Emit(itemColon)
case r == quote:
{
empty = false
if err := l.LexQuotedString(); err != nil {
return l.Errorf("%s", err.Error())
}
l.Emit(itemName)
}
case isEndLiteral(r):
{
empty = false
l.AcceptUntil(isEndLiteral) // This call will backup the ending ".
l.Next() // Consume the " .
l.Emit(itemName)
}
case r == leftSquare:
l.Emit(itemLeftSquare)
case r == rightSquare:
l.Emit(itemRightSquare)
case r == leftCurl:
empty = false
l.Emit(itemLeftCurl)
// Design decision: Emit brace tokens without affecting ArgDepth tracking.
// The parser validates whether braces are legal in context.
// Trade-off: Queries with multiple syntax errors (e.g., missing ')' AND stray '}')
// will report structural errors (Unclosed Brackets) rather than character-specific
// errors. This is acceptable as the query is still rejected with a clear error.
case r == rightCurl:
l.Emit(itemRightCurl)
// Don't decrement ArgDepth for braces; let parser validate context.
// See leftCurl case above for full rationale.
case r == '#':
return lexComment
case r == '.':
l.Emit(itemPeriod)
default:
return l.Errorf("Unrecognized character inside a func: %#U", r)
}
}
}
func lexTopLevel(l *lex.Lexer) lex.StateFn {
// TODO(Aman): Find a way to identify different blocks in future. We only have
// Upsert block right now. BlockDepth tells us nesting of blocks. Currently, only
// the Upsert block has nested mutation/query/fragment blocks.
if l.BlockDepth != 0 {
return lexUpsertBlock
}
l.Mode = lexTopLevel
Loop:
for {
switch r := l.Next(); {
case r == leftCurl:
l.Depth++ // one level down.
l.Emit(itemLeftCurl)
return lexIdentifyMutationOrQuery
case r == rightCurl:
return l.Errorf("Too many right curl")
case r == lex.EOF:
break Loop
case r == '#':
return lexComment
case r == leftRound:
l.Backup()
l.Emit(itemText)
l.Next()
l.Emit(itemLeftRound)
l.ArgDepth++
return lexQuery
case isSpace(r) || lex.IsEndOfLine(r):
l.Ignore()
case isNameBegin(r):
l.Backup()
return lexOperationType
}
}
if l.Pos > l.Start {
l.Emit(itemText)
}
l.Emit(lex.ItemEOF)
return nil
}
func lexIdentifyMutationOrQuery(l *lex.Lexer) lex.StateFn {
l.Mode = lexIdentifyMutationOrQuery
for {
switch r := l.Next(); {
case isSpace(r) || lex.IsEndOfLine(r):
l.Ignore()
case isNameBegin(r):
l.AcceptRun(isNameSuffix)
op := l.Input[l.Start:l.Pos]
// If it is a mutation
if op == "set" || op == "delete" || op == "del" {
l.Emit(itemMutationOp)
return lexInsideMutation
}
// else it is a query
l.Emit(itemName)
return lexQuery
case r == lsThan:
l.Backup()
return lexQuery
case r == '#':
return lexComment
case r == lex.EOF:
return l.Errorf("Invalid DQL")
default:
return l.Errorf("Unexpected character while lexing DQL: %#U", r)
}
}
}
// lexQuery lexes the input string and calls other lex functions.
func lexQuery(l *lex.Lexer) lex.StateFn {
l.Mode = lexQuery
for {
switch r := l.Next(); {
case r == period:
l.Emit(itemPeriod)
case r == rightCurl:
l.Depth--
l.Emit(itemRightCurl)
if l.Depth == 0 {
return lexTopLevel
}
case r == leftCurl:
l.Depth++
l.Emit(itemLeftCurl)
case r == lex.EOF:
return l.Errorf("Unclosed action")
case isSpace(r) || lex.IsEndOfLine(r):
l.Ignore()
case r == comma:
l.Emit(itemComma)
case isNameBegin(r):
return lexName
case r == '#':
return lexComment
case r == '-':
l.Emit(itemMathOp)
case r == leftRound:
l.Emit(itemLeftRound)
l.AcceptRun(isSpace)
l.Ignore()
l.ArgDepth++
return lexFuncOrArg
case r == colon:
l.Emit(itemColon)
case r == at:
l.Emit(itemAt)
return lexDirectiveOrLangList
case r == lsThan:
return lexIRIRef
case r == star:
l.Emit(itemStar)
default:
return l.Errorf("Unrecognized character in lexText: %#U", r)
}
}
}
func lexIRIRef(l *lex.Lexer) lex.StateFn {
if err := lex.IRIRef(l, itemName); err != nil {
return l.Errorf("%s", err.Error())
}
return l.Mode
}
// lexDirectiveOrLangList is called right after we see a @.
func lexDirectiveOrLangList(l *lex.Lexer) lex.StateFn {
r := l.Next()
// Check first character.
if !isNameBegin(r) && r != period && r != star {
return l.Errorf("Unrecognized character in lexDirective: %#U", r)
}
l.Backup()
for {
r := l.Next()
if r == period {
l.Emit(itemName)
return l.Mode
}
if isLangOrDirective(r) {
continue
}
l.Backup()
l.Emit(itemName)
break
}
return l.Mode
}
func lexName(l *lex.Lexer) lex.StateFn {
l.AcceptRun(isNameSuffix)
l.Emit(itemName)
return l.Mode
}
// lexComment lexes a comment text.
func lexComment(l *lex.Lexer) lex.StateFn {
for {
r := l.Next()
if lex.IsEndOfLine(r) {
l.Ignore()
return l.Mode
}
if r == lex.EOF {
break
}
}
l.Ignore()
l.Emit(lex.ItemEOF)
return l.Mode
}
// lexNameMutation lexes the itemMutationOp, which could be set or delete.
func lexNameMutation(l *lex.Lexer) lex.StateFn {
for {
// The caller already checked isNameBegin, and absorbed one rune.
r := l.Next()
if isNameSuffix(r) {
continue
}
l.Backup()
l.Emit(itemMutationOp)
break
}
return l.Mode
}
// lexTextMutation lexes and absorbs the text inside a mutation operation block.
func lexTextMutation(l *lex.Lexer) lex.StateFn {
for {
r := l.Next()
if r == lex.EOF {
return l.Errorf("Unclosed mutation text")
}
if r == quote {
return lexMutationValue
}
if r == leftCurl {
return l.Errorf("Invalid character '{' inside mutation text")
}
if r != rightCurl {
// Absorb everything until we find '}'.
continue
}
l.Backup()
l.Emit(itemMutationOpContent)
break
}
return lexInsideMutation
}
// This function is used to absorb the object value.
func lexMutationValue(l *lex.Lexer) lex.StateFn {
LOOP:
for {
r := l.Next()
switch r {
case lex.EOF:
return l.Errorf("Unclosed mutation value")
case quote:
break LOOP
case '\\':
l.Next() // skip one.
}
}
return lexTextMutation
}
func lexRegex(l *lex.Lexer) lex.StateFn {
LOOP:
for {
r := l.Next()
switch r {
case lex.EOF:
return l.Errorf("Unclosed regexp")
case '\\':
l.Next()
case '/':
break LOOP
}
}
l.AcceptRun(isRegexFlag)
l.Emit(itemRegex)
return l.Mode
}
// lexOperationType lexes a query or mutation or schema operation type.
func lexOperationType(l *lex.Lexer) lex.StateFn {
l.AcceptRun(isNameSuffix)
// l.Pos would be index of the end of operation type + 1.
word := l.Input[l.Start:l.Pos]
switch word {
case "mutation":
l.Emit(itemOpType)
return lexInsideMutation
case "fragment":
l.Emit(itemOpType)
return lexQuery
case "query":
l.Emit(itemOpType)
return lexQuery
case "schema":
l.Emit(itemOpType)
return lexInsideSchema
case "upsert":
l.Emit(itemUpsertBlock)
return lexUpsertBlock
default:
return l.Errorf("Invalid operation type: %s", word)
}
}
// lexArgName lexes and emits the name part of an argument.
func lexArgName(l *lex.Lexer) lex.StateFn {
l.AcceptRun(isNameSuffix)
l.Emit(itemName)
return l.Mode
}
// isDollar returns true if the rune is a Dollar($).
func isDollar(r rune) bool {
return r == '$' || r == '\u0024'
}
// isSpace returns true if the rune is a tab or space.
func isSpace(r rune) bool {
return r == '\u0009' || r == '\u0020'
}
// isEndLiteral returns true if rune is quotation mark.
func isEndLiteral(r rune) bool {
return r == '"' || r == '\u000d' || r == '\u000a'
}
func isLangOrDirective(r rune) bool {
if isNameBegin(r) {
return true
}
if r == '-' {
return true
}
if r >= '0' && r <= '9' {
return true
}
if r == '*' {
return true
}
return false
}
// isNameBegin returns true if the rune is an alphabet or an '_' or '~'.
func isNameBegin(r rune) bool {
switch {
case r >= 'a' && r <= 'z':
return true
case r >= 'A' && r <= 'Z':
return true
case r == '_':
return true
case r == '~':
return true
default:
return false
}
}
func isMathOp(r rune) bool {
switch r {
case '+', '-', '*', '/', '%':
return true
default:
return false
}
}
func isInequalityOp(r rune) bool {
switch r {
case '<', '>', '=', '!':
return true
default:
return false
}
}
func isNumber(r rune) bool {
switch {
case (r >= '0' && r <= '9'):
return true
default:
return false
}
}
func isNameSuffix(r rune) bool {
if isMathOp(r) {
return false
}
if isNameBegin(r) || isNumber(r) {
return true
}
if r == '.' /*|| r == '!'*/ { // Use by freebase.
return true
}
return false
}
func isRegexFlag(r rune) bool {
switch {
case r >= 'a' && r <= 'z':
return true
case r >= 'A' && r <= 'Z':
return true
default:
return false
}
}