mirror of
https://github.com/MontFerret/ferret.git
synced 2026-06-20 01:17:53 +02:00
chore: update FqlParser to include queryOptionsOpt in grammar definition (#951)
This commit is contained in:
@@ -19,6 +19,7 @@ github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfV
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
|
||||
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
||||
github.com/maxatome/go-testdeep v1.14.0 h1:rRlLv1+kI8eOI3OaBXZwb3O7xY3exRzdW5QyX48g9wI=
|
||||
github.com/maxatome/go-testdeep v1.14.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
@@ -34,9 +35,11 @@ github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQ
|
||||
github.com/segmentio/encoding v0.3.4 h1:WM4IBnxH8B9TakiM2QD5LyNl9JSndh88QbHqVC+Pauc=
|
||||
github.com/segmentio/encoding v0.3.4/go.mod h1:n0JeuIqEQrQoPDGsjo8UNd1iA0U8d8+oHAA4E3G3OxM=
|
||||
github.com/smarty/assertions v1.16.0 h1:EvHNkdRA4QHMrn75NZSoUQ/mAUXAYWfatfB01yTCzfY=
|
||||
github.com/smarty/assertions v1.16.0/go.mod h1:duaaFdCS0K9dnoM50iyek/eYINOZ64gbh1Xlf6LG7AI=
|
||||
github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY=
|
||||
github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
|
||||
@@ -44,5 +47,7 @@ github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV
|
||||
github.com/wI2L/jettison v0.7.4 h1:ptjriu75R/k5RAZO0DJzy2t55f7g+dPiBxBY38icaKg=
|
||||
github.com/wI2L/jettison v0.7.4/go.mod h1:O+F+T7X7ZN6kTsd167Qk4aZMC8jNrH48SMedNmkfPb0=
|
||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
|
||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
|
||||
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
|
||||
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
||||
@@ -95,16 +95,19 @@ func (c *exprQueryCompiler) emitQueryEnvelope(ctx fql.IQueryExpressionContext, s
|
||||
queryReg := c.ctx.Function.Registers.Allocate()
|
||||
|
||||
c.ctx.Program.Emitter.WithSpan(span, func() {
|
||||
c.ctx.Program.Emitter.EmitArray(queryReg, 3)
|
||||
c.ctx.Program.Emitter.EmitArray(queryReg, 4)
|
||||
})
|
||||
|
||||
kind := c.compileQueryKindOperand(ctx)
|
||||
c.emitQueryEnvelopeOperand(span, queryReg, kind)
|
||||
|
||||
payload := c.compileQueryPayloadOperand(ctx.QueryPayload())
|
||||
c.emitQueryEnvelopeOperand(span, queryReg, payload)
|
||||
expression := c.compileQueryExpressionOperand(ctx.QueryPayload())
|
||||
c.emitQueryEnvelopeOperand(span, queryReg, expression)
|
||||
|
||||
options := c.compileQueryOptionsOperand(ctx.QueryWithOpt())
|
||||
params := c.compileQueryParamsOperand(ctx.QueryWithOpt())
|
||||
c.emitQueryEnvelopeOperand(span, queryReg, params)
|
||||
|
||||
options := c.compileQueryOptionsOperand(ctx.QueryOptionsOpt())
|
||||
c.emitQueryEnvelopeOperand(span, queryReg, options)
|
||||
|
||||
return queryReg
|
||||
@@ -119,7 +122,7 @@ func (c *exprQueryCompiler) compileQueryKindOperand(ctx fql.IQueryExpressionCont
|
||||
return c.facts.LoadConstant(runtime.NewString(kind))
|
||||
}
|
||||
|
||||
func (c *exprQueryCompiler) compileQueryPayloadOperand(ctx fql.IQueryPayloadContext) bytecode.Operand {
|
||||
func (c *exprQueryCompiler) compileQueryExpressionOperand(ctx fql.IQueryPayloadContext) bytecode.Operand {
|
||||
if ctx == nil {
|
||||
return c.facts.LoadConstant(runtime.EmptyString)
|
||||
}
|
||||
@@ -143,7 +146,15 @@ func (c *exprQueryCompiler) compileQueryPayloadOperand(ctx fql.IQueryPayloadCont
|
||||
return c.facts.LoadConstant(runtime.EmptyString)
|
||||
}
|
||||
|
||||
func (c *exprQueryCompiler) compileQueryOptionsOperand(ctx fql.IQueryWithOptContext) bytecode.Operand {
|
||||
func (c *exprQueryCompiler) compileQueryParamsOperand(ctx fql.IQueryWithOptContext) bytecode.Operand {
|
||||
if ctx == nil || ctx.Expression() == nil {
|
||||
return c.facts.LoadConstant(runtime.None)
|
||||
}
|
||||
|
||||
return c.callbacks.compileExpr(ctx.Expression())
|
||||
}
|
||||
|
||||
func (c *exprQueryCompiler) compileQueryOptionsOperand(ctx fql.IQueryOptionsOptContext) bytecode.Operand {
|
||||
if ctx == nil || ctx.Expression() == nil {
|
||||
return c.facts.LoadConstant(runtime.None)
|
||||
}
|
||||
@@ -181,7 +192,7 @@ func (c *exprQueryCompiler) compileQueryLiteral(ctx fql.IQueryLiteralContext) by
|
||||
span := diagnostics.SpanFromRuleContext(ctx)
|
||||
|
||||
c.ctx.Program.Emitter.WithSpan(span, func() {
|
||||
c.ctx.Program.Emitter.EmitArray(dst, 3)
|
||||
c.ctx.Program.Emitter.EmitArray(dst, 4)
|
||||
})
|
||||
|
||||
kindReg := c.facts.LoadConstant(runtime.NewString(kind))
|
||||
@@ -190,17 +201,17 @@ func (c *exprQueryCompiler) compileQueryLiteral(ctx fql.IQueryLiteralContext) by
|
||||
c.ctx.Program.Emitter.EmitArrayPush(dst, kindReg)
|
||||
})
|
||||
|
||||
payloadReg := c.facts.LoadConstant(runtime.EmptyString)
|
||||
expressionReg := c.facts.LoadConstant(runtime.EmptyString)
|
||||
if str := ctx.StringLiteral(); str != nil {
|
||||
if val, ok := parseStringLiteralConst(str); ok {
|
||||
payloadReg = c.facts.LoadConstant(val)
|
||||
expressionReg = c.facts.LoadConstant(val)
|
||||
} else {
|
||||
payloadReg = c.literals.CompileStringLiteral(str)
|
||||
expressionReg = c.literals.CompileStringLiteral(str)
|
||||
}
|
||||
}
|
||||
|
||||
c.ctx.Program.Emitter.WithSpan(span, func() {
|
||||
c.ctx.Program.Emitter.EmitArrayPush(dst, payloadReg)
|
||||
c.ctx.Program.Emitter.EmitArrayPush(dst, expressionReg)
|
||||
})
|
||||
|
||||
params := ctx.Expression()
|
||||
@@ -212,8 +223,11 @@ func (c *exprQueryCompiler) compileQueryLiteral(ctx fql.IQueryLiteralContext) by
|
||||
paramsReg = c.callbacks.compileExpr(params)
|
||||
}
|
||||
|
||||
optionsReg := c.facts.LoadConstant(runtime.None)
|
||||
|
||||
c.ctx.Program.Emitter.WithSpan(span, func() {
|
||||
c.ctx.Program.Emitter.EmitArrayPush(dst, paramsReg)
|
||||
c.ctx.Program.Emitter.EmitArrayPush(dst, optionsReg)
|
||||
})
|
||||
|
||||
if dst.IsRegister() {
|
||||
|
||||
@@ -569,7 +569,7 @@ func (f *expressionFormatter) formatQueryExpression(ctx *fql.QueryExpressionCont
|
||||
return
|
||||
}
|
||||
|
||||
if ctx.QueryWithOpt() == nil || f.p.forceSingleLine {
|
||||
if (ctx.QueryWithOpt() == nil && ctx.QueryOptionsOpt() == nil) || f.p.forceSingleLine {
|
||||
f.formatQueryExpressionWith(f.p, ctx, true)
|
||||
return
|
||||
}
|
||||
@@ -623,25 +623,36 @@ func (f *expressionFormatter) formatQueryExpressionWith(p *printer, ctx *fql.Que
|
||||
|
||||
if with := ctx.QueryWithOpt(); with != nil {
|
||||
if expr := with.Expression(); expr != nil {
|
||||
if inline {
|
||||
p.space()
|
||||
f.writeKeywordWith(p, keywordWith)
|
||||
p.space()
|
||||
f.formatExpressionWith(p, expr.(*fql.ExpressionContext))
|
||||
} else {
|
||||
p.newline()
|
||||
p.withIndent(func() {
|
||||
f.writeKeywordWith(p, keywordWith)
|
||||
p.space()
|
||||
f.formatExpressionWith(p, expr.(*fql.ExpressionContext))
|
||||
})
|
||||
}
|
||||
f.formatQueryClauseWith(p, keywordWith, expr, inline)
|
||||
}
|
||||
}
|
||||
|
||||
if options := ctx.QueryOptionsOpt(); options != nil {
|
||||
if expr := options.Expression(); expr != nil {
|
||||
f.formatQueryClauseWith(p, keywordOptions, expr, inline)
|
||||
}
|
||||
}
|
||||
|
||||
f.formatRecoveryTailsWith(p, ctx.RecoveryTails())
|
||||
}
|
||||
|
||||
func (f *expressionFormatter) formatQueryClauseWith(p *printer, keyword string, expr fql.IExpressionContext, inline bool) {
|
||||
if inline {
|
||||
p.space()
|
||||
f.writeKeywordWith(p, keyword)
|
||||
p.space()
|
||||
f.formatExpressionWith(p, expr.(*fql.ExpressionContext))
|
||||
return
|
||||
}
|
||||
|
||||
p.newline()
|
||||
p.withIndent(func() {
|
||||
f.writeKeywordWith(p, keyword)
|
||||
p.space()
|
||||
f.formatExpressionWith(p, expr.(*fql.ExpressionContext))
|
||||
})
|
||||
}
|
||||
|
||||
func (f *expressionFormatter) writeQueryModifierWith(p *printer, modifier fql.IQueryModifierContext) {
|
||||
if modifier == nil {
|
||||
return
|
||||
|
||||
@@ -113,6 +113,20 @@ func TestExpressionFormatter_QueryExpressionInline(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpressionFormatter_QueryExpressionOptionsInline(t *testing.T) {
|
||||
input := "RETURN QUERY `.items` IN doc USING css OPTIONS { timeout: 5000 }"
|
||||
program := parseProgram(t, input)
|
||||
expr := mustFirst[*fql.ExpressionContext](t, program)
|
||||
|
||||
var buf bytes.Buffer
|
||||
e := newEngine(source.NewAnonymous(input), &buf, DefaultOptions())
|
||||
|
||||
e.expression.formatExpression(expr)
|
||||
if got := buf.String(); got != "QUERY `.items` IN doc USING css OPTIONS { timeout: 5000 }" {
|
||||
t.Fatalf("unexpected query expression formatting: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpressionFormatter_QueryExpressionParamPayload(t *testing.T) {
|
||||
input := "RETURN QUERY @q IN doc USING css"
|
||||
program := parseProgram(t, input)
|
||||
@@ -229,7 +243,7 @@ func TestExpressionFormatter_ParenthesizedErrorPolicyTail(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestExpressionFormatter_QueryExpressionErrorPolicyTail(t *testing.T) {
|
||||
input := "RETURN QUERY `.items` IN doc USING css ON ERROR RETURN NONE"
|
||||
input := "RETURN QUERY `.items` IN doc USING css OPTIONS { timeout: 5000 } ON ERROR RETURN NONE"
|
||||
program := parseProgram(t, input)
|
||||
expr := mustFirst[*fql.ExpressionContext](t, program)
|
||||
|
||||
@@ -237,13 +251,13 @@ func TestExpressionFormatter_QueryExpressionErrorPolicyTail(t *testing.T) {
|
||||
e := newEngine(source.NewAnonymous(input), &buf, DefaultOptions())
|
||||
|
||||
e.expression.formatExpression(expr)
|
||||
if got := buf.String(); got != "QUERY `.items` IN doc USING css ON ERROR RETURN NONE" {
|
||||
if got := buf.String(); got != "QUERY `.items` IN doc USING css OPTIONS { timeout: 5000 } ON ERROR RETURN NONE" {
|
||||
t.Fatalf("unexpected query error policy formatting: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpressionFormatter_QueryExpressionOneModifierWithMultiline(t *testing.T) {
|
||||
input := "RETURN QUERY ONE `.items` IN doc USING css WITH { limit: 10, timeout: 5 }"
|
||||
input := "RETURN QUERY ONE `.items` IN doc USING css WITH { limit: 10, timeout: 5 } OPTIONS { retry: 2, delay: 50 }"
|
||||
program := parseProgram(t, input)
|
||||
expr := mustFirst[*fql.ExpressionContext](t, program)
|
||||
|
||||
@@ -253,7 +267,7 @@ func TestExpressionFormatter_QueryExpressionOneModifierWithMultiline(t *testing.
|
||||
e := newEngine(source.NewAnonymous(input), &buf, opts)
|
||||
|
||||
e.expression.formatExpression(expr)
|
||||
if got := buf.String(); got != "QUERY ONE `.items` IN doc USING css\n WITH {\n limit: 10,\n timeout: 5\n }" {
|
||||
if got := buf.String(); got != "QUERY ONE `.items` IN doc USING css\n WITH {\n limit: 10,\n timeout: 5\n }\n OPTIONS {\n retry: 2,\n delay: 50\n }" {
|
||||
t.Fatalf("unexpected query expression formatting: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1102,8 +1102,8 @@ implicitMemberExpressionStart
|
||||
;
|
||||
|
||||
queryExpression
|
||||
: Query queryModifier queryPayload In expression Using dialect=Identifier queryWithOpt? (recoveryTails)?
|
||||
| Query {!p.isQueryModifierAhead()}? queryPayload In expression Using dialect=Identifier queryWithOpt? (recoveryTails)?
|
||||
: Query queryModifier queryPayload In expression Using dialect=Identifier queryWithOpt? queryOptionsOpt? (recoveryTails)?
|
||||
| Query {!p.isQueryModifierAhead()}? queryPayload In expression Using dialect=Identifier queryWithOpt? queryOptionsOpt? (recoveryTails)?
|
||||
;
|
||||
|
||||
queryModifier
|
||||
@@ -1122,6 +1122,10 @@ queryWithOpt
|
||||
: With expression
|
||||
;
|
||||
|
||||
queryOptionsOpt
|
||||
: Options expression
|
||||
;
|
||||
|
||||
queryLiteral
|
||||
: Identifier (stringLiteral (OpenParen expression CloseParen)?)?
|
||||
;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package diagnostics
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/MontFerret/ferret/v2/pkg/diagnostics"
|
||||
@@ -17,14 +18,46 @@ func matchQueryErrors(src *source.Source, err *diagnostics.Diagnostic, offending
|
||||
return false
|
||||
}
|
||||
|
||||
if !hasPrevToken(offending, "QUERY", 10) && !is(offending, "QUERY") && !hasPrevToken(offending, "USING", 10) {
|
||||
if with := outOfOrderQueryWithNode(src, offending); with != nil {
|
||||
span := spanFromTokenSafe(with.Token(), src)
|
||||
err.Message = "WITH must appear before OPTIONS in QUERY"
|
||||
err.Hint = "Move the WITH clause before OPTIONS."
|
||||
err.Spans = []diagnostics.ErrorSpan{
|
||||
diagnostics.NewMainErrorSpan(span, "out-of-order WITH clause"),
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
if !hasPrevToken(offending, "QUERY", 64) && !is(offending, "QUERY") && !hasPrevToken(offending, "USING", 64) && !sourceHasQueryBefore(src, offending) {
|
||||
return false
|
||||
}
|
||||
|
||||
if prev := offending.Prev(); prev != nil && is(prev, "WITH") {
|
||||
if is(offending, "WITH") && (hasTokenBefore(offending, "QUERY", "OPTIONS", 64) || sourceHasQueryOptionsBefore(src, offending)) {
|
||||
span := spanFromTokenSafe(offending.Token(), src)
|
||||
err.Message = "WITH must appear before OPTIONS in QUERY"
|
||||
err.Hint = "Move the WITH clause before OPTIONS."
|
||||
err.Spans = []diagnostics.ErrorSpan{
|
||||
diagnostics.NewMainErrorSpan(span, "out-of-order WITH clause"),
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
if (is(offending, "WITH") || is(offending, "OPTIONS")) && !hasPrevToken(offending, "USING", 2) {
|
||||
clause := offending.GetText()
|
||||
span := spanFromTokenSafe(offending.Token(), src)
|
||||
err.Message = fmt.Sprintf("Expected query %s expression after %s", queryClauseValueName(clause), clause)
|
||||
err.Hint = queryClauseHint(clause)
|
||||
err.Spans = []diagnostics.ErrorSpan{
|
||||
diagnostics.NewMainErrorSpan(span, "missing expression"),
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
if prev := offending.Prev(); prev != nil && (is(prev, "WITH") || is(prev, "OPTIONS")) {
|
||||
span := spanFromTokenSafe(prev.Token(), src)
|
||||
err.Message = "Expected options expression after WITH"
|
||||
err.Hint = "Provide an options expression, e.g. WITH { limit: 10 }."
|
||||
clause := prev.GetText()
|
||||
err.Message = fmt.Sprintf("Expected query %s expression after %s", queryClauseValueName(clause), clause)
|
||||
err.Hint = queryClauseHint(clause)
|
||||
err.Spans = []diagnostics.ErrorSpan{
|
||||
diagnostics.NewMainErrorSpan(span, "missing expression"),
|
||||
}
|
||||
@@ -56,7 +89,7 @@ func matchQueryErrors(src *source.Source, err *diagnostics.Diagnostic, offending
|
||||
return true
|
||||
}
|
||||
|
||||
if (is(offending, "USING") || is(offending, "WITH") || isEOF(offending)) && hasPrevToken(offending, "IN", 2) {
|
||||
if (is(offending, "USING") || is(offending, "WITH") || is(offending, "OPTIONS") || isEOF(offending)) && hasPrevToken(offending, "IN", 2) {
|
||||
span := spanFromTokenSafe(offending.Token(), src)
|
||||
err.Message = "Expected expression after IN"
|
||||
err.Hint = "Provide a source expression, e.g. QUERY `.items` IN doc USING css."
|
||||
@@ -106,7 +139,7 @@ func matchQueryErrors(src *source.Source, err *diagnostics.Diagnostic, offending
|
||||
diagnostics.NewMainErrorSpan(span, "invalid dialect"),
|
||||
}
|
||||
return true
|
||||
} else if next := offending.Next(); next == nil || isEOF(next) || is(next, "WITH") {
|
||||
} else if next := offending.Next(); next == nil || isEOF(next) || is(next, "WITH") || is(next, "OPTIONS") {
|
||||
span := spanFromTokenSafe(offending.Token(), src)
|
||||
err.Message = "Expected dialect identifier after USING"
|
||||
err.Hint = "Provide a dialect identifier, e.g. USING css."
|
||||
@@ -117,7 +150,7 @@ func matchQueryErrors(src *source.Source, err *diagnostics.Diagnostic, offending
|
||||
}
|
||||
}
|
||||
|
||||
if is(offending, "WITH") || isEOF(offending) {
|
||||
if is(offending, "WITH") || is(offending, "OPTIONS") || isEOF(offending) {
|
||||
span := spanFromTokenSafe(offending.Token(), src)
|
||||
err.Message = "Expected dialect identifier after USING"
|
||||
err.Hint = "Provide a dialect identifier, e.g. USING css."
|
||||
@@ -151,6 +184,87 @@ func matchQueryErrors(src *source.Source, err *diagnostics.Diagnostic, offending
|
||||
return false
|
||||
}
|
||||
|
||||
func queryClauseHint(clause string) string {
|
||||
switch clause {
|
||||
case "WITH":
|
||||
return "Provide a params expression, e.g. WITH { params: [1] }."
|
||||
case "OPTIONS":
|
||||
return "Provide an options expression, e.g. OPTIONS { timeout: 5000 }."
|
||||
default:
|
||||
return "Provide a query clause expression."
|
||||
}
|
||||
}
|
||||
|
||||
func queryClauseValueName(clause string) string {
|
||||
switch clause {
|
||||
case "WITH":
|
||||
return "params"
|
||||
case "OPTIONS":
|
||||
return "options"
|
||||
default:
|
||||
return "clause"
|
||||
}
|
||||
}
|
||||
|
||||
func sourceHasQueryBefore(src *source.Source, offending *TokenNode) bool {
|
||||
prefix := sourcePrefixBefore(src, offending)
|
||||
query := strings.LastIndex(prefix, "QUERY")
|
||||
using := strings.LastIndex(prefix, "USING")
|
||||
dispatch := strings.LastIndex(prefix, "DISPATCH")
|
||||
|
||||
return query >= 0 && using > query && dispatch < query
|
||||
}
|
||||
|
||||
func sourceHasQueryOptionsBefore(src *source.Source, offending *TokenNode) bool {
|
||||
prefix := sourcePrefixBefore(src, offending)
|
||||
query := strings.LastIndex(prefix, "QUERY")
|
||||
options := strings.LastIndex(prefix, "OPTIONS")
|
||||
|
||||
return query >= 0 && options > query
|
||||
}
|
||||
|
||||
func sourceHasOutOfOrderQueryWith(src *source.Source) bool {
|
||||
if src == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
content := strings.ToUpper(src.Content())
|
||||
query := strings.LastIndex(content, "QUERY")
|
||||
options := strings.LastIndex(content, "OPTIONS")
|
||||
with := strings.LastIndex(content, "WITH")
|
||||
dispatch := strings.LastIndex(content, "DISPATCH")
|
||||
|
||||
return query >= 0 && options > query && with > options && dispatch < query
|
||||
}
|
||||
|
||||
func outOfOrderQueryWithNode(src *source.Source, offending *TokenNode) *TokenNode {
|
||||
if !sourceHasOutOfOrderQueryWith(src) {
|
||||
return nil
|
||||
}
|
||||
if is(offending, "WITH") {
|
||||
return offending
|
||||
}
|
||||
if next := offending.Next(); is(next, "WITH") {
|
||||
return next
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func sourcePrefixBefore(src *source.Source, offending *TokenNode) string {
|
||||
if src == nil || offending == nil || offending.Token() == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
content := []rune(src.Content())
|
||||
end := offending.Token().GetStart()
|
||||
if end < 0 || end > len(content) {
|
||||
end = len(content)
|
||||
}
|
||||
|
||||
return strings.ToUpper(string(content[:end]))
|
||||
}
|
||||
|
||||
func isMissingQueryLiteral(msg string, offending *TokenNode) bool {
|
||||
if offending == nil {
|
||||
return false
|
||||
|
||||
File diff suppressed because one or more lines are too long
+1982
-1807
File diff suppressed because it is too large
Load Diff
@@ -978,6 +978,12 @@ func (s *BaseFqlParserListener) EnterQueryWithOpt(ctx *QueryWithOptContext) {}
|
||||
// ExitQueryWithOpt is called when production queryWithOpt is exited.
|
||||
func (s *BaseFqlParserListener) ExitQueryWithOpt(ctx *QueryWithOptContext) {}
|
||||
|
||||
// EnterQueryOptionsOpt is called when production queryOptionsOpt is entered.
|
||||
func (s *BaseFqlParserListener) EnterQueryOptionsOpt(ctx *QueryOptionsOptContext) {}
|
||||
|
||||
// ExitQueryOptionsOpt is called when production queryOptionsOpt is exited.
|
||||
func (s *BaseFqlParserListener) ExitQueryOptionsOpt(ctx *QueryOptionsOptContext) {}
|
||||
|
||||
// EnterQueryLiteral is called when production queryLiteral is entered.
|
||||
func (s *BaseFqlParserListener) EnterQueryLiteral(ctx *QueryLiteralContext) {}
|
||||
|
||||
|
||||
@@ -631,6 +631,10 @@ func (v *BaseFqlParserVisitor) VisitQueryWithOpt(ctx *QueryWithOptContext) inter
|
||||
return v.VisitChildren(ctx)
|
||||
}
|
||||
|
||||
func (v *BaseFqlParserVisitor) VisitQueryOptionsOpt(ctx *QueryOptionsOptContext) interface{} {
|
||||
return v.VisitChildren(ctx)
|
||||
}
|
||||
|
||||
func (v *BaseFqlParserVisitor) VisitQueryLiteral(ctx *QueryLiteralContext) interface{} {
|
||||
return v.VisitChildren(ctx)
|
||||
}
|
||||
|
||||
@@ -475,6 +475,9 @@ type FqlParserListener interface {
|
||||
// EnterQueryWithOpt is called when entering the queryWithOpt production.
|
||||
EnterQueryWithOpt(c *QueryWithOptContext)
|
||||
|
||||
// EnterQueryOptionsOpt is called when entering the queryOptionsOpt production.
|
||||
EnterQueryOptionsOpt(c *QueryOptionsOptContext)
|
||||
|
||||
// EnterQueryLiteral is called when entering the queryLiteral production.
|
||||
EnterQueryLiteral(c *QueryLiteralContext)
|
||||
|
||||
@@ -979,6 +982,9 @@ type FqlParserListener interface {
|
||||
// ExitQueryWithOpt is called when exiting the queryWithOpt production.
|
||||
ExitQueryWithOpt(c *QueryWithOptContext)
|
||||
|
||||
// ExitQueryOptionsOpt is called when exiting the queryOptionsOpt production.
|
||||
ExitQueryOptionsOpt(c *QueryOptionsOptContext)
|
||||
|
||||
// ExitQueryLiteral is called when exiting the queryLiteral production.
|
||||
ExitQueryLiteral(c *QueryLiteralContext)
|
||||
|
||||
|
||||
@@ -475,6 +475,9 @@ type FqlParserVisitor interface {
|
||||
// Visit a parse tree produced by FqlParser#queryWithOpt.
|
||||
VisitQueryWithOpt(ctx *QueryWithOptContext) interface{}
|
||||
|
||||
// Visit a parse tree produced by FqlParser#queryOptionsOpt.
|
||||
VisitQueryOptionsOpt(ctx *QueryOptionsOptContext) interface{}
|
||||
|
||||
// Visit a parse tree produced by FqlParser#queryLiteral.
|
||||
VisitQueryLiteral(ctx *QueryLiteralContext) interface{}
|
||||
|
||||
|
||||
@@ -5,11 +5,14 @@ import (
|
||||
)
|
||||
|
||||
type (
|
||||
// Query represents a query literal used by the operator index.
|
||||
// Query describes a query operation passed to a Queryable value.
|
||||
// Params contains query input supplied by WITH, while Options contains
|
||||
// execution policy supplied by OPTIONS.
|
||||
Query struct {
|
||||
Options Value `json:"options"`
|
||||
Kind String `json:"kind"`
|
||||
Payload String `json:"payload"` // TODO: Rename to "expression" or "value"
|
||||
Options Value `json:"options"`
|
||||
Params Value `json:"params"`
|
||||
Kind String `json:"kind"`
|
||||
Expression String `json:"expression"`
|
||||
}
|
||||
|
||||
// QueryFunc is the list-returning query implementation used by default modifier helpers.
|
||||
|
||||
@@ -2,6 +2,7 @@ package runtime
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -47,3 +48,32 @@ func TestDefaultQueryOneReturnsFirstOrNone(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryJSONFieldNames(t *testing.T) {
|
||||
data, err := json.Marshal(Query{
|
||||
Options: None,
|
||||
Params: NewObjectWith(map[string]Value{"value": NewInt(1)}),
|
||||
Kind: NewString("capture"),
|
||||
Expression: NewString("test"),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to marshal query: %v", err)
|
||||
}
|
||||
|
||||
var fields map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &fields); err != nil {
|
||||
t.Fatalf("failed to unmarshal query fields: %v", err)
|
||||
}
|
||||
|
||||
for _, key := range []string{"options", "params", "kind", "expression"} {
|
||||
if _, ok := fields[key]; !ok {
|
||||
t.Fatalf("expected query JSON field %q in %s", key, data)
|
||||
}
|
||||
}
|
||||
if len(fields) != 4 {
|
||||
t.Fatalf("expected exactly four query JSON fields, got %d in %s", len(fields), data)
|
||||
}
|
||||
if _, ok := fields["payload"]; ok {
|
||||
t.Fatalf("did not expect legacy payload field in %s", data)
|
||||
}
|
||||
}
|
||||
|
||||
+59
-14
@@ -98,12 +98,40 @@ func coerceDispatchArgs(
|
||||
func coerceQueryDescriptor(ctx context.Context, descriptor runtime.Value) (runtime.Query, error) {
|
||||
switch value := descriptor.(type) {
|
||||
case runtime.ObjectLike:
|
||||
length, err := value.Length(ctx)
|
||||
if err != nil {
|
||||
return runtime.Query{}, err
|
||||
}
|
||||
if length != 4 {
|
||||
return runtime.Query{}, runtime.Error(runtime.ErrInvalidOperation, errQueryFormatUnexpected)
|
||||
}
|
||||
|
||||
for _, key := range []runtime.Value{
|
||||
queryDescriptorKeyKind,
|
||||
queryDescriptorKeyExpression,
|
||||
queryDescriptorKeyParams,
|
||||
queryDescriptorKeyOptions,
|
||||
} {
|
||||
ok, err := value.ContainsKey(ctx, key)
|
||||
if err != nil {
|
||||
return runtime.Query{}, err
|
||||
}
|
||||
if !ok {
|
||||
return runtime.Query{}, runtime.Error(runtime.ErrInvalidOperation, errQueryFormatUnexpected)
|
||||
}
|
||||
}
|
||||
|
||||
kind, err := value.Get(ctx, queryDescriptorKeyKind)
|
||||
if err != nil {
|
||||
return runtime.Query{}, err
|
||||
}
|
||||
|
||||
payload, err := value.Get(ctx, queryDescriptorKeyPayload)
|
||||
expression, err := value.Get(ctx, queryDescriptorKeyExpression)
|
||||
if err != nil {
|
||||
return runtime.Query{}, err
|
||||
}
|
||||
|
||||
params, err := value.Get(ctx, queryDescriptorKeyParams)
|
||||
if err != nil {
|
||||
return runtime.Query{}, err
|
||||
}
|
||||
@@ -113,10 +141,21 @@ func coerceQueryDescriptor(ctx context.Context, descriptor runtime.Value) (runti
|
||||
return runtime.Query{}, err
|
||||
}
|
||||
|
||||
kindString, err := runtime.CastString(kind)
|
||||
if err != nil {
|
||||
return runtime.Query{}, runtime.TypeErrorOf(kind, runtime.TypeString)
|
||||
}
|
||||
|
||||
expressionString, err := runtime.CastString(expression)
|
||||
if err != nil {
|
||||
return runtime.Query{}, runtime.TypeErrorOf(expression, runtime.TypeString)
|
||||
}
|
||||
|
||||
return runtime.Query{
|
||||
Kind: runtime.CastOr[runtime.String](kind, runtime.EmptyString),
|
||||
Payload: runtime.CastOr[runtime.String](payload, runtime.EmptyString),
|
||||
Options: options,
|
||||
Kind: kindString,
|
||||
Expression: expressionString,
|
||||
Params: params,
|
||||
Options: options,
|
||||
}, nil
|
||||
case *runtime.Array:
|
||||
length, err := value.Length(ctx)
|
||||
@@ -124,7 +163,7 @@ func coerceQueryDescriptor(ctx context.Context, descriptor runtime.Value) (runti
|
||||
return runtime.Query{}, err
|
||||
}
|
||||
|
||||
if length != 3 {
|
||||
if length != 4 {
|
||||
return runtime.Query{}, runtime.Error(runtime.ErrInvalidOperation, errQueryFormatUnexpected)
|
||||
}
|
||||
|
||||
@@ -133,12 +172,17 @@ func coerceQueryDescriptor(ctx context.Context, descriptor runtime.Value) (runti
|
||||
return runtime.Query{}, err
|
||||
}
|
||||
|
||||
payloadVal, err := value.At(ctx, runtime.NewInt(1))
|
||||
expressionVal, err := value.At(ctx, runtime.NewInt(1))
|
||||
if err != nil {
|
||||
return runtime.Query{}, err
|
||||
}
|
||||
|
||||
optionsVal, err := value.At(ctx, runtime.NewInt(2))
|
||||
paramsVal, err := value.At(ctx, runtime.NewInt(2))
|
||||
if err != nil {
|
||||
return runtime.Query{}, err
|
||||
}
|
||||
|
||||
optionsVal, err := value.At(ctx, runtime.NewInt(3))
|
||||
if err != nil {
|
||||
return runtime.Query{}, err
|
||||
}
|
||||
@@ -148,18 +192,19 @@ func coerceQueryDescriptor(ctx context.Context, descriptor runtime.Value) (runti
|
||||
return runtime.Query{}, runtime.TypeErrorOf(kindVal, runtime.TypeString)
|
||||
}
|
||||
|
||||
payload := runtime.EmptyString
|
||||
if payloadVal != runtime.None {
|
||||
payload, err = runtime.CastString(payloadVal)
|
||||
expression := runtime.EmptyString
|
||||
if expressionVal != runtime.None {
|
||||
expression, err = runtime.CastString(expressionVal)
|
||||
if err != nil {
|
||||
return runtime.Query{}, runtime.TypeErrorOf(payloadVal, runtime.TypeString, runtime.TypeNone)
|
||||
return runtime.Query{}, runtime.TypeErrorOf(expressionVal, runtime.TypeString, runtime.TypeNone)
|
||||
}
|
||||
}
|
||||
|
||||
return runtime.Query{
|
||||
Kind: kind,
|
||||
Payload: payload,
|
||||
Options: optionsVal,
|
||||
Kind: kind,
|
||||
Expression: expression,
|
||||
Params: paramsVal,
|
||||
Options: optionsVal,
|
||||
}, nil
|
||||
default:
|
||||
return runtime.Query{}, runtime.Error(runtime.ErrInvalidOperation, errQueryFormatUnexpected)
|
||||
|
||||
+8
-6
@@ -7,17 +7,19 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
queryDescriptorKind = "kind"
|
||||
queryDescriptorPayload = "payload"
|
||||
queryDescriptorOptions = "options"
|
||||
queryDescriptorKind = "kind"
|
||||
queryDescriptorExpression = "expression"
|
||||
queryDescriptorParams = "params"
|
||||
queryDescriptorOptions = "options"
|
||||
|
||||
errQueryFormatUnexpected = "unexpected query format"
|
||||
)
|
||||
|
||||
var (
|
||||
queryDescriptorKeyKind = runtime.NewString(queryDescriptorKind)
|
||||
queryDescriptorKeyPayload = runtime.NewString(queryDescriptorPayload)
|
||||
queryDescriptorKeyOptions = runtime.NewString(queryDescriptorOptions)
|
||||
queryDescriptorKeyKind = runtime.NewString(queryDescriptorKind)
|
||||
queryDescriptorKeyExpression = runtime.NewString(queryDescriptorExpression)
|
||||
queryDescriptorKeyParams = runtime.NewString(queryDescriptorParams)
|
||||
queryDescriptorKeyOptions = runtime.NewString(queryDescriptorOptions)
|
||||
)
|
||||
|
||||
func applyQuery(ctx context.Context, src runtime.Value, descriptor runtime.Value) (runtime.Value, error) {
|
||||
|
||||
+62
-16
@@ -99,6 +99,7 @@ func validDescriptor() runtime.Value {
|
||||
runtime.NewString("css"),
|
||||
runtime.NewString(".items"),
|
||||
runtime.None,
|
||||
runtime.None,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -131,7 +132,7 @@ func assertStringArray(t *testing.T, out runtime.Value, expected ...runtime.Stri
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyQuery_ObjectDescriptorIgnoresModifier(t *testing.T) {
|
||||
func TestApplyQuery_ObjectDescriptorUsesCanonicalShape(t *testing.T) {
|
||||
src := &queryStub{
|
||||
result: runtime.NewArrayWith(runtime.NewString("ok")),
|
||||
}
|
||||
@@ -139,9 +140,9 @@ func TestApplyQuery_ObjectDescriptorIgnoresModifier(t *testing.T) {
|
||||
obj := runtime.NewObject()
|
||||
ctx := context.Background()
|
||||
_ = obj.Set(ctx, runtime.NewString("kind"), runtime.NewString("css"))
|
||||
_ = obj.Set(ctx, runtime.NewString("payload"), runtime.NewString(".items"))
|
||||
_ = obj.Set(ctx, runtime.NewString("expression"), runtime.NewString(".items"))
|
||||
_ = obj.Set(ctx, runtime.NewString("params"), runtime.NewString("input"))
|
||||
_ = obj.Set(ctx, runtime.NewString("options"), runtime.None)
|
||||
_ = obj.Set(ctx, runtime.NewString("modifier"), runtime.NewString("ONE"))
|
||||
|
||||
out, err := applyQuery(ctx, src, obj)
|
||||
if err != nil {
|
||||
@@ -153,21 +154,21 @@ func TestApplyQuery_ObjectDescriptorIgnoresModifier(t *testing.T) {
|
||||
if len(src.queries) != 1 {
|
||||
t.Fatalf("unexpected query count: got %d, want 1", len(src.queries))
|
||||
}
|
||||
if src.queries[0].Params != runtime.NewString("input") || src.queries[0].Options != runtime.None {
|
||||
t.Fatalf("unexpected query params/options: %#v", src.queries[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyQuery_ArrayDescriptorRequiresExactTupleSize(t *testing.T) {
|
||||
src := &queryStub{
|
||||
result: runtime.NewArrayWith(runtime.NewString("ok")),
|
||||
}
|
||||
func TestApplyQuery_ObjectDescriptorRejectsExtraFields(t *testing.T) {
|
||||
obj := runtime.NewObjectWith(map[string]runtime.Value{
|
||||
"kind": runtime.NewString("css"),
|
||||
"expression": runtime.NewString(".items"),
|
||||
"params": runtime.None,
|
||||
"options": runtime.None,
|
||||
"modifier": runtime.NewString("ONE"),
|
||||
})
|
||||
|
||||
descriptor := runtime.NewArrayWith(
|
||||
runtime.NewString("css"),
|
||||
runtime.NewString(".items"),
|
||||
runtime.None,
|
||||
runtime.NewString("count"),
|
||||
)
|
||||
|
||||
_, err := applyQuery(context.Background(), src, descriptor)
|
||||
_, err := applyQuery(context.Background(), &queryStub{}, obj)
|
||||
if err == nil {
|
||||
t.Fatal("expected runtime error")
|
||||
}
|
||||
@@ -177,7 +178,51 @@ func TestApplyQuery_ArrayDescriptorRequiresExactTupleSize(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyQuery_ArrayDescriptorPayloadTypeValidation(t *testing.T) {
|
||||
func TestApplyQuery_ObjectDescriptorRejectsLegacyPayloadShape(t *testing.T) {
|
||||
obj := runtime.NewObjectWith(map[string]runtime.Value{
|
||||
"kind": runtime.NewString("css"),
|
||||
"payload": runtime.NewString(".items"),
|
||||
"params": runtime.None,
|
||||
"options": runtime.None,
|
||||
})
|
||||
|
||||
_, err := applyQuery(context.Background(), &queryStub{}, obj)
|
||||
if err == nil {
|
||||
t.Fatal("expected runtime error")
|
||||
}
|
||||
|
||||
if !strings.Contains(strings.ToLower(err.Error()), "unexpected query format") {
|
||||
t.Fatalf("expected unexpected query format error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyQuery_ArrayDescriptorRequiresExactTupleSize(t *testing.T) {
|
||||
for _, descriptor := range []*runtime.Array{
|
||||
runtime.NewArrayWith(
|
||||
runtime.NewString("css"),
|
||||
runtime.NewString(".items"),
|
||||
runtime.None,
|
||||
),
|
||||
runtime.NewArrayWith(
|
||||
runtime.NewString("css"),
|
||||
runtime.NewString(".items"),
|
||||
runtime.None,
|
||||
runtime.None,
|
||||
runtime.None,
|
||||
),
|
||||
} {
|
||||
_, err := applyQuery(context.Background(), &queryStub{}, descriptor)
|
||||
if err == nil {
|
||||
t.Fatal("expected runtime error")
|
||||
}
|
||||
|
||||
if !strings.Contains(strings.ToLower(err.Error()), "unexpected query format") {
|
||||
t.Fatalf("expected unexpected query format error, got %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyQuery_ArrayDescriptorExpressionTypeValidation(t *testing.T) {
|
||||
src := &queryStub{
|
||||
result: runtime.NewArrayWith(runtime.NewString("ok")),
|
||||
}
|
||||
@@ -186,6 +231,7 @@ func TestApplyQuery_ArrayDescriptorPayloadTypeValidation(t *testing.T) {
|
||||
runtime.NewString("css"),
|
||||
runtime.NewInt(1),
|
||||
runtime.None,
|
||||
runtime.None,
|
||||
)
|
||||
|
||||
_, err := applyQuery(context.Background(), src, descriptor)
|
||||
|
||||
@@ -13,6 +13,11 @@ RETURN [
|
||||
QUERY ONE ".items" IN @doc USING css,
|
||||
]`
|
||||
|
||||
const queryableConfiguredQuery = `
|
||||
RETURN QUERY ".items" IN @doc USING css
|
||||
WITH { value: 1 }
|
||||
OPTIONS { timeout: 5000 }`
|
||||
|
||||
func BenchmarkQueryableModifiers_O0(b *testing.B) {
|
||||
RunBenchmarkO0(b, queryableModifiersQuery, vm.WithParam("doc", newBenchmarkQueryable()))
|
||||
}
|
||||
@@ -20,3 +25,11 @@ func BenchmarkQueryableModifiers_O0(b *testing.B) {
|
||||
func BenchmarkQueryableModifiers_O1(b *testing.B) {
|
||||
RunBenchmarkO1(b, queryableModifiersQuery, vm.WithParam("doc", newBenchmarkQueryable()))
|
||||
}
|
||||
|
||||
func BenchmarkQueryableConfigured_O0(b *testing.B) {
|
||||
RunBenchmarkO0(b, queryableConfiguredQuery, vm.WithParam("doc", newBenchmarkQueryable()))
|
||||
}
|
||||
|
||||
func BenchmarkQueryableConfigured_O1(b *testing.B) {
|
||||
RunBenchmarkO1(b, queryableConfiguredQuery, vm.WithParam("doc", newBenchmarkQueryable()))
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@ package compiler_test
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/MontFerret/ferret/v2/pkg/compiler"
|
||||
parserd "github.com/MontFerret/ferret/v2/pkg/parser/diagnostics"
|
||||
"github.com/MontFerret/ferret/v2/pkg/source"
|
||||
"github.com/MontFerret/ferret/v2/test/spec"
|
||||
. "github.com/MontFerret/ferret/v2/test/spec/compile"
|
||||
)
|
||||
@@ -113,10 +115,47 @@ func TestSyntaxErrorsQueryExpression(t *testing.T) {
|
||||
"RETURN QUERY `.x` IN @doc USING css WITH )",
|
||||
E{
|
||||
Kind: parserd.SyntaxError,
|
||||
Message: "Expected options expression after WITH",
|
||||
Hint: "Provide an options expression, e.g. WITH { limit: 10 }.",
|
||||
Message: "Expected query params expression after WITH",
|
||||
Hint: "Provide a params expression, e.g. WITH { params: [1] }.",
|
||||
},
|
||||
"Missing WITH value",
|
||||
).Skip(),
|
||||
),
|
||||
Failure(
|
||||
"RETURN QUERY `.x` IN @doc USING css OPTIONS )",
|
||||
E{
|
||||
Kind: parserd.SyntaxError,
|
||||
Message: "Expected query options expression after OPTIONS",
|
||||
Hint: "Provide an options expression, e.g. OPTIONS { timeout: 5000 }.",
|
||||
},
|
||||
"Missing OPTIONS value",
|
||||
),
|
||||
Failure(
|
||||
"RETURN QUERY `.x` IN @doc USING css OPTIONS { timeout: 5000 } WITH { params: [1] }",
|
||||
E{
|
||||
Kind: parserd.SyntaxError,
|
||||
Message: "WITH must appear before OPTIONS in QUERY",
|
||||
Hint: "Move the WITH clause before OPTIONS.",
|
||||
},
|
||||
"WITH after OPTIONS",
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
func TestSyntaxErrorsQueryExpressionWithAfterOptionsSpan(t *testing.T) {
|
||||
query := "RETURN QUERY `.x` IN @doc USING css OPTIONS { timeout: 5000 } WITH { params: [1] }"
|
||||
|
||||
_, err := compiler.New().Compile(source.NewAnonymous(query))
|
||||
if err == nil {
|
||||
t.Fatal("expected compilation error")
|
||||
}
|
||||
|
||||
diag := firstCompilationError(err)
|
||||
if diag == nil || len(diag.Spans) == 0 {
|
||||
t.Fatalf("expected diagnostic span, got %v", err)
|
||||
}
|
||||
|
||||
span := diag.Spans[0].Span
|
||||
if got := query[span.Start:span.End]; got != "WITH" {
|
||||
t.Fatalf("expected diagnostic to point at WITH, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"github.com/MontFerret/ferret/v2/test/spec/compile/inspect"
|
||||
)
|
||||
|
||||
func threeSlotQueryDescriptorFor(code []bytecode.Instruction, opcode bytecode.Opcode) error {
|
||||
func fourSlotQueryDescriptorFor(code []bytecode.Instruction, opcode bytecode.Opcode) error {
|
||||
applyIdx, ok := inspect.FindFirstOpcodeIndex(code, opcode)
|
||||
if !ok {
|
||||
return fmt.Errorf("expected %s in bytecode", opcode)
|
||||
@@ -21,15 +21,15 @@ func threeSlotQueryDescriptorFor(code []bytecode.Instruction, opcode bytecode.Op
|
||||
return fmt.Errorf("expected OpLoadArray for query descriptor before OpQuery")
|
||||
}
|
||||
|
||||
if size != 3 {
|
||||
return fmt.Errorf("expected 3-slot query descriptor, got %d", size)
|
||||
if size != 4 {
|
||||
return fmt.Errorf("expected 4-slot query descriptor, got %d", size)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func threeSlotQueryDescriptor(code []bytecode.Instruction) error {
|
||||
return threeSlotQueryDescriptorFor(code, bytecode.OpQuery)
|
||||
func fourSlotQueryDescriptor(code []bytecode.Instruction) error {
|
||||
return fourSlotQueryDescriptorFor(code, bytecode.OpQuery)
|
||||
}
|
||||
|
||||
func TestQueryExpressionSourceImplicitCurrentCompiles(t *testing.T) {
|
||||
@@ -38,7 +38,7 @@ func TestQueryExpressionSourceImplicitCurrentCompiles(t *testing.T) {
|
||||
LET sections = @sections
|
||||
LET linksBySection = sections[* RETURN (QUERY "a" IN . USING css)]
|
||||
RETURN linksBySection[**]`, func(prog *bytecode.Program) error {
|
||||
return threeSlotQueryDescriptor(prog.Bytecode)
|
||||
return fourSlotQueryDescriptor(prog.Bytecode)
|
||||
}, "Should compile query expression with implicit current source"),
|
||||
})
|
||||
}
|
||||
@@ -46,7 +46,7 @@ RETURN linksBySection[**]`, func(prog *bytecode.Program) error {
|
||||
func TestQueryModifierLowering_OneUsesDirectOpcode(t *testing.T) {
|
||||
RunSpecs(t, []spec.Spec{
|
||||
ProgramCheck(`RETURN QUERY ONE ".items" IN @doc USING css`, func(prog *bytecode.Program) error {
|
||||
if err := threeSlotQueryDescriptorFor(prog.Bytecode, bytecode.OpQueryOne); err != nil {
|
||||
if err := fourSlotQueryDescriptorFor(prog.Bytecode, bytecode.OpQueryOne); err != nil {
|
||||
return err
|
||||
}
|
||||
if inspect.HasOpcode(prog, bytecode.OpLength) {
|
||||
@@ -70,7 +70,7 @@ func TestQueryModifierLowering_OneUsesDirectOpcode(t *testing.T) {
|
||||
func TestQueryShorthandLowering(t *testing.T) {
|
||||
RunSpecs(t, []spec.Spec{
|
||||
ProgramCheck("RETURN @doc[~ css`.items`]", func(prog *bytecode.Program) error {
|
||||
if err := threeSlotQueryDescriptorFor(prog.Bytecode, bytecode.OpQuery); err != nil {
|
||||
if err := fourSlotQueryDescriptorFor(prog.Bytecode, bytecode.OpQuery); err != nil {
|
||||
return err
|
||||
}
|
||||
if inspect.HasOpcode(prog, bytecode.OpQueryOne) {
|
||||
@@ -80,7 +80,7 @@ func TestQueryShorthandLowering(t *testing.T) {
|
||||
return nil
|
||||
}, "regular query shorthand lowering"),
|
||||
ProgramCheck("RETURN @doc[~? css`.items`]", func(prog *bytecode.Program) error {
|
||||
if err := threeSlotQueryDescriptorFor(prog.Bytecode, bytecode.OpQueryOne); err != nil {
|
||||
if err := fourSlotQueryDescriptorFor(prog.Bytecode, bytecode.OpQueryOne); err != nil {
|
||||
return err
|
||||
}
|
||||
if inspect.HasOpcode(prog, bytecode.OpQuery) {
|
||||
@@ -115,7 +115,7 @@ func TestQueryModifierLowering_ExistsCount(t *testing.T) {
|
||||
specs := make([]spec.Spec, 0, len(cases))
|
||||
for _, tc := range cases {
|
||||
specs = append(specs, ProgramCheck(tc.expr, func(prog *bytecode.Program) error {
|
||||
if err := threeSlotQueryDescriptorFor(prog.Bytecode, tc.opcode); err != nil {
|
||||
if err := fourSlotQueryDescriptorFor(prog.Bytecode, tc.opcode); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -74,6 +74,12 @@ FOR u IN users
|
||||
Env: []vm.EnvironmentOption{vm.WithParams(runtime.Params{"doc": queryable})},
|
||||
Description: "Query literal with params",
|
||||
},
|
||||
{
|
||||
Input: spec.NewExpressionInput("RETURN QUERY `.items` IN @doc USING css WITH { value: 1 } OPTIONS { timeout: 5000 }"),
|
||||
Expected: []any{"ok"},
|
||||
Env: []vm.EnvironmentOption{vm.WithParams(runtime.Params{"doc": queryable})},
|
||||
Description: "Configured query expression",
|
||||
},
|
||||
{
|
||||
Input: spec.NewExpressionInput(`
|
||||
FUNC add(x, y) => x + y
|
||||
|
||||
@@ -98,6 +98,12 @@ FOR u IN users
|
||||
Env: []vm.EnvironmentOption{vm.WithParams(map[string]runtime.Value{"doc": queryable})},
|
||||
Description: "Query literal without params",
|
||||
},
|
||||
{
|
||||
Input: spec.NewExpressionInput("RETURN QUERY `.items` IN @doc USING css WITH { value: 1 } OPTIONS { timeout: 5000 }"),
|
||||
Expected: []any{"ok"},
|
||||
Env: []vm.EnvironmentOption{vm.WithParams(map[string]runtime.Value{"doc": queryable})},
|
||||
Description: "Configured query expression",
|
||||
},
|
||||
{
|
||||
Input: spec.NewExpressionInput(`
|
||||
FUNC add(x, y) => x + y
|
||||
|
||||
@@ -73,6 +73,7 @@ func programWithApplyQueryConstSource(src runtime.Value) *bytecode.Program {
|
||||
runtime.NewString("css"),
|
||||
runtime.NewString(".items"),
|
||||
runtime.None,
|
||||
runtime.None,
|
||||
),
|
||||
},
|
||||
}
|
||||
@@ -98,8 +99,8 @@ func TestApplyQueryConstantSource_Strict(t *testing.T) {
|
||||
return fmt.Errorf("unexpected query kind: got %q, want %q", got, want)
|
||||
}
|
||||
|
||||
if got, want := stub.queries[0].Payload, runtime.NewString(".items"); got != want {
|
||||
return fmt.Errorf("unexpected query payload: got %q, want %q", got, want)
|
||||
if got, want := stub.queries[0].Expression, runtime.NewString(".items"); got != want {
|
||||
return fmt.Errorf("unexpected query expression: got %q, want %q", got, want)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -3,6 +3,7 @@ package vm_test
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/MontFerret/ferret/v2/pkg/runtime"
|
||||
@@ -21,12 +22,16 @@ func TestQueryable(t *testing.T) {
|
||||
Array("RETURN @doc[~ css`.items`]", []any{"ok"}, "Should apply query literal"),
|
||||
S("RETURN @doc[~ css`.items`][0]", "ok", "Should apply query literal and index tail"),
|
||||
Array("RETURN QUERY `.items` IN @doc USING css", []any{"ok"}, "Should apply query expression"),
|
||||
Array("RETURN QUERY @q IN @doc USING css", []any{"ok"}, "Should apply query expression with param payload"),
|
||||
Array("LET q = \".dynamic-var\"\nRETURN QUERY q IN @doc USING css", []any{"ok"}, "Should apply query expression with variable payload"),
|
||||
Array("RETURN QUERY @q IN @doc USING css", []any{"ok"}, "Should apply query expression with param expression"),
|
||||
Array("LET q = \".dynamic-var\"\nRETURN QUERY q IN @doc USING css", []any{"ok"}, "Should apply query expression with variable expression"),
|
||||
Array("RETURN QUERY `.with` IN @doc USING css WITH { value: 1 }", []any{"ok"}, "Should apply query expression with params"),
|
||||
Array("RETURN QUERY `.options` IN @doc USING css OPTIONS { timeout: 5000 }", []any{"ok"}, "Should apply query expression with options"),
|
||||
Array("RETURN QUERY `.both` IN @doc USING css WITH { value: 2 } OPTIONS { timeout: 6000 }", []any{"ok"}, "Should apply query expression with params and options"),
|
||||
S("RETURN QUERY ONE `.one-both` IN @doc USING css WITH { value: 3 } OPTIONS { timeout: 7000 }", "ok", "Should apply query-one expression with params and options"),
|
||||
Array("RETURN @doc[~ sql`SELECT * FROM products`({ c: \"laptops\" })]", []any{"ok"}, "Should apply query literal with params"),
|
||||
S("RETURN @doc[~? sql`SELECT * FROM featured`({ c: \"tablets\" })]", "ok", "Should apply query-one literal with params"),
|
||||
Array("RETURN QUERY `SELECT * FROM products` IN @doc USING sql WITH { c: \"phones\" }", []any{"ok"}, "Should apply query expression with options"),
|
||||
Array("RETURN @doc[~ text]", []any{"ok"}, "Should apply query literal with no string payload"),
|
||||
Array("RETURN @doc[~ text]", []any{"ok"}, "Should apply query literal with no string expression"),
|
||||
Array(`RETURN @sections[* RETURN (QUERY "a" IN . USING css)][**]`, []any{"one", "two"}, "Should apply query expression to implicit current source"),
|
||||
spec.NewSpec("RETURN @val[~ css`x`]").Expect().ExecError(ShouldBeRuntimeError, &ExpectedRuntimeError{
|
||||
Message: "invalid type",
|
||||
@@ -46,26 +51,46 @@ func TestQueryable(t *testing.T) {
|
||||
var hasSQLParamsOne bool
|
||||
var hasSQLQueryExpr bool
|
||||
var hasText bool
|
||||
var hasNoClauses bool
|
||||
var hasWithOnly bool
|
||||
var hasOptionsOnly bool
|
||||
var hasBoth bool
|
||||
var hasOneBoth bool
|
||||
|
||||
for _, q := range queryable.MockQueries() {
|
||||
switch q.Kind {
|
||||
case runtime.NewString("css"):
|
||||
if q.Payload == runtime.NewString(".items") {
|
||||
if q.Expression == runtime.NewString(".items") {
|
||||
hasCSS = true
|
||||
hasNoClauses = q.Params == runtime.None && q.Options == runtime.None
|
||||
}
|
||||
if q.Payload == runtime.NewString(".dynamic-param") {
|
||||
if q.Expression == runtime.NewString(".dynamic-param") {
|
||||
hasCSSParam = true
|
||||
}
|
||||
if q.Payload == runtime.NewString(".dynamic-var") {
|
||||
if q.Expression == runtime.NewString(".dynamic-var") {
|
||||
hasCSSVar = true
|
||||
}
|
||||
if q.Expression == runtime.NewString(".with") {
|
||||
hasWithOnly = queryMapValue(t, q.Params, "value") == runtime.NewInt(1) && q.Options == runtime.None
|
||||
}
|
||||
if q.Expression == runtime.NewString(".options") {
|
||||
hasOptionsOnly = q.Params == runtime.None && queryMapValue(t, q.Options, "timeout") == runtime.NewInt(5000)
|
||||
}
|
||||
if q.Expression == runtime.NewString(".both") {
|
||||
hasBoth = queryMapValue(t, q.Params, "value") == runtime.NewInt(2) &&
|
||||
queryMapValue(t, q.Options, "timeout") == runtime.NewInt(6000)
|
||||
}
|
||||
if q.Expression == runtime.NewString(".one-both") {
|
||||
hasOneBoth = queryMapValue(t, q.Params, "value") == runtime.NewInt(3) &&
|
||||
queryMapValue(t, q.Options, "timeout") == runtime.NewInt(7000)
|
||||
}
|
||||
case runtime.NewString("text"):
|
||||
if q.Payload == runtime.EmptyString {
|
||||
if q.Expression == runtime.EmptyString {
|
||||
hasText = true
|
||||
}
|
||||
case runtime.NewString("sql"):
|
||||
if q.Payload == runtime.NewString("SELECT * FROM products") {
|
||||
params, err := runtime.ToMap(context.Background(), q.Options)
|
||||
if q.Expression == runtime.NewString("SELECT * FROM products") {
|
||||
params, err := runtime.ToMap(context.Background(), q.Params)
|
||||
if err != nil {
|
||||
t.Fatalf("sql query expression params decode failed: %v", err)
|
||||
}
|
||||
@@ -75,41 +100,56 @@ func TestQueryable(t *testing.T) {
|
||||
hasSQLQueryExpr = true
|
||||
}
|
||||
}
|
||||
if q.Payload == runtime.NewString("SELECT * FROM featured") {
|
||||
params, err := runtime.ToMap(context.Background(), q.Options)
|
||||
if q.Expression == runtime.NewString("SELECT * FROM featured") {
|
||||
params, err := runtime.ToMap(context.Background(), q.Params)
|
||||
if err != nil {
|
||||
t.Fatalf("sql query-one params decode failed: %v", err)
|
||||
}
|
||||
|
||||
value, err := params.Get(context.Background(), runtime.NewString("c"))
|
||||
if err == nil && value == runtime.NewString("tablets") {
|
||||
if err == nil && value == runtime.NewString("tablets") && q.Options == runtime.None {
|
||||
hasSQLParamsOne = true
|
||||
}
|
||||
}
|
||||
|
||||
params, err := runtime.ToMap(context.Background(), q.Options)
|
||||
params, err := runtime.ToMap(context.Background(), q.Params)
|
||||
if err != nil {
|
||||
t.Fatalf("sql params decode failed: %v", err)
|
||||
}
|
||||
|
||||
value, err := params.Get(context.Background(), runtime.NewString("c"))
|
||||
if err == nil && value == runtime.NewString("laptops") {
|
||||
if err == nil && value == runtime.NewString("laptops") && q.Options == runtime.None {
|
||||
hasSQLParams = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !hasCSS {
|
||||
t.Fatal(fmt.Sprintf("expected to receive a query with kind %q and payload %q", "css", ".items"))
|
||||
t.Fatal(fmt.Sprintf("expected to receive a query with kind %q and expression %q", "css", ".items"))
|
||||
}
|
||||
if !hasCSSParam {
|
||||
t.Fatal(fmt.Sprintf("expected to receive a query with kind %q and payload %q", "css", ".dynamic-param"))
|
||||
t.Fatal(fmt.Sprintf("expected to receive a query with kind %q and expression %q", "css", ".dynamic-param"))
|
||||
}
|
||||
if !hasCSSVar {
|
||||
t.Fatal(fmt.Sprintf("expected to receive a query with kind %q and payload %q", "css", ".dynamic-var"))
|
||||
t.Fatal(fmt.Sprintf("expected to receive a query with kind %q and expression %q", "css", ".dynamic-var"))
|
||||
}
|
||||
if !hasText {
|
||||
t.Fatal(fmt.Sprintf("expected to receive a query with kind %q and empty payload", "text"))
|
||||
t.Fatal(fmt.Sprintf("expected to receive a query with kind %q and empty expression", "text"))
|
||||
}
|
||||
if !hasNoClauses {
|
||||
t.Fatal("expected omitted WITH and OPTIONS clauses to produce NONE")
|
||||
}
|
||||
if !hasWithOnly {
|
||||
t.Fatal("expected WITH to populate params without options")
|
||||
}
|
||||
if !hasOptionsOnly {
|
||||
t.Fatal("expected OPTIONS to populate options without params")
|
||||
}
|
||||
if !hasBoth {
|
||||
t.Fatal("expected WITH and OPTIONS to remain distinct")
|
||||
}
|
||||
if !hasOneBoth {
|
||||
t.Fatal("expected QUERY ONE to receive distinct params and options")
|
||||
}
|
||||
if !hasSQLQueryExpr {
|
||||
t.Fatal(fmt.Sprintf("expected to receive a query with kind %q and params containing %q=%q", "sql", "c", "phones"))
|
||||
@@ -123,6 +163,69 @@ func TestQueryable(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestQueryExpressionClausesEvaluatedOnce(t *testing.T) {
|
||||
queryable := mock.NewQueryable(runtime.NewArrayWith(runtime.NewString("ok")))
|
||||
paramsCalls := 0
|
||||
optionsCalls := 0
|
||||
var clauseOrder []string
|
||||
|
||||
RunSpecs(t, []spec.Spec{
|
||||
Array(
|
||||
"RETURN QUERY `.once` IN @doc USING css WITH PARAMS_FN() OPTIONS OPTIONS_FN()",
|
||||
[]any{"ok"},
|
||||
"Should evaluate query params and options once",
|
||||
),
|
||||
},
|
||||
vm.WithParam("doc", queryable),
|
||||
vm.WithFunction("PARAMS_FN", func(context.Context, ...runtime.Value) (runtime.Value, error) {
|
||||
paramsCalls++
|
||||
clauseOrder = append(clauseOrder, "params")
|
||||
return runtime.NewObjectWith(map[string]runtime.Value{"value": runtime.NewInt(1)}), nil
|
||||
}),
|
||||
vm.WithFunction("OPTIONS_FN", func(context.Context, ...runtime.Value) (runtime.Value, error) {
|
||||
optionsCalls++
|
||||
clauseOrder = append(clauseOrder, "options")
|
||||
return runtime.NewObjectWith(map[string]runtime.Value{"timeout": runtime.NewInt(5000)}), nil
|
||||
}),
|
||||
)
|
||||
|
||||
if paramsCalls != 2 || optionsCalls != 2 {
|
||||
t.Fatalf("expected params and options to be evaluated once per O0/O1 execution, got params=%d options=%d", paramsCalls, optionsCalls)
|
||||
}
|
||||
if got, want := strings.Join(clauseOrder, ","), "params,options,params,options"; got != want {
|
||||
t.Fatalf("unexpected clause evaluation order: got %q, want %q", got, want)
|
||||
}
|
||||
|
||||
queries := queryable.MockQueries()
|
||||
if len(queries) != 2 {
|
||||
t.Fatalf("expected one captured query per O0/O1 execution, got %d", len(queries))
|
||||
}
|
||||
for _, query := range queries {
|
||||
if queryMapValue(t, query.Params, "value") != runtime.NewInt(1) {
|
||||
t.Fatal("expected captured params")
|
||||
}
|
||||
if queryMapValue(t, query.Options, "timeout") != runtime.NewInt(5000) {
|
||||
t.Fatal("expected captured options")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func queryMapValue(t *testing.T, value runtime.Value, key string) runtime.Value {
|
||||
t.Helper()
|
||||
|
||||
out, err := runtime.ToMap(context.Background(), value)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to decode query map: %v", err)
|
||||
}
|
||||
|
||||
item, err := out.Get(context.Background(), runtime.NewString(key))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read query map key %q: %v", key, err)
|
||||
}
|
||||
|
||||
return item
|
||||
}
|
||||
|
||||
func TestComplexQueries(t *testing.T) {
|
||||
queryableDoc := mock.NewNode("doc")
|
||||
queryableDB := mock.NewDBQueryable()
|
||||
|
||||
@@ -42,7 +42,7 @@ func (n *Node) Iterate(ctx context.Context) (runtime.Iterator, error) {
|
||||
func (n *Node) Query(_ context.Context, q runtime.Query) (runtime.List, error) {
|
||||
switch q.Kind.String() {
|
||||
case "css":
|
||||
switch q.Payload.String() {
|
||||
switch q.Expression.String() {
|
||||
case ".product":
|
||||
return runtime.NewArrayWith(NewNode("product")), nil
|
||||
case ".title":
|
||||
|
||||
@@ -25,7 +25,7 @@ func (m *DBQueryable) Query(ctx context.Context, q runtime.Query) (runtime.List,
|
||||
return runtime.NewArray(0), nil
|
||||
}
|
||||
|
||||
params, err := runtime.ToMap(ctx, q.Options)
|
||||
params, err := runtime.ToMap(ctx, q.Params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user