// Copyright (c) 2014 Couchbase, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package query import ( "reflect" "strings" "testing" "time" "github.com/blevesearch/bleve/mapping" ) func TestQuerySyntaxParserValid(t *testing.T) { thirtyThreePointOh := 33.0 twoPointOh := 2.0 fivePointOh := 5.0 theTruth := true theFalsehood := false theDate, err := time.Parse(time.RFC3339, "2006-01-02T15:04:05Z") if err != nil { t.Fatal(err) } tests := []struct { input string result Query mapping mapping.IndexMapping }{ { input: "test", mapping: mapping.NewIndexMapping(), result: NewBooleanQueryForQueryString( nil, []Query{ NewMatchQuery("test"), }, nil), }, { input: `"test phrase 1"`, mapping: mapping.NewIndexMapping(), result: NewBooleanQueryForQueryString( nil, []Query{ NewMatchPhraseQuery("test phrase 1"), }, nil), }, { input: "field:test", mapping: mapping.NewIndexMapping(), result: NewBooleanQueryForQueryString( nil, []Query{ func() Query { q := NewMatchQuery("test") q.SetField("field") return q }(), }, nil), }, // - is allowed inside a term, just not the start { input: "field:t-est", mapping: mapping.NewIndexMapping(), result: NewBooleanQueryForQueryString( nil, []Query{ func() Query { q := NewMatchQuery("t-est") q.SetField("field") return q }(), }, nil), }, // + is allowed inside a term, just not the start { input: "field:t+est", mapping: mapping.NewIndexMapping(), result: NewBooleanQueryForQueryString( nil, []Query{ func() Query { q := NewMatchQuery("t+est") q.SetField("field") return q }(), }, nil), }, // > is allowed inside a term, just not the start { input: "field:t>est", mapping: mapping.NewIndexMapping(), result: NewBooleanQueryForQueryString( nil, []Query{ func() Query { q := NewMatchQuery("t>est") q.SetField("field") return q }(), }, nil), }, // < is allowed inside a term, just not the start { input: "field:t5`, mapping: mapping.NewIndexMapping(), result: NewBooleanQueryForQueryString( nil, []Query{ func() Query { q := NewNumericRangeInclusiveQuery(&fivePointOh, nil, &theFalsehood, nil) q.SetField("field") return q }(), }, nil), }, { input: `field:>=5`, mapping: mapping.NewIndexMapping(), result: NewBooleanQueryForQueryString( nil, []Query{ func() Query { q := NewNumericRangeInclusiveQuery(&fivePointOh, nil, &theTruth, nil) q.SetField("field") return q }(), }, nil), }, { input: `field:<5`, mapping: mapping.NewIndexMapping(), result: NewBooleanQueryForQueryString( nil, []Query{ func() Query { q := NewNumericRangeInclusiveQuery(nil, &fivePointOh, nil, &theFalsehood) q.SetField("field") return q }(), }, nil), }, { input: `field:<=5`, mapping: mapping.NewIndexMapping(), result: NewBooleanQueryForQueryString( nil, []Query{ func() Query { q := NewNumericRangeInclusiveQuery(nil, &fivePointOh, nil, &theTruth) q.SetField("field") return q }(), }, nil), }, { input: `field:>"2006-01-02T15:04:05Z"`, mapping: mapping.NewIndexMapping(), result: NewBooleanQueryForQueryString( nil, []Query{ func() Query { q := NewDateRangeInclusiveQuery(theDate, time.Time{}, &theFalsehood, nil) q.SetField("field") return q }(), }, nil), }, { input: `field:>="2006-01-02T15:04:05Z"`, mapping: mapping.NewIndexMapping(), result: NewBooleanQueryForQueryString( nil, []Query{ func() Query { q := NewDateRangeInclusiveQuery(theDate, time.Time{}, &theTruth, nil) q.SetField("field") return q }(), }, nil), }, { input: `field:<"2006-01-02T15:04:05Z"`, mapping: mapping.NewIndexMapping(), result: NewBooleanQueryForQueryString( nil, []Query{ func() Query { q := NewDateRangeInclusiveQuery(time.Time{}, theDate, nil, &theFalsehood) q.SetField("field") return q }(), }, nil), }, { input: `field:<="2006-01-02T15:04:05Z"`, mapping: mapping.NewIndexMapping(), result: NewBooleanQueryForQueryString( nil, []Query{ func() Query { q := NewDateRangeInclusiveQuery(time.Time{}, theDate, nil, &theTruth) q.SetField("field") return q }(), }, nil), }, { input: `/mar.*ty/`, mapping: mapping.NewIndexMapping(), result: NewBooleanQueryForQueryString( nil, []Query{ NewRegexpQuery("mar.*ty"), }, nil), }, { input: `name:/mar.*ty/`, mapping: mapping.NewIndexMapping(), result: NewBooleanQueryForQueryString( nil, []Query{ func() Query { q := NewRegexpQuery("mar.*ty") q.SetField("name") return q }(), }, nil), }, { input: `mart*`, mapping: mapping.NewIndexMapping(), result: NewBooleanQueryForQueryString( nil, []Query{ NewWildcardQuery("mart*"), }, nil), }, { input: `name:mart*`, mapping: mapping.NewIndexMapping(), result: NewBooleanQueryForQueryString( nil, []Query{ func() Query { q := NewWildcardQuery("mart*") q.SetField("name") return q }(), }, nil), }, // tests for escaping // escape : as field delimeter { input: `name\:marty`, mapping: mapping.NewIndexMapping(), result: NewBooleanQueryForQueryString( nil, []Query{ NewMatchQuery("name:marty"), }, nil), }, // first colon delimiter, second escaped { input: `name:marty\:couchbase`, mapping: mapping.NewIndexMapping(), result: NewBooleanQueryForQueryString( nil, []Query{ func() Query { q := NewMatchQuery("marty:couchbase") q.SetField("name") return q }(), }, nil), }, // escape space, single arguemnt to match query { input: `marty\ couchbase`, mapping: mapping.NewIndexMapping(), result: NewBooleanQueryForQueryString( nil, []Query{ NewMatchQuery("marty couchbase"), }, nil), }, // escape leading plus, not a must clause { input: `\+marty`, mapping: mapping.NewIndexMapping(), result: NewBooleanQueryForQueryString( nil, []Query{ NewMatchQuery("+marty"), }, nil), }, // escape leading minus, not a must not clause { input: `\-marty`, mapping: mapping.NewIndexMapping(), result: NewBooleanQueryForQueryString( nil, []Query{ NewMatchQuery("-marty"), }, nil), }, // escape quote inside of phrase { input: `"what does \"quote\" mean"`, mapping: mapping.NewIndexMapping(), result: NewBooleanQueryForQueryString( nil, []Query{ NewMatchPhraseQuery(`what does "quote" mean`), }, nil), }, // escaping an unsupported character retains backslash { input: `can\ i\ escap\e`, mapping: mapping.NewIndexMapping(), result: NewBooleanQueryForQueryString( nil, []Query{ NewMatchQuery(`can i escap\e`), }, nil), }, // leading spaces { input: ` what`, mapping: mapping.NewIndexMapping(), result: NewBooleanQueryForQueryString( nil, []Query{ NewMatchQuery(`what`), }, nil), }, // no boost value defaults to 1 { input: `term^`, mapping: mapping.NewIndexMapping(), result: NewBooleanQueryForQueryString( nil, []Query{ func() Query { q := NewMatchQuery(`term`) q.SetBoost(1.0) return q }(), }, nil), }, // weird lexer cases, something that starts like a number // but contains escape and ends up as string { input: `3.0\:`, mapping: mapping.NewIndexMapping(), result: NewBooleanQueryForQueryString( nil, []Query{ NewMatchQuery(`3.0:`), }, nil), }, { input: `3.0\a`, mapping: mapping.NewIndexMapping(), result: NewBooleanQueryForQueryString( nil, []Query{ NewMatchQuery(`3.0\a`), }, nil), }, } // turn on lexer debugging // debugLexer = true // debugParser = true // logger = log.New(os.Stderr, "bleve ", log.LstdFlags) for _, test := range tests { q, err := parseQuerySyntax(test.input) if err != nil { t.Fatal(err) } if !reflect.DeepEqual(q, test.result) { t.Errorf("Expected %#v, got %#v: for %s", test.result, q, test.input) } } } func TestQuerySyntaxParserInvalid(t *testing.T) { tests := []struct { input string }{ {"^"}, {"^5"}, {"field:-text"}, {"field:+text"}, {"field:>text"}, {"field:>=text"}, {"field: 0 { tokenTypes = append(tokenTypes, rv) tokens = append(tokens, lval) lval.s = "" lval.n = 0 rv = l.Lex(&lval) } } }