diff --git a/README.md b/README.md index 34aaf48..32dcb6b 100644 --- a/README.md +++ b/README.md @@ -11,11 +11,11 @@ The `GetAst()` function will convert the JSON header into a struct that can be t ```go package example -import epsearchast_v3 "github.com/elasticpath/epcc-search-ast-helper/external/epsearchast/v3" +import "github.com/elasticpath/epcc-search-ast-helper" -func Example(headerValue string) (*epsearchast_v3.AstNode, error) { +func Example(headerValue string) (*epsearchast.AstNode, error) { - ast, err := epsearchast_v3.GetAst(headerValue) + ast, err := epsearchast.GetAst(headerValue) if err != nil { return nil, err @@ -37,12 +37,12 @@ This package provides a way to support aliases for fields, this will allow a use ```go package example -import epsearchast_v3 "github.com/elasticpath/epcc-search-ast-helper/external/epsearchast/v3" +import "github.com/elasticpath/epcc-search-ast-helper" -func Example(ast *epsearchast_v3.AstNode) error { +func Example(ast *epsearchast.AstNode) error { //The ast from the user will be converted into a new one, and if the user specified a payment_status field, the new ast will have it recorded as status. - aliasedAst, err := ApplyAliases(ast, map[string]string{"payment_status": "status"}) + aliasedAst, err := epsearchast.ApplyAliases(ast, map[string]string{"payment_status": "status"}) if err != nil { return err @@ -73,12 +73,12 @@ This package provides a concise way to validate that the operators and fields sp ```go package example -import epsearchast_v3 "github.com/elasticpath/epcc-search-ast-helper/external/epsearchast/v3" +import "github.com/elasticpath/epcc-search-ast-helper" -func Example(ast *epsearchast_v3.AstNode) error { +func Example(ast *epsearchast.AstNode) error { var err error // The following is an implementation of all the filter operators for orders https://elasticpath.dev/docs/orders/orders-api/orders-api-overview#filtering - err = epsearchast_v3.ValidateAstFieldAndOperators(ast, map[string][]string { + err = epsearchast.ValidateAstFieldAndOperators(ast, map[string][]string { "status": {"eq"}, "payment": {"eq"}, "shipping": {"eq"}, @@ -106,27 +106,27 @@ func Example(ast *epsearchast_v3.AstNode) error { // You can additionally create aliases which allows for one field to reference another: // In this case any headers that search for a field of `order_status` will be mapped to `status` and use those rules instead. - err = epsearchast_v3.ValidateAstFieldAndOperatorsWithAliases(ast, map[string][]string {"status": {"eq"}}, map[string]string {"order_status": "status"}) + err = epsearchast.ValidateAstFieldAndOperatorsWithAliases(ast, map[string][]string {"status": {"eq"}}, map[string]string {"order_status": "status"}) if err != nil { return err } // You can also supply validators on fields, which may be necessary in some cases depending on your data model or to improve user experience. // Validation is provided by the go-playground/validator package https://github.com/go-playground/validator#usage-and-documentation - err = epsearchast_v3.ValidateAstFieldAndOperatorsWithValueValidation(ast, map[string][]string {"status": {"eq"}}, map[string]string {"status": "oneof=incomplete complete processing cancelled"}) + err = epsearchast.ValidateAstFieldAndOperatorsWithValueValidation(ast, map[string][]string {"status": {"eq"}}, map[string]string {"status": "oneof=incomplete complete processing cancelled"}) if err != nil { return err } // Finally you can also restrict certain fields to types, which may be necessary in some cases depending on your data model or to improve user experience. - err = epsearchast_v3.ValidateAstFieldAndOperatorsWithFieldTypes(ast, map[string][]string {"with_tax": {"eq"}}, map[string]epsearchast_v3.FieldType{"with_tax": epsearchast_v3.Int64}) + err = epsearchast.ValidateAstFieldAndOperatorsWithFieldTypes(ast, map[string][]string {"with_tax": {"eq"}}, map[string]epsearchast.FieldType{"with_tax": epsearchast.Int64}) if err != nil { return err } - // All of these options together can be done with epsearchast_v3.ValidateAstFieldAndOperatorsWithAliasesAndValueValidationAndFieldTypes + // All of these options together can be done with epsearchast.ValidateAstFieldAndOperatorsWithAliasesAndValueValidationAndFieldTypes return err } ``` @@ -160,7 +160,7 @@ The library provides two approaches for processing AST trees: `ReduceAst()` and ```go // Example: Collect all field names from the AST -result, _ := epsearchast_v3.ReduceAst(ast, func(node *epsearchast_v3.AstNode, children []*[]string) (*[]string, error) { +result, _ := epsearchast.ReduceAst(ast, func(node *epsearchast.AstNode, children []*[]string) (*[]string, error) { fields := []string{} if len(node.Args) > 0 { fields = append(fields, node.Args[0]) @@ -180,8 +180,8 @@ result, _ := epsearchast_v3.ReduceAst(ast, func(node *epsearchast_v3.AstNode, ch ```go // Example: Generate a SQL query using GORM -var qb epsearchast_v3.SemanticReducer[epsearchast_v3_gorm.SubQuery] = epsearchast_v3_gorm.DefaultGormQueryBuilder{} -sq, err := epsearchast_v3.SemanticReduceAst(ast, qb) +var qb epsearchast.SemanticReducer[astgorm.SubQuery] = astgorm.DefaultGormQueryBuilder{} +sq, err := epsearchast.SemanticReduceAst(ast, qb) ``` **When to use which:** @@ -201,9 +201,9 @@ The library provides several utility functions for working with ASTs: Returns all first arguments (field names) from the AST. Useful for permission checking, index optimization, or field validation. ```go -fields := epsearchast_v3.GetAllFirstArgs(ast) // []string{"status", "amount", "status"} - includes duplicates -sortedFields := epsearchast_v3.GetAllFirstArgsSorted(ast) // []string{"amount", "status", "status"} - sorted -uniqueFields := epsearchast_v3.GetAllFirstArgsUnique(ast) // map[string]struct{}{"status": {}, "amount": {}} +fields := epsearchast.GetAllFirstArgs(ast) // []string{"status", "amount", "status"} - includes duplicates +sortedFields := epsearchast.GetAllFirstArgsSorted(ast) // []string{"amount", "status", "status"} - sorted +uniqueFields := epsearchast.GetAllFirstArgsUnique(ast) // map[string]struct{}{"status": {}, "amount": {}} ``` ##### HasFirstArg() @@ -211,7 +211,7 @@ uniqueFields := epsearchast_v3.GetAllFirstArgsUnique(ast) // map[string]struct{ Returns true if a specific field name appears anywhere in the AST. Useful for quickly checking if a field is referenced before performing expensive operations. ```go -hasStatus := epsearchast_v3.HasFirstArg(ast, "status") // true if "status" appears as a field name anywhere in the query +hasStatus := epsearchast.HasFirstArg(ast, "status") // true if "status" appears as a field name anywhere in the query ``` ##### GetAstDepth() @@ -219,7 +219,7 @@ hasStatus := epsearchast_v3.HasFirstArg(ast, "status") // true if "status" appe Returns the maximum depth of the AST tree. Useful for limiting query complexity. ```go -depth := epsearchast_v3.GetAstDepth(ast) +depth := epsearchast.GetAstDepth(ast) ``` ##### GetEffectiveIndexIntersectionCount() @@ -227,7 +227,7 @@ depth := epsearchast_v3.GetAstDepth(ast) Returns a heuristic measure of query complexity based on potential index intersections. Used internally to cap OR query complexity (default limit is 4). See the "OR Filter Restrictions" section for more details. ```go -count, err := epsearchast_v3.GetEffectiveIndexIntersectionCount(ast) +count, err := epsearchast.GetEffectiveIndexIntersectionCount(ast) ``` @@ -241,20 +241,20 @@ The following examples shows how to generate a Gorm query with this library. ```go package example -import epsearchast_v3 "github.com/elasticpath/epcc-search-ast-helper/external/epsearchast/v3" -import epsearchast_v3_gorm "github.com/elasticpath/epcc-search-ast-helper/external/epsearchast/v3/gorm" +import "github.com/elasticpath/epcc-search-ast-helper" +import "github.com/elasticpath/epcc-search-ast-helper/gorm" import "gorm.io/gorm" -func Example(ast *epsearchast_v3.AstNode, query *gorm.DB, tenantBoundaryId string) error { +func Example(ast *epsearchast.AstNode, query *gorm.DB, tenantBoundaryId string) error { var err error // Not Shown: Validation // Create query builder - var qb epsearchast_v3.SemanticReducer[epsearchast_v3_gorm.SubQuery] = epsearchast_v3_gorm.DefaultGormQueryBuilder{} + var qb epsearchast.SemanticReducer[astgorm.SubQuery] = astgorm.DefaultGormQueryBuilder{} - sq, err := epsearchast_v3.SemanticReduceAst(ast, qb) + sq, err := epsearchast.SemanticReduceAst(ast, qb) if err != nil { return err @@ -287,22 +287,22 @@ in this case, we want all eq queries for emails to use the lower case, compariso package example import ( - epsearchast_v3 "github.com/elasticpath/epcc-search-ast-helper/external/epsearchast/v3" + "github.com/elasticpath/epcc-search-ast-helper" "strconv" ) -import epsearchast_v3_gorm "github.com/elasticpath/epcc-search-ast-helper/external/epsearchast/v3/gorm" +import "github.com/elasticpath/epcc-search-ast-helper/gorm" import "gorm.io/gorm" -func Example(ast *epsearchast_v3.AstNode, query *gorm.DB, tenantBoundaryId string) error { +func Example(ast *epsearchast.AstNode, query *gorm.DB, tenantBoundaryId string) error { var err error // Not Shown: Validation // Create query builder - var qb epsearchast_v3.SemanticReducer[epsearchast_v3_gorm.SubQuery] = &CustomQueryBuilder{} + var qb epsearchast.SemanticReducer[astgorm.SubQuery] = &CustomQueryBuilder{} - sq, err := epsearchast_v3.SemanticReduceAst(ast, qb) + sq, err := epsearchast.SemanticReduceAst(ast, qb) if err != nil { return err @@ -316,12 +316,12 @@ func Example(ast *epsearchast_v3.AstNode, query *gorm.DB, tenantBoundaryId strin } type CustomQueryBuilder struct { - epsearchast_v3_gorm.DefaultGormQueryBuilder + astgorm.DefaultGormQueryBuilder } -func (l *CustomQueryBuilder) VisitEq(first, second string) (*epsearchast_v3_gorm.SubQuery, error) { +func (l *CustomQueryBuilder) VisitEq(first, second string) (*astgorm.SubQuery, error) { if first == "email" { - return &epsearchast_v3_gorm.SubQuery{ + return &astgorm.SubQuery{ Clause: fmt.Sprintf("LOWER(%s::text) = LOWER(?)", first), Args: []interface{}{second}, }, nil @@ -330,7 +330,7 @@ func (l *CustomQueryBuilder) VisitEq(first, second string) (*epsearchast_v3_gorm if err != nil { return nil, err } - return &epsearchast_v3_gorm.SubQuery{ + return &astgorm.SubQuery{ Clause: fmt.Sprintf("%s = ?", first), Args: []interface{}{n}, }, nil @@ -349,20 +349,20 @@ package example import ( "context" - epsearchast_v3 "github.com/elasticpath/epcc-search-ast-helper/external/epsearchast/v3" - epsearchast_v3_mongo "github.com/elasticpath/epcc-search-ast-helper/external/epsearchast/v3/mongo" - "go.mongodb.org/mongo-driver/bson" - "go.mongodb.org/mongo-driver/mongo" + "github.com/elasticpath/epcc-search-ast-helper" + "github.com/elasticpath/epcc-search-ast-helper/mongo" + "go.mongodb.org/mongo-driver/v2/bson" + "go.mongodb.org/mongo-driver/v2/mongo" ) -func Example(ast *epsearchast_v3.AstNode, collection *mongo.Collection, tenantBoundaryQuery bson.M) (*mongo.Cursor, error) { +func Example(ast *epsearchast.AstNode, collection *mongo.Collection, tenantBoundaryQuery bson.M) (*mongo.Cursor, error) { // Not Shown: Validation // Create query builder - var qb epsearchast_v3.SemanticReducer[bson.D] = DefaultMongoQueryBuilder{} + var qb epsearchast.SemanticReducer[bson.D] = DefaultMongoQueryBuilder{} // Create Query Object - queryObj, err := epsearchast_v3.SemanticReduceAst(ast, qb) + queryObj, err := epsearchast.SemanticReduceAst(ast, qb) if err != nil { return nil, err @@ -399,23 +399,23 @@ package example import ( "context" - epsearchast_v3 "github.com/elasticpath/epcc-search-ast-helper/external/epsearchast/v3" - epsearchast_v3_mongo "github.com/elasticpath/epcc-search-ast-helper/external/epsearchast/v3/mongo" - "go.mongodb.org/mongo-driver/bson" - "go.mongodb.org/mongo-driver/mongo" + "github.com/elasticpath/epcc-search-ast-helper" + "github.com/elasticpath/epcc-search-ast-helper/mongo" + "go.mongodb.org/mongo-driver/v2/bson" + "go.mongodb.org/mongo-driver/v2/mongo" "strings" ) -func Example(ast *epsearchast_v3.AstNode, collection *mongo.Collection, tenantBoundaryQuery *bson.M) (*mongo.Cursor, error) { +func Example(ast *epsearchast.AstNode, collection *mongo.Collection, tenantBoundaryQuery *bson.M) (*mongo.Cursor, error) { // Not Shown: Validation // Create query builder - var qb epsearchast_v3.SemanticReducer[bson.D] = &epsearchast_v3_mongo.DefaultMongoQueryBuilder{ - FieldTypes: map[string]epsearchast_v3_mongo.FieldType{"with_tax": epsearchast_v3_mongo.Int64}, + var qb epsearchast.SemanticReducer[bson.D] = &astmongo.DefaultMongoQueryBuilder{ + FieldTypes: map[string]astmongo.FieldType{"with_tax": astmongo.Int64}, } // Create Query Object - queryObj, err := epsearchast_v3.SemanticReduceAst(ast, qb) + queryObj, err := epsearchast.SemanticReduceAst(ast, qb) if err != nil { return nil, err @@ -443,21 +443,21 @@ package example import ( "context" - epsearchast_v3 "github.com/elasticpath/epcc-search-ast-helper/external/epsearchast/v3" - epsearchast_v3_mongo "github.com/elasticpath/epcc-search-ast-helper/external/epsearchast/v3/mongo" - "go.mongodb.org/mongo-driver/bson" - "go.mongodb.org/mongo-driver/mongo" + "github.com/elasticpath/epcc-search-ast-helper" + "github.com/elasticpath/epcc-search-ast-helper/mongo" + "go.mongodb.org/mongo-driver/v2/bson" + "go.mongodb.org/mongo-driver/v2/mongo" "strings" ) -func Example(ast *epsearchast_v3.AstNode, collection *mongo.Collection, tenantBoundaryQuery *bson.M) (*mongo.Cursor, error) { +func Example(ast *epsearchast.AstNode, collection *mongo.Collection, tenantBoundaryQuery *bson.M) (*mongo.Cursor, error) { // Not Shown: Validation // Create query builder - var qb epsearchast_v3.SemanticReducer[bson.D] = &LowerCaseEmail{} + var qb epsearchast.SemanticReducer[bson.D] = &LowerCaseEmail{} // Create Query Object - queryObj, err := epsearchast_v3.SemanticReduceAst(ast, qb) + queryObj, err := epsearchast.SemanticReduceAst(ast, qb) if err != nil { return nil, err @@ -475,7 +475,7 @@ func Example(ast *epsearchast_v3.AstNode, collection *mongo.Collection, tenantBo } type LowerCaseEmail struct { - epsearchast_v3_mongo.DefaultMongoQueryBuilder + astmongo.DefaultMongoQueryBuilder } func (l *LowerCaseEmail) VisitEq(first, second string) (*bson.D, error) { @@ -495,13 +495,13 @@ The following examples shows how to generate an Elasticsearch Query with this li ```go package example -import epsearchast_v3 "github.com/elasticpath/epcc-search-ast-helper/external/epsearchast/v3" -import epsearchast_v3_es "github.com/elasticpath/epcc-search-ast-helper/external/epsearchast/v3/els" +import "github.com/elasticpath/epcc-search-ast-helper" +import "github.com/elasticpath/epcc-search-ast-helper/els" var qb = &LowerCaseEmail{ - epsearchast_v3_es.DefaultEsQueryBuilder{ - OpTypeToFieldNames: map[string]*epsearchast_v3_es.OperatorTypeToMultiFieldName{ + astes.DefaultEsQueryBuilder{ + OpTypeToFieldNames: map[string]*astes.OperatorTypeToMultiFieldName{ "status": { Wildcard: "status.wildcard", }, @@ -515,12 +515,12 @@ func init() { qb.MustValidate() } -func Example(ast *epsearchast_v3.AstNode, tenantBoundaryId string) (string, error) { +func Example(ast *epsearchast.AstNode, tenantBoundaryId string) (string, error) { // Not Shown: Validation // Create Query Object - query, err := epsearchast_v3.SemanticReduceAst[epsearchast_v3_es.JsonObject](astNode, qb) + query, err := astes.SemanticReduceAst[astes.JsonObject](astNode, qb) if err != nil { return nil, err @@ -532,14 +532,14 @@ func Example(ast *epsearchast_v3.AstNode, tenantBoundaryId string) (string, err } type LowerCaseEmail struct { - epsearchast_v3_es.DefaultEsQueryBuilder + astes.DefaultEsQueryBuilder } -func (l *LowerCaseEmail) VisitEq(first, second string) (*epsearchast_v3_es.JsonObject, error) { +func (l *LowerCaseEmail) VisitEq(first, second string) (*astes.JsonObject, error) { if first == "email" { - return epsearchast_v3_es.DefaultEsQueryBuilder.VisitEq(l.DefaultEsQueryBuilder, first, strings.ToLower(second)) + return astes.DefaultEsQueryBuilder.VisitEq(l.DefaultEsQueryBuilder, first, strings.ToLower(second)) } else { - return epsearchast_v3_es.DefaultEsQueryBuilder.VisitEq(l.DefaultEsQueryBuilder, first, second) + return astes.DefaultEsQueryBuilder.VisitEq(l.DefaultEsQueryBuilder, first, second) } } @@ -582,6 +582,157 @@ The Elasticsearch Query Builder has a couple of family of methods that can be ov In Mongo and Postgres there is a near 1-1 translation between an AST node and a query. In Elasticsearch, due to [Nested Queries](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-nested-query.html) the mapping is not 1-to-1, due to visiting a nested field. If you need to override behaviour pertaining to a nested field, the `Get____QueryBuilder()` functions are probably where the override should happen, otherwise `Visit____()` might be simpler. +#### MongoDB Atlas Search (Beta) + +The following example shows how to generate a MongoDB Atlas Search query with this library. + +**Note**: MongoDB Atlas Search support is currently in beta. Some operators are not yet implemented. + +```go +package example + +import ( + "context" + epsearchast_v3 "github.com/elasticpath/epcc-search-ast-helper/external/epsearchast/v3" + epsearchast_v3_mongo "github.com/elasticpath/epcc-search-ast-helper/external/epsearchast/v3/mongo" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" +) + +func Example(ast *epsearchast_v3.AstNode, collection *mongo.Collection, tenantBoundaryId string) (*mongo.Cursor, error) { + // Not Shown: Validation + + // Create Atlas Search query builder + // Configure multi-analyzers for fields that support LIKE/ILIKE + var qb epsearchast_v3.SemanticReducer[bson.D] = epsearchast_v3_mongo.DefaultAtlasSearchQueryBuilder{ + FieldToMultiAnalyzers: map[string]*epsearchast_v3_mongo.StringMultiAnalyzers{ + "name": { + WildcardCaseInsensitive: "caseInsensitiveAnalyzer", + WildcardCaseSensitive: "caseSensitiveAnalyzer", + }, + "email": { + WildcardCaseInsensitive: "caseInsensitiveAnalyzer", + WildcardCaseSensitive: "caseSensitiveAnalyzer", + }, + }, + } + + // Create Query Object + query, err := epsearchast_v3.SemanticReduceAst(ast, qb) + + if err != nil { + return nil, err + } + + // Execute the search using aggregation pipeline + pipeline := mongo.Pipeline{ + {{Key: "$search", Value: query}}, + // Don't forget to add additional filters using $match stage if needed + {{Key: "$match", Value: bson.D{{"tenant_boundary_id", tenantBoundaryId}}}}, + } + + return collection.Aggregate(context.TODO(), pipeline) +} +``` + +##### Supported Operators + +The following operators are currently supported: +- `text` - Full-text search with analyzers +- `eq` - Exact case-sensitive equality matching (string fields only) +- `in` - Multiple value exact matching (string fields only) +- `like` - Case-sensitive wildcard matching +- `ilike` - Case-insensitive wildcard matching +- `gt` - Greater than (lexicographic comparison for strings) +- `ge` - Greater than or equal (lexicographic comparison for strings) +- `lt` - Less than (lexicographic comparison for strings) +- `le` - Less than or equal (lexicographic comparison for strings) + +##### Field Configuration + +###### Multi-Analyzer Configuration for LIKE/ILIKE + +To support `like` and `ilike` operators with proper case sensitivity handling, you need to: + +1. **Define custom analyzers in your search index** with appropriate tokenization and case handling +2. **Configure multi-analyzers on your string fields** to index the same field with different analyzers +3. **Map fields to analyzer names** in the query builder using `FieldToMultiAnalyzers` + +**Example Search Index Definition:** + +```json +{ + "analyzers": [ + { + "name": "caseInsensitiveAnalyzer", + "tokenizer": { + "type": "keyword" + }, + "tokenFilters": [ + { + "type": "lowercase" + } + ] + } + ], + "mappings": { + "dynamic": false, + "fields": { + "name": [ + { + "type": "string", + "analyzer": "lucene.standard", + "multi": { + "caseInsensitiveAnalyzer": { + "type": "string", + "analyzer": "caseInsensitiveAnalyzer" + }, + "caseSensitiveAnalyzer": { + "type": "string", + "analyzer": "lucene.keyword" + } + } + }, + { + "type": "token" + } + ] + } + } +} +``` + +**Query Builder Configuration:** + +The `FieldToMultiAnalyzers` map specifies which multi-analyzer to use for each field: + +```go +FieldToMultiAnalyzers: map[string]*StringMultiAnalyzers{ + "name": { + WildcardCaseInsensitive: "caseInsensitiveAnalyzer", // Used for ILIKE + WildcardCaseSensitive: "caseSensitiveAnalyzer", // Used for LIKE + }, +} +``` + +**Behavior:** +If a field is **not** in `FieldToMultiAnalyzers`, if you specify a non empty analyzer, then a "multi" attribute is generated with the name (e.g., `{"path": {"value": "fieldName", "multi": "analyzerName"}}`) + +This allows you to mix fields with and without multi-analyzer support in the same index. + +##### Limitations + +1. The following operators are not yet implemented: `contains`, `contains_any`, `contains_all`, `is_null` +2. The following field types are not currently supported: UUID fields, Date fields, Numeric fields (numbers are compared as strings) +3. Range operators (`gt`, `ge`, `lt`, `le`) perform lexicographic comparison on string fields only +4. Atlas Search requires proper [search index configuration](https://www.mongodb.com/docs/atlas/atlas-search/create-index/) with appropriate field types: + - String fields used with `like`/`ilike` should be indexed with multi-analyzers as shown above + - String fields used with `eq`/`in` should be indexed with `token` type + - String fields used with range operators (`gt`/`ge`/`lt`/`le`) work with `token` type for lexicographic comparison + - Text fields should be indexed with `string` type and an appropriate analyzer +5. Unlike regular MongoDB queries, Atlas Search queries use the aggregation pipeline with the `$search` stage +6. Additional filters (like tenant boundaries) should be added as separate `$match` stages in the pipeline + ### FAQ #### Design diff --git a/external/epsearchast/v3/aliases.go b/aliases.go similarity index 98% rename from external/epsearchast/v3/aliases.go rename to aliases.go index ff136a1..7e7a7d3 100644 --- a/external/epsearchast/v3/aliases.go +++ b/aliases.go @@ -1,4 +1,4 @@ -package epsearchast_v3 +package epsearchast import "regexp" diff --git a/external/epsearchast/v3/aliases_test.go b/aliases_test.go similarity index 99% rename from external/epsearchast/v3/aliases_test.go rename to aliases_test.go index 1a8c2e3..409951c 100644 --- a/external/epsearchast/v3/aliases_test.go +++ b/aliases_test.go @@ -1,4 +1,4 @@ -package epsearchast_v3 +package epsearchast import ( "github.com/stretchr/testify/require" diff --git a/external/epsearchast/v3/ast.go b/ast.go similarity index 96% rename from external/epsearchast/v3/ast.go rename to ast.go index 6e550c1..054a91c 100644 --- a/external/epsearchast/v3/ast.go +++ b/ast.go @@ -1,5 +1,5 @@ -// Package epsearchast_v3 implements structs and functions for working with the EP-Internal-Search-AST-v3 header. -package epsearchast_v3 +// Package epsearchast implements structs and functions for working with the EP-Internal-Search-AST-v3 header. +package epsearchast import ( "encoding/json" @@ -102,7 +102,7 @@ func GetAst(jsonTxt string) (*AstNode, error) { // The AstVisitor interface provides a way of specifying a [Visitor] for visiting an AST. // -// This interface is clunky to use for conversions or when you need to return state, and you should use [epsearchast_v3.ReduceAst] instead. +// This interface is clunky to use for conversions or when you need to return state, and you should use [epsearchast.ReduceAst] instead. // In particular because the return values are restricted to error, you need to manage and combine the state yourself, which can be more annoying than necessary. // // [Visitor]: https://en.wikipedia.org/wiki/Visitor_pattern diff --git a/external/epsearchast/v3/ast_test.go b/ast_test.go similarity index 99% rename from external/epsearchast/v3/ast_test.go rename to ast_test.go index 211a3aa..db28e3f 100644 --- a/external/epsearchast/v3/ast_test.go +++ b/ast_test.go @@ -1,4 +1,4 @@ -package epsearchast_v3 +package epsearchast import ( "github.com/stretchr/testify/require" diff --git a/external/epsearchast/v3/ast_visitor_test.go b/ast_visitor_test.go similarity index 99% rename from external/epsearchast/v3/ast_visitor_test.go rename to ast_visitor_test.go index e45a28c..d34016a 100644 --- a/external/epsearchast/v3/ast_visitor_test.go +++ b/ast_visitor_test.go @@ -1,4 +1,4 @@ -package epsearchast_v3 +package epsearchast import ( "fmt" diff --git a/docker-compose.yml b/docker-compose.yml index 8117542..8121963 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,8 +1,12 @@ +volumes: + mongo-community: + mongo-community-search: + services: postgres: image: postgres:15.0-bullseye ports: - - '20001:5432' + - '127.0.0.1:20001:5432' environment: POSTGRES_PASSWORD: admin POSTGRES_USER: admin @@ -13,12 +17,12 @@ services: timeout: 5s retries: 5 mongo: - image: mongo:7.0 + image: mongodb/mongodb-community-server:8.2.1-ubi8 ports: - - '20002:27017' + - '127.0.0.1:20002:27017' environment: - MONGO_INITDB_ROOT_USERNAME: admin - MONGO_INITDB_ROOT_PASSWORD: admin + MONGODB_INITDB_ROOT_USERNAME: admin + MONGODB_INITDB_ROOT_PASSWORD: admin healthcheck: test: [ "CMD-SHELL", "echo 'db.runCommand({ ping: 1 })' | mongosh mongodb://localhost:27017/test_db --quiet" ] interval: 10s @@ -28,7 +32,7 @@ services: elasticsearch: image: elasticsearch:7.17.25 ports: - - '20003:9200' + - '127.0.0.1:20003:9200' environment: discovery.type: single-node ES_JAVA_OPTS: "-Xms1024m -Xmx1024m" @@ -38,3 +42,155 @@ services: timeout: 5s retries: 5 + + mongo-community: + container_name: search-ast-helper-mongo-community + hostname: mongo-community + extra_hosts: + # We override the hostname for mongo to point to localhost. + # Because in the init mode mongo is only available to localhost. + # But we need the replicaset to be configured with a name other containers can see. + # https://github.com/docker-library/mongo/issues/339#issuecomment-2253159258 + - "mongo-community:127.0.0.1" + #image: mongodb/mongodb-community-server:8.2.1-ubi8 + image: mongo:8.2.1 + command: + - "--bind_ip_all" + - "--replSet" + - "rs0" + - "--setParameter" + - "mongotHost=mongo-community-search:27027" + - "--setParameter" + - "searchIndexManagementHostAndPort=mongo-community-search:27027" + - "--setParameter" + - "skipAuthenticationToSearchIndexManagementServer=false" + - "--setParameter" + - "useGrpcForSearch=true" + - "--keyFile" + - "/keyfile" + - "--auth" + ports: + - '127.0.0.1:20004:27017' + volumes: + - mongo-community:/data/db:delegated + configs: + - source: mongo-rs-config.js + target: /docker-entrypoint-initdb.d/mongo-rs-config.js + - source: mongo-user-config.js + target: /docker-entrypoint-initdb.d/mongo-user-config.js + - source: keyfile + target: /keyfile + mode: 0400 + uid: "999" + gid: "999" + healthcheck: + test: > + mongosh --quiet "localhost/test" --eval 'quit(db.runCommand({ ping: 1 }).ok ? 0 : 2)' + interval: 10s + timeout: 5s + retries: 10 + start_period: 40s + + mongo-community-search: + container_name: search-ast-helper-mongo-community-search + hostname: mongo-community-search + image: mongodb/mongodb-community-search:0.55.0 + ports: + - '127.0.0.1:20005:27027' + volumes: + - mongo-community-search:/data/mongot:delegated + configs: + - source: config.default.yml + target: /mongot-community/config.default.yml + - source: passwordFile + target: /etc/mongot/secrets/passwordFile + mode: 0400 + uid: "999" + gid: "999" + stop_grace_period: 1s + depends_on: + mongo-community: + condition: service_healthy + + + +configs: + mongo-rs-config.js: + # language=javascript + content: | + print("\n\n\nStarting Replica Set Configuration\n\n\n"); + rs.initiate({ + _id: "rs0", + members: [ + { _id: 0, host: `mongo-community:27017` } + ] + }); + + while (!rs.status().members.some(m => m.stateStr === "PRIMARY")) { + print("Waiting for primary..."); + sleep(1000); // sleep 1 second + } + + print("\n\n\nReplica Set Configuration Completed\n\n\n"); + + mongo-user-config.js: + # language=javascript + content: | + print("\n\n\nCreating Users\n\n\n"); + var db = db.getSiblingDB("admin"); + db.createUser( + { + user: "admin", + pwd: "admin", + mechanisms: ["SCRAM-SHA-256"], + roles: [ + { + role: "root", + db: "admin" + } + ] + }, + { + w: "majority", + wtimeout: 5000 + } + ); + + db.createUser( + { + user: "search", + pwd: "search", + mechanisms: ["SCRAM-SHA-256"], + roles: [ "searchCoordinator" ] + } + ); + + print("\n\n\nUsers created\n\n\n"); + keyfile: + content: | + helloworld + passwordFile: + content: "search" + config.default.yml: + # language=yaml + content: | + syncSource: + replicaSet: + hostAndPort: "mongo-community:27017" + username: "search" + passwordFile: "/etc/mongot/secrets/passwordFile" + tls: false + storage: + dataPath: "/data/mongot" + server: + grpc: + address: "0.0.0.0:27027" + tls: + mode: "disabled" + metrics: + enabled: true + address: "0.0.0.0:9946" + healthCheck: + address: "0.0.0.0:8080" + logging: + verbosity: INFO \ No newline at end of file diff --git a/external/epsearchast/v3/errors.go b/errors.go similarity index 95% rename from external/epsearchast/v3/errors.go rename to errors.go index d518398..ae0aedb 100644 --- a/external/epsearchast/v3/errors.go +++ b/errors.go @@ -1,4 +1,4 @@ -package epsearchast_v3 +package epsearchast import "fmt" diff --git a/external/epsearchast/v3/es/es_query_builder.go b/es/es_query_builder.go similarity index 98% rename from external/epsearchast/v3/es/es_query_builder.go rename to es/es_query_builder.go index 91ba5a4..2e8d39b 100644 --- a/external/epsearchast/v3/es/es_query_builder.go +++ b/es/es_query_builder.go @@ -1,11 +1,12 @@ -package epsearchast_v3_es +package astes import ( "fmt" - epsearchast_v3 "github.com/elasticpath/epcc-search-ast-helper/external/epsearchast/v3" "regexp" "sort" "strings" + + "github.com/elasticpath/epcc-search-ast-helper" ) type JsonObject map[string]any @@ -150,7 +151,7 @@ func sortByDecreasingLength(groupKeys []string) { }) } -var _ epsearchast_v3.SemanticReducer[JsonObject] = (*DefaultEsQueryBuilder)(nil) +var _ epsearchast.SemanticReducer[JsonObject] = (*DefaultEsQueryBuilder)(nil) func (d DefaultEsQueryBuilder) PostVisitAnd(rs []*JsonObject) (*JsonObject, error) { return &JsonObject{ diff --git a/external/epsearchast/v3/es/es_query_builder_int_test.go b/es/es_query_builder_int_test.go similarity index 98% rename from external/epsearchast/v3/es/es_query_builder_int_test.go rename to es/es_query_builder_int_test.go index 8eed2bb..0189735 100644 --- a/external/epsearchast/v3/es/es_query_builder_int_test.go +++ b/es/es_query_builder_int_test.go @@ -1,14 +1,15 @@ -package epsearchast_v3_es +package astes import ( "bytes" "encoding/json" "fmt" - epsearchast_v3 "github.com/elasticpath/epcc-search-ast-helper/external/epsearchast/v3" "io/ioutil" "net/http" "os" "testing" + + "github.com/elasticpath/epcc-search-ast-helper" ) const esBaseURL = "http://localhost:20003" @@ -24,7 +25,7 @@ type testStruct struct { func (t *testStruct) String() string { - ast, err := epsearchast_v3.GetAst(t.filter) + ast, err := epsearchast.GetAst(t.filter) if err != nil { panic(fmt.Sprintf("Failed to get filter: %s)", err)) @@ -1048,12 +1049,12 @@ func TestSmokeTestElasticSearchWithFilters(t *testing.T) { } // Build Elasticsearch query - ast, err := epsearchast_v3.GetAst(tc.filter) + ast, err := epsearchast.GetAst(tc.filter) if err != nil { t.Fatalf("Failed to parse filter: %v", err) } - var qb epsearchast_v3.SemanticReducer[JsonObject] = DefaultEsQueryBuilder{ + var qb epsearchast.SemanticReducer[JsonObject] = DefaultEsQueryBuilder{ OpTypeToFieldNames: map[string]*OperatorTypeToMultiFieldName{ "key_value_field.description": { Wildcard: "key_value_field.description.wildcard", @@ -1082,7 +1083,7 @@ func TestSmokeTestElasticSearchWithFilters(t *testing.T) { DefaultFuzziness: "AUTO", } - query, err := epsearchast_v3.SemanticReduceAst(ast, qb) + query, err := epsearchast.SemanticReduceAst(ast, qb) if err != nil { t.Fatalf("Failed to reduce AST: %v", err) } diff --git a/external/epsearchast/v3/es/es_query_builder_test.go b/es/es_query_builder_test.go similarity index 72% rename from external/epsearchast/v3/es/es_query_builder_test.go rename to es/es_query_builder_test.go index e6d0762..69f7edf 100644 --- a/external/epsearchast/v3/es/es_query_builder_test.go +++ b/es/es_query_builder_test.go @@ -1,14 +1,15 @@ -package epsearchast_v3_es_test +package astes import ( "encoding/json" "fmt" - epsearchast_v3 "github.com/elasticpath/epcc-search-ast-helper/external/epsearchast/v3" - epsearchast_v3_es "github.com/elasticpath/epcc-search-ast-helper/external/epsearchast/v3/es" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" "strings" "testing" + + "github.com/elasticpath/epcc-search-ast-helper" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestSimpleBinaryEqOperatorGeneratesCorrectQuery(t *testing.T) { @@ -30,13 +31,13 @@ func TestSimpleBinaryEqOperatorGeneratesCorrectQuery(t *testing.T) { } }` - astNode, err := epsearchast_v3.GetAst(jsonTxt) + astNode, err := epsearchast.GetAst(jsonTxt) require.NoError(t, err) - var qb epsearchast_v3.SemanticReducer[epsearchast_v3_es.JsonObject] = epsearchast_v3_es.DefaultEsQueryBuilder{} + var qb epsearchast.SemanticReducer[JsonObject] = DefaultEsQueryBuilder{} // Execute SUT - query, err := epsearchast_v3.SemanticReduceAst(astNode, qb) + query, err := epsearchast.SemanticReduceAst(astNode, qb) require.NoError(t, err) // Verification @@ -65,11 +66,11 @@ func TestSimpleBinaryEqOperatorGeneratesCorrectQueryWithFieldOverride(t *testing } }` - astNode, err := epsearchast_v3.GetAst(jsonTxt) + astNode, err := epsearchast.GetAst(jsonTxt) require.NoError(t, err) - var qb epsearchast_v3.SemanticReducer[epsearchast_v3_es.JsonObject] = epsearchast_v3_es.DefaultEsQueryBuilder{ - OpTypeToFieldNames: map[string]*epsearchast_v3_es.OperatorTypeToMultiFieldName{ + var qb epsearchast.SemanticReducer[JsonObject] = DefaultEsQueryBuilder{ + OpTypeToFieldNames: map[string]*OperatorTypeToMultiFieldName{ "email": { Equality: "email.keyword", }, @@ -77,7 +78,7 @@ func TestSimpleBinaryEqOperatorGeneratesCorrectQueryWithFieldOverride(t *testing } // Execute SUT - query, err := epsearchast_v3.SemanticReduceAst(astNode, qb) + query, err := epsearchast.SemanticReduceAst(astNode, qb) require.NoError(t, err) // Verification @@ -108,13 +109,13 @@ func TestSimpleBinaryLeOperatorGeneratesCorrectQuery(t *testing.T) { } }` - astNode, err := epsearchast_v3.GetAst(jsonTxt) + astNode, err := epsearchast.GetAst(jsonTxt) require.NoError(t, err) - var qb epsearchast_v3.SemanticReducer[epsearchast_v3_es.JsonObject] = epsearchast_v3_es.DefaultEsQueryBuilder{} + var qb epsearchast.SemanticReducer[JsonObject] = DefaultEsQueryBuilder{} // Execute SUT - query, err := epsearchast_v3.SemanticReduceAst(astNode, qb) + query, err := epsearchast.SemanticReduceAst(astNode, qb) require.NoError(t, err) // Verification @@ -145,11 +146,11 @@ func TestSimpleBinaryLeOperatorGeneratesCorrectQueryWithFieldOverride(t *testing } }` - astNode, err := epsearchast_v3.GetAst(jsonTxt) + astNode, err := epsearchast.GetAst(jsonTxt) require.NoError(t, err) - var qb epsearchast_v3.SemanticReducer[epsearchast_v3_es.JsonObject] = epsearchast_v3_es.DefaultEsQueryBuilder{ - OpTypeToFieldNames: map[string]*epsearchast_v3_es.OperatorTypeToMultiFieldName{ + var qb epsearchast.SemanticReducer[JsonObject] = DefaultEsQueryBuilder{ + OpTypeToFieldNames: map[string]*OperatorTypeToMultiFieldName{ "amount": { Relational: "amount.range", }, @@ -157,7 +158,7 @@ func TestSimpleBinaryLeOperatorGeneratesCorrectQueryWithFieldOverride(t *testing } // Execute SUT - query, err := epsearchast_v3.SemanticReduceAst(astNode, qb) + query, err := epsearchast.SemanticReduceAst(astNode, qb) require.NoError(t, err) // Verification @@ -188,13 +189,13 @@ func TestSimpleBinaryLtOperatorGeneratesCorrectQuery(t *testing.T) { } }` - astNode, err := epsearchast_v3.GetAst(jsonTxt) + astNode, err := epsearchast.GetAst(jsonTxt) require.NoError(t, err) - var qb epsearchast_v3.SemanticReducer[epsearchast_v3_es.JsonObject] = epsearchast_v3_es.DefaultEsQueryBuilder{} + var qb epsearchast.SemanticReducer[JsonObject] = DefaultEsQueryBuilder{} // Execute SUT - query, err := epsearchast_v3.SemanticReduceAst(astNode, qb) + query, err := epsearchast.SemanticReduceAst(astNode, qb) require.NoError(t, err) // Verification @@ -225,11 +226,11 @@ func TestSimpleBinaryLtOperatorGeneratesCorrectQueryWithFieldOverride(t *testing } }` - astNode, err := epsearchast_v3.GetAst(jsonTxt) + astNode, err := epsearchast.GetAst(jsonTxt) require.NoError(t, err) - var qb epsearchast_v3.SemanticReducer[epsearchast_v3_es.JsonObject] = epsearchast_v3_es.DefaultEsQueryBuilder{ - OpTypeToFieldNames: map[string]*epsearchast_v3_es.OperatorTypeToMultiFieldName{ + var qb epsearchast.SemanticReducer[JsonObject] = DefaultEsQueryBuilder{ + OpTypeToFieldNames: map[string]*OperatorTypeToMultiFieldName{ "amount": { Relational: "amount.range", }, @@ -237,7 +238,7 @@ func TestSimpleBinaryLtOperatorGeneratesCorrectQueryWithFieldOverride(t *testing } // Execute SUT - query, err := epsearchast_v3.SemanticReduceAst(astNode, qb) + query, err := epsearchast.SemanticReduceAst(astNode, qb) require.NoError(t, err) // Verification @@ -268,13 +269,13 @@ func TestSimpleBinaryGtOperatorGeneratesCorrectQuery(t *testing.T) { } }` - astNode, err := epsearchast_v3.GetAst(jsonTxt) + astNode, err := epsearchast.GetAst(jsonTxt) require.NoError(t, err) - var qb epsearchast_v3.SemanticReducer[epsearchast_v3_es.JsonObject] = epsearchast_v3_es.DefaultEsQueryBuilder{} + var qb epsearchast.SemanticReducer[JsonObject] = DefaultEsQueryBuilder{} // Execute SUT - query, err := epsearchast_v3.SemanticReduceAst(astNode, qb) + query, err := epsearchast.SemanticReduceAst(astNode, qb) require.NoError(t, err) // Verification @@ -305,11 +306,11 @@ func TestSimpleBinaryGtOperatorGeneratesCorrectQueryWithFieldOverride(t *testing } }` - astNode, err := epsearchast_v3.GetAst(jsonTxt) + astNode, err := epsearchast.GetAst(jsonTxt) require.NoError(t, err) - var qb epsearchast_v3.SemanticReducer[epsearchast_v3_es.JsonObject] = epsearchast_v3_es.DefaultEsQueryBuilder{ - OpTypeToFieldNames: map[string]*epsearchast_v3_es.OperatorTypeToMultiFieldName{ + var qb epsearchast.SemanticReducer[JsonObject] = DefaultEsQueryBuilder{ + OpTypeToFieldNames: map[string]*OperatorTypeToMultiFieldName{ "amount": { Relational: "amount.range", }, @@ -317,7 +318,7 @@ func TestSimpleBinaryGtOperatorGeneratesCorrectQueryWithFieldOverride(t *testing } // Execute SUT - query, err := epsearchast_v3.SemanticReduceAst(astNode, qb) + query, err := epsearchast.SemanticReduceAst(astNode, qb) require.NoError(t, err) // Verification @@ -348,13 +349,13 @@ func TestSimpleBinaryGEOperatorGeneratesCorrectQuery(t *testing.T) { } }` - astNode, err := epsearchast_v3.GetAst(jsonTxt) + astNode, err := epsearchast.GetAst(jsonTxt) require.NoError(t, err) - var qb epsearchast_v3.SemanticReducer[epsearchast_v3_es.JsonObject] = epsearchast_v3_es.DefaultEsQueryBuilder{} + var qb epsearchast.SemanticReducer[JsonObject] = DefaultEsQueryBuilder{} // Execute SUT - query, err := epsearchast_v3.SemanticReduceAst(astNode, qb) + query, err := epsearchast.SemanticReduceAst(astNode, qb) require.NoError(t, err) // Verification @@ -385,11 +386,11 @@ func TestSimpleBinaryGEOperatorGeneratesCorrectQueryWithFieldOverride(t *testing } }` - astNode, err := epsearchast_v3.GetAst(jsonTxt) + astNode, err := epsearchast.GetAst(jsonTxt) require.NoError(t, err) - var qb epsearchast_v3.SemanticReducer[epsearchast_v3_es.JsonObject] = epsearchast_v3_es.DefaultEsQueryBuilder{ - OpTypeToFieldNames: map[string]*epsearchast_v3_es.OperatorTypeToMultiFieldName{ + var qb epsearchast.SemanticReducer[JsonObject] = DefaultEsQueryBuilder{ + OpTypeToFieldNames: map[string]*OperatorTypeToMultiFieldName{ "amount": { Relational: "amount.range", }, @@ -397,7 +398,7 @@ func TestSimpleBinaryGEOperatorGeneratesCorrectQueryWithFieldOverride(t *testing } // Execute SUT - query, err := epsearchast_v3.SemanticReduceAst(astNode, qb) + query, err := epsearchast.SemanticReduceAst(astNode, qb) require.NoError(t, err) // Verification @@ -429,13 +430,13 @@ func TestSimpleBinaryLikeOperatorGeneratesCorrectQuery(t *testing.T) { } }` - astNode, err := epsearchast_v3.GetAst(jsonTxt) + astNode, err := epsearchast.GetAst(jsonTxt) require.NoError(t, err) - var qb epsearchast_v3.SemanticReducer[epsearchast_v3_es.JsonObject] = epsearchast_v3_es.DefaultEsQueryBuilder{} + var qb epsearchast.SemanticReducer[JsonObject] = DefaultEsQueryBuilder{} // Execute SUT - query, err := epsearchast_v3.SemanticReduceAst(astNode, qb) + query, err := epsearchast.SemanticReduceAst(astNode, qb) require.NoError(t, err) // Verification @@ -467,11 +468,11 @@ func TestSimpleBinaryLikeOperatorGeneratesCorrectQueryWithFieldOverride(t *testi } }` - astNode, err := epsearchast_v3.GetAst(jsonTxt) + astNode, err := epsearchast.GetAst(jsonTxt) require.NoError(t, err) - var qb epsearchast_v3.SemanticReducer[epsearchast_v3_es.JsonObject] = epsearchast_v3_es.DefaultEsQueryBuilder{ - OpTypeToFieldNames: map[string]*epsearchast_v3_es.OperatorTypeToMultiFieldName{ + var qb epsearchast.SemanticReducer[JsonObject] = DefaultEsQueryBuilder{ + OpTypeToFieldNames: map[string]*OperatorTypeToMultiFieldName{ "email": { Wildcard: "email.keyword", }, @@ -479,7 +480,7 @@ func TestSimpleBinaryLikeOperatorGeneratesCorrectQueryWithFieldOverride(t *testi } // Execute SUT - query, err := epsearchast_v3.SemanticReduceAst(astNode, qb) + query, err := epsearchast.SemanticReduceAst(astNode, qb) require.NoError(t, err) // Verification @@ -528,13 +529,13 @@ func TestSimpleBinaryLikeOperatorGeneratesCorrectQueryWithWildcards(t *testing.T } }`, strings.ReplaceAll(tc.expectedWildcardTerm, `\`, `\\`)) - astNode, err := epsearchast_v3.GetAst(jsonTxt) + astNode, err := epsearchast.GetAst(jsonTxt) require.NoError(t, err) - var qb epsearchast_v3.SemanticReducer[epsearchast_v3_es.JsonObject] = epsearchast_v3_es.DefaultEsQueryBuilder{} + var qb epsearchast.SemanticReducer[JsonObject] = DefaultEsQueryBuilder{} // Execute SUT - query, err := epsearchast_v3.SemanticReduceAst(astNode, qb) + query, err := epsearchast.SemanticReduceAst(astNode, qb) require.NoError(t, err) // Verification @@ -594,11 +595,11 @@ func TestSimpleRecursiveStructure(t *testing.T) { ] } }` - astNode, err := epsearchast_v3.GetAst(jsonTxt) + astNode, err := epsearchast.GetAst(jsonTxt) require.NoError(t, err) - var qb epsearchast_v3.SemanticReducer[epsearchast_v3_es.JsonObject] = epsearchast_v3_es.DefaultEsQueryBuilder{ - OpTypeToFieldNames: map[string]*epsearchast_v3_es.OperatorTypeToMultiFieldName{ + var qb epsearchast.SemanticReducer[JsonObject] = DefaultEsQueryBuilder{ + OpTypeToFieldNames: map[string]*OperatorTypeToMultiFieldName{ "status": { Equality: "status.keyword", }, @@ -606,7 +607,7 @@ func TestSimpleRecursiveStructure(t *testing.T) { } // Execute SUT - query, err := epsearchast_v3.SemanticReduceAst(astNode, qb) + query, err := epsearchast.SemanticReduceAst(astNode, qb) require.NoError(t, err) // Verification @@ -656,12 +657,12 @@ func TestSimpleRecursiveWithStringOverrideStruct(t *testing.T) { } }` - astNode, err := epsearchast_v3.GetAst(jsonTxt) + astNode, err := epsearchast.GetAst(jsonTxt) require.NoError(t, err) var qb = &LowerCaseEmail{ - epsearchast_v3_es.DefaultEsQueryBuilder{ - OpTypeToFieldNames: map[string]*epsearchast_v3_es.OperatorTypeToMultiFieldName{ + DefaultEsQueryBuilder{ + OpTypeToFieldNames: map[string]*OperatorTypeToMultiFieldName{ "status": { Equality: "status.keyword", }, @@ -670,7 +671,7 @@ func TestSimpleRecursiveWithStringOverrideStruct(t *testing.T) { } // Execute SUT - query, err := epsearchast_v3.SemanticReduceAst[epsearchast_v3_es.JsonObject](astNode, qb) + query, err := epsearchast.SemanticReduceAst[JsonObject](astNode, qb) require.NoError(t, err) // Verification @@ -704,13 +705,13 @@ func TestSimpleBinaryTextOperatorGeneratesCorrectQuery(t *testing.T) { } }` - astNode, err := epsearchast_v3.GetAst(jsonTxt) + astNode, err := epsearchast.GetAst(jsonTxt) require.NoError(t, err) - var qb epsearchast_v3.SemanticReducer[epsearchast_v3_es.JsonObject] = epsearchast_v3_es.DefaultEsQueryBuilder{} + var qb epsearchast.SemanticReducer[JsonObject] = DefaultEsQueryBuilder{} // Execute SUT - query, err := epsearchast_v3.SemanticReduceAst(astNode, qb) + query, err := epsearchast.SemanticReduceAst(astNode, qb) require.NoError(t, err) // Verification @@ -743,11 +744,11 @@ func TestSimpleBinaryTextOperatorGeneratesCorrectQueryWithFieldOverride(t *testi } }` - astNode, err := epsearchast_v3.GetAst(jsonTxt) + astNode, err := epsearchast.GetAst(jsonTxt) require.NoError(t, err) - var qb epsearchast_v3.SemanticReducer[epsearchast_v3_es.JsonObject] = epsearchast_v3_es.DefaultEsQueryBuilder{ - OpTypeToFieldNames: map[string]*epsearchast_v3_es.OperatorTypeToMultiFieldName{ + var qb epsearchast.SemanticReducer[JsonObject] = DefaultEsQueryBuilder{ + OpTypeToFieldNames: map[string]*OperatorTypeToMultiFieldName{ "description": { Text: "description.text", }, @@ -755,7 +756,7 @@ func TestSimpleBinaryTextOperatorGeneratesCorrectQueryWithFieldOverride(t *testi } // Execute SUT - query, err := epsearchast_v3.SemanticReduceAst(astNode, qb) + query, err := epsearchast.SemanticReduceAst(astNode, qb) require.NoError(t, err) // Verification @@ -788,15 +789,15 @@ func TestSimpleBinaryTextOperatorGeneratesCorrectQueryWithFuzzinessSetting(t *te } }` - astNode, err := epsearchast_v3.GetAst(jsonTxt) + astNode, err := epsearchast.GetAst(jsonTxt) require.NoError(t, err) - var qb epsearchast_v3.SemanticReducer[epsearchast_v3_es.JsonObject] = epsearchast_v3_es.DefaultEsQueryBuilder{ + var qb epsearchast.SemanticReducer[JsonObject] = DefaultEsQueryBuilder{ DefaultFuzziness: "AUTO", } // Execute SUT - query, err := epsearchast_v3.SemanticReduceAst(astNode, qb) + query, err := epsearchast.SemanticReduceAst(astNode, qb) require.NoError(t, err) // Verification @@ -828,13 +829,13 @@ func TestSimpleUnaryIsNullOperatorGeneratesCorrectQuery(t *testing.T) { } }` - astNode, err := epsearchast_v3.GetAst(jsonTxt) + astNode, err := epsearchast.GetAst(jsonTxt) require.NoError(t, err) - var qb epsearchast_v3.SemanticReducer[epsearchast_v3_es.JsonObject] = epsearchast_v3_es.DefaultEsQueryBuilder{} + var qb epsearchast.SemanticReducer[JsonObject] = DefaultEsQueryBuilder{} // Execute SUT - query, err := epsearchast_v3.SemanticReduceAst(astNode, qb) + query, err := epsearchast.SemanticReduceAst(astNode, qb) require.NoError(t, err) // Verification @@ -866,11 +867,11 @@ func TestSimpleUnaryIsNullOperatorGeneratesCorrectQueryWithFieldOverride(t *test } }` - astNode, err := epsearchast_v3.GetAst(jsonTxt) + astNode, err := epsearchast.GetAst(jsonTxt) require.NoError(t, err) - var qb epsearchast_v3.SemanticReducer[epsearchast_v3_es.JsonObject] = epsearchast_v3_es.DefaultEsQueryBuilder{ - OpTypeToFieldNames: map[string]*epsearchast_v3_es.OperatorTypeToMultiFieldName{ + var qb epsearchast.SemanticReducer[JsonObject] = DefaultEsQueryBuilder{ + OpTypeToFieldNames: map[string]*OperatorTypeToMultiFieldName{ "sort_order": { Equality: "sort_order.keyword", }, @@ -878,7 +879,7 @@ func TestSimpleUnaryIsNullOperatorGeneratesCorrectQueryWithFieldOverride(t *test } // Execute SUT - query, err := epsearchast_v3.SemanticReduceAst(astNode, qb) + query, err := epsearchast.SemanticReduceAst(astNode, qb) require.NoError(t, err) // Verification @@ -890,7 +891,7 @@ func TestSimpleUnaryIsNullOperatorGeneratesCorrectQueryWithFieldOverride(t *test func TestMustValidateDoesNotPanicOnEmptyObject(t *testing.T) { // Fixture Setup - qb := epsearchast_v3_es.DefaultEsQueryBuilder{} + qb := DefaultEsQueryBuilder{} // Execute SUT & Verification assert.NotPanics(t, func() { @@ -900,7 +901,7 @@ func TestMustValidateDoesNotPanicOnEmptyObject(t *testing.T) { func TestMustValidatePanicsWhenRegexDoesNotCompile(t *testing.T) { // Fixture Setup - qb := epsearchast_v3_es.DefaultEsQueryBuilder{NestedFieldToQuery: map[string]epsearchast_v3_es.NestedReplacement{ + qb := DefaultEsQueryBuilder{NestedFieldToQuery: map[string]NestedReplacement{ "tes(": { Path: "foo", }, @@ -915,7 +916,7 @@ func TestMustValidatePanicsWhenRegexDoesNotCompile(t *testing.T) { func TestMustValidatePanicsWhenRegexDoesNotHaveStartAnchor(t *testing.T) { // Fixture Setup - qb := epsearchast_v3_es.DefaultEsQueryBuilder{NestedFieldToQuery: map[string]epsearchast_v3_es.NestedReplacement{ + qb := DefaultEsQueryBuilder{NestedFieldToQuery: map[string]NestedReplacement{ "test": { Path: "foo", }, @@ -930,7 +931,7 @@ func TestMustValidatePanicsWhenRegexDoesNotHaveStartAnchor(t *testing.T) { func TestMustValidatePanicsWhenRegexDoesNotHaveEndAnchor(t *testing.T) { // Fixture Setup - qb := epsearchast_v3_es.DefaultEsQueryBuilder{NestedFieldToQuery: map[string]epsearchast_v3_es.NestedReplacement{ + qb := DefaultEsQueryBuilder{NestedFieldToQuery: map[string]NestedReplacement{ "^test": { Path: "foo", }, @@ -945,7 +946,7 @@ func TestMustValidatePanicsWhenRegexDoesNotHaveEndAnchor(t *testing.T) { func TestMustValidatePanicsWhenNoPathIsSet(t *testing.T) { // Fixture Setup - qb := epsearchast_v3_es.DefaultEsQueryBuilder{NestedFieldToQuery: map[string]epsearchast_v3_es.NestedReplacement{ + qb := DefaultEsQueryBuilder{NestedFieldToQuery: map[string]NestedReplacement{ "^test.foo$": {}, }} @@ -958,7 +959,7 @@ func TestMustValidatePanicsWhenNoPathIsSet(t *testing.T) { func TestMustValidatePanicsWhenRegexHasValueCaptureGroup(t *testing.T) { // Fixture Setup - qb := epsearchast_v3_es.DefaultEsQueryBuilder{NestedFieldToQuery: map[string]epsearchast_v3_es.NestedReplacement{ + qb := DefaultEsQueryBuilder{NestedFieldToQuery: map[string]NestedReplacement{ "^test.(?P.+)value$": { Path: "foo", }, @@ -973,10 +974,10 @@ func TestMustValidatePanicsWhenRegexHasValueCaptureGroup(t *testing.T) { func TestMustValidatePanicsWhenRegexKeyHasValueReplacement(t *testing.T) { // Fixture Setup - qb := epsearchast_v3_es.DefaultEsQueryBuilder{NestedFieldToQuery: map[string]epsearchast_v3_es.NestedReplacement{ + qb := DefaultEsQueryBuilder{NestedFieldToQuery: map[string]NestedReplacement{ "^test.(?P.+).value$": { Path: "foo", - Subqueries: map[string]epsearchast_v3_es.Replacement{ + Subqueries: map[string]Replacement{ "foo.$value": { Value: "$value", }, @@ -993,10 +994,10 @@ func TestMustValidatePanicsWhenRegexKeyHasValueReplacement(t *testing.T) { func TestMustValidatePanicsNoSubqueriesSet(t *testing.T) { // Fixture Setup - qb := epsearchast_v3_es.DefaultEsQueryBuilder{NestedFieldToQuery: map[string]epsearchast_v3_es.NestedReplacement{ + qb := DefaultEsQueryBuilder{NestedFieldToQuery: map[string]NestedReplacement{ "^test.(?P.+).value$": { Path: "foo", - Subqueries: map[string]epsearchast_v3_es.Replacement{}, + Subqueries: map[string]Replacement{}, }, }} @@ -1008,10 +1009,10 @@ func TestMustValidatePanicsNoSubqueriesSet(t *testing.T) { func TestMustValidatePanicsWhenFieldHasTemplateNotInField(t *testing.T) { // Fixture Setup - qb := epsearchast_v3_es.DefaultEsQueryBuilder{NestedFieldToQuery: map[string]epsearchast_v3_es.NestedReplacement{ + qb := DefaultEsQueryBuilder{NestedFieldToQuery: map[string]NestedReplacement{ "^test.(?P.+).value$": { Path: "foo", - Subqueries: map[string]epsearchast_v3_es.Replacement{ + Subqueries: map[string]Replacement{ "foo.$i.$id": { Value: "$id", }, @@ -1027,10 +1028,10 @@ func TestMustValidatePanicsWhenFieldHasTemplateNotInField(t *testing.T) { func TestMustValidatePanicsWhenFieldValueHasTemplateNotInField(t *testing.T) { // Fixture Setup - qb := epsearchast_v3_es.DefaultEsQueryBuilder{NestedFieldToQuery: map[string]epsearchast_v3_es.NestedReplacement{ + qb := DefaultEsQueryBuilder{NestedFieldToQuery: map[string]NestedReplacement{ "^test.(?P.+).value.(?P.+)$": { Path: "foo", - Subqueries: map[string]epsearchast_v3_es.Replacement{ + Subqueries: map[string]Replacement{ "foo.$id": { Value: "$yas.$bar", }, @@ -1085,10 +1086,10 @@ func TestMultipleRegexMatchesAreReplacedCorrectlyWhenCaptureGroupsOverlap(t *tes } }` - qb := &epsearchast_v3_es.DefaultEsQueryBuilder{NestedFieldToQuery: map[string]epsearchast_v3_es.NestedReplacement{ + qb := &DefaultEsQueryBuilder{NestedFieldToQuery: map[string]NestedReplacement{ `^field\.(?P[^.]+)\.(?P[^.]+)\.(?P[^.]+)$`: { Path: "foo", - Subqueries: map[string]epsearchast_v3_es.Replacement{ + Subqueries: map[string]Replacement{ "foo.$aaa$aa$a": { Value: "$a$aa$aaa", }, @@ -1099,11 +1100,11 @@ func TestMultipleRegexMatchesAreReplacedCorrectlyWhenCaptureGroupsOverlap(t *tes }, }} - astNode, err := epsearchast_v3.GetAst(jsonTxt) + astNode, err := epsearchast.GetAst(jsonTxt) require.NoError(t, err) // Execute SUT - query, err := epsearchast_v3.SemanticReduceAst[epsearchast_v3_es.JsonObject](astNode, qb) + query, err := epsearchast.SemanticReduceAst[JsonObject](astNode, qb) require.NoError(t, err) // Verification @@ -1205,10 +1206,10 @@ func TestMultipleReplacementFieldsInNestedObjectAreSortedInDeterministicOrder(t } }` - qb := &epsearchast_v3_es.DefaultEsQueryBuilder{NestedFieldToQuery: map[string]epsearchast_v3_es.NestedReplacement{ + qb := &DefaultEsQueryBuilder{NestedFieldToQuery: map[string]NestedReplacement{ `^field\.(?P[^.]+)$`: { Path: "foo", - Subqueries: map[string]epsearchast_v3_es.Replacement{ + Subqueries: map[string]Replacement{ "foo.j": {Value: "$value"}, "foo.i": {Value: "$value"}, "foo.l": {Value: "$value"}, @@ -1226,11 +1227,11 @@ func TestMultipleReplacementFieldsInNestedObjectAreSortedInDeterministicOrder(t }, }} - astNode, err := epsearchast_v3.GetAst(jsonTxt) + astNode, err := epsearchast.GetAst(jsonTxt) require.NoError(t, err) // Execute SUT - query, err := epsearchast_v3.SemanticReduceAst[epsearchast_v3_es.JsonObject](astNode, qb) + query, err := epsearchast.SemanticReduceAst[JsonObject](astNode, qb) require.NoError(t, err) // Verification @@ -1242,13 +1243,13 @@ func TestMultipleReplacementFieldsInNestedObjectAreSortedInDeterministicOrder(t } type LowerCaseEmail struct { - epsearchast_v3_es.DefaultEsQueryBuilder + DefaultEsQueryBuilder } -func (l *LowerCaseEmail) VisitEq(first, second string) (*epsearchast_v3_es.JsonObject, error) { +func (l *LowerCaseEmail) VisitEq(first, second string) (*JsonObject, error) { if first == "email" { - return epsearchast_v3_es.DefaultEsQueryBuilder.VisitEq(l.DefaultEsQueryBuilder, first, strings.ToLower(second)) + return DefaultEsQueryBuilder.VisitEq(l.DefaultEsQueryBuilder, first, strings.ToLower(second)) } else { - return epsearchast_v3_es.DefaultEsQueryBuilder.VisitEq(l.DefaultEsQueryBuilder, first, second) + return DefaultEsQueryBuilder.VisitEq(l.DefaultEsQueryBuilder, first, second) } } diff --git a/go.mod b/go.mod index 8aa7231..9c13438 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/go-playground/validator/v10 v10.28.0 github.com/lib/pq v1.10.9 github.com/stretchr/testify v1.11.1 - go.mongodb.org/mongo-driver v1.17.6 + go.mongodb.org/mongo-driver/v2 v2.4.0 gorm.io/driver/postgres v1.6.0 gorm.io/gorm v1.31.1 ) @@ -16,7 +16,7 @@ require ( github.com/gabriel-vasile/mimetype v1.4.10 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/golang/snappy v0.0.4 // indirect + github.com/golang/snappy v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgx/v5 v5.7.2 // indirect @@ -26,7 +26,6 @@ require ( github.com/klauspost/compress v1.16.7 // indirect github.com/kr/text v0.2.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect - github.com/montanaflynn/stats v0.7.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rogpeppe/go-internal v1.13.1 // indirect github.com/stretchr/objx v0.5.2 // indirect diff --git a/go.sum b/go.sum index 95e8178..089ec6d 100644 --- a/go.sum +++ b/go.sum @@ -12,8 +12,8 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688= github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU= -github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= -github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= +github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= @@ -38,8 +38,6 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= -github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= @@ -60,8 +58,8 @@ github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gi github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss= -go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= +go.mongodb.org/mongo-driver/v2 v2.4.0 h1:Oq6BmUAAFTzMeh6AonuDlgZMuAuEiUxoAD1koK5MuFo= +go.mongodb.org/mongo-driver/v2 v2.4.0/go.mod h1:jHeEDJHJq7tm6ZF45Issun9dbogjfnPySb1vXA7EeAI= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= diff --git a/external/epsearchast/v3/gorm/gorm_query_builder.go b/gorm/gorm_query_builder.go similarity index 95% rename from external/epsearchast/v3/gorm/gorm_query_builder.go rename to gorm/gorm_query_builder.go index 0424f27..5ed0001 100644 --- a/external/epsearchast/v3/gorm/gorm_query_builder.go +++ b/gorm/gorm_query_builder.go @@ -1,10 +1,11 @@ -package epsearchast_v3_gorm +package astgorm import ( "fmt" - epsearchast_v3 "github.com/elasticpath/epcc-search-ast-helper/external/epsearchast/v3" - "github.com/lib/pq" "strings" + + "github.com/elasticpath/epcc-search-ast-helper" + "github.com/lib/pq" ) type SubQuery struct { @@ -16,7 +17,7 @@ type SubQuery struct { type DefaultGormQueryBuilder struct{} -var _ epsearchast_v3.SemanticReducer[SubQuery] = (*DefaultGormQueryBuilder)(nil) +var _ epsearchast.SemanticReducer[SubQuery] = (*DefaultGormQueryBuilder)(nil) func (g DefaultGormQueryBuilder) PostVisitAnd(sqs []*SubQuery) (*SubQuery, error) { clauses := make([]string, 0, len(sqs)) diff --git a/external/epsearchast/v3/gorm/gorm_query_builder_int_test.go b/gorm/gorm_query_builder_int_test.go similarity index 97% rename from external/epsearchast/v3/gorm/gorm_query_builder_int_test.go rename to gorm/gorm_query_builder_int_test.go index 776d042..4fa92f4 100644 --- a/external/epsearchast/v3/gorm/gorm_query_builder_int_test.go +++ b/gorm/gorm_query_builder_int_test.go @@ -1,9 +1,10 @@ -package epsearchast_v3_gorm +package astgorm import ( "context" "fmt" - epsearchast_v3 "github.com/elasticpath/epcc-search-ast-helper/external/epsearchast/v3" + + "github.com/elasticpath/epcc-search-ast-helper" "github.com/lib/pq" "gorm.io/driver/postgres" "gorm.io/gorm" @@ -707,7 +708,7 @@ func TestSmokeTestPostgresWithFilters(t *testing.T) { } for _, tc := range testCases { - ast, err := epsearchast_v3.GetAst(tc.filter) + ast, err := epsearchast.GetAst(tc.filter) if err != nil { t.Fatalf("Failed to get filter: %v", err) } @@ -728,15 +729,15 @@ func TestSmokeTestPostgresWithFilters(t *testing.T) { // Perform a count query with a filter // Create query builder - var qb epsearchast_v3.SemanticReducer[SubQuery] = DefaultGormQueryBuilder{} + var qb epsearchast.SemanticReducer[SubQuery] = DefaultGormQueryBuilder{} // Create Query Object - ast, err := epsearchast_v3.GetAst(tc.filter) + ast, err := epsearchast.GetAst(tc.filter) if err != nil { t.Fatalf("Failed to get filter: %v", err) } - query, err := epsearchast_v3.SemanticReduceAst(ast, qb) + query, err := epsearchast.SemanticReduceAst(ast, qb) if err != nil { t.Fatalf("Failed to convert filter: %v", err) diff --git a/external/epsearchast/v3/gorm/gorm_query_builder_test.go b/gorm/gorm_query_builder_test.go similarity index 77% rename from external/epsearchast/v3/gorm/gorm_query_builder_test.go rename to gorm/gorm_query_builder_test.go index 6ff03db..9d95347 100644 --- a/external/epsearchast/v3/gorm/gorm_query_builder_test.go +++ b/gorm/gorm_query_builder_test.go @@ -1,8 +1,8 @@ -package epsearchast_v3_gorm +package astgorm import ( "fmt" - epsearchast_v3 "github.com/elasticpath/epcc-search-ast-helper/external/epsearchast/v3" + "github.com/elasticpath/epcc-search-ast-helper" "github.com/stretchr/testify/require" "strconv" "testing" @@ -41,13 +41,13 @@ func TestSimpleBinaryOperatorFiltersGeneratesCorrectWhereClause(t *testing.T) { "args": [ "amount", "5"] }`, binOp.AstOp) - astNode, err := epsearchast_v3.GetAst(jsonTxt) + astNode, err := epsearchast.GetAst(jsonTxt) require.NoError(t, err) - var qb epsearchast_v3.SemanticReducer[SubQuery] = DefaultGormQueryBuilder{} + var qb epsearchast.SemanticReducer[SubQuery] = DefaultGormQueryBuilder{} // Execute SUT - query, err := epsearchast_v3.SemanticReduceAst(astNode, qb) + query, err := epsearchast.SemanticReduceAst(astNode, qb) // Verification @@ -71,13 +71,13 @@ func TestSimpleUnaryOperatorFiltersGeneratesCorrectWhereClause(t *testing.T) { "args": [ "amount"] }`, unaryOp.AstOp) - astNode, err := epsearchast_v3.GetAst(jsonTxt) + astNode, err := epsearchast.GetAst(jsonTxt) require.NoError(t, err) - var sr epsearchast_v3.SemanticReducer[SubQuery] = DefaultGormQueryBuilder{} + var sr epsearchast.SemanticReducer[SubQuery] = DefaultGormQueryBuilder{} // Execute SUT - query, err := epsearchast_v3.SemanticReduceAst(astNode, sr) + query, err := epsearchast.SemanticReduceAst(astNode, sr) // Verification @@ -100,13 +100,13 @@ func TestSimpleVariableOperatorFiltersGeneratesCorrectWhereClause(t *testing.T) "args": ["amount", "5", "6", "7"] }`, varOp.AstOp) - astNode, err := epsearchast_v3.GetAst(jsonTxt) + astNode, err := epsearchast.GetAst(jsonTxt) require.NoError(t, err) - var qb epsearchast_v3.SemanticReducer[SubQuery] = DefaultGormQueryBuilder{} + var qb epsearchast.SemanticReducer[SubQuery] = DefaultGormQueryBuilder{} // Execute SUT - query, err := epsearchast_v3.SemanticReduceAst(astNode, qb) + query, err := epsearchast.SemanticReduceAst(astNode, qb) // Verification @@ -129,13 +129,13 @@ func TestLikeFilterWildCards(t *testing.T) { "args": [ "email", "%s"] }`, astLiteral) - astNode, err := epsearchast_v3.GetAst(jsonTxt) + astNode, err := epsearchast.GetAst(jsonTxt) require.NoError(t, err) - var qb epsearchast_v3.SemanticReducer[SubQuery] = DefaultGormQueryBuilder{} + var qb epsearchast.SemanticReducer[SubQuery] = DefaultGormQueryBuilder{} // Execute SUT - query, err := epsearchast_v3.SemanticReduceAst(astNode, qb) + query, err := epsearchast.SemanticReduceAst(astNode, qb) // Verification @@ -163,13 +163,13 @@ func TestTextBinaryOperatorFiltersGeneratesCorrectWhereClause(t *testing.T) { "args": [ "name", "computer"] }`, "TEXT") - astNode, err := epsearchast_v3.GetAst(jsonTxt) + astNode, err := epsearchast.GetAst(jsonTxt) require.NoError(t, err) - var qb epsearchast_v3.SemanticReducer[SubQuery] = DefaultGormQueryBuilder{} + var qb epsearchast.SemanticReducer[SubQuery] = DefaultGormQueryBuilder{} // Execute SUT - query, err := epsearchast_v3.SemanticReduceAst(astNode, qb) + query, err := epsearchast.SemanticReduceAst(astNode, qb) // Verification @@ -198,13 +198,13 @@ func TestSimpleRecursiveStructure(t *testing.T) { } ` - astNode, err := epsearchast_v3.GetAst(jsonTxt) + astNode, err := epsearchast.GetAst(jsonTxt) require.NoError(t, err) - var qb epsearchast_v3.SemanticReducer[SubQuery] = DefaultGormQueryBuilder{} + var qb epsearchast.SemanticReducer[SubQuery] = DefaultGormQueryBuilder{} // Execute SUT - query, err := epsearchast_v3.SemanticReduceAst(astNode, qb) + query, err := epsearchast.SemanticReduceAst(astNode, qb) // Verification @@ -233,13 +233,13 @@ func TestSimpleRecursiveWithStringOverrideStruct(t *testing.T) { } ` - astNode, err := epsearchast_v3.GetAst(jsonTxt) + astNode, err := epsearchast.GetAst(jsonTxt) require.NoError(t, err) - var qb epsearchast_v3.SemanticReducer[SubQuery] = &LowerCaseEmail{} + var qb epsearchast.SemanticReducer[SubQuery] = &LowerCaseEmail{} // Execute SUT - query, err := epsearchast_v3.SemanticReduceAst(astNode, qb) + query, err := epsearchast.SemanticReduceAst(astNode, qb) // Verification @@ -267,13 +267,13 @@ func TestSimpleRecursiveWithIntFieldStruct(t *testing.T) { } ` - astNode, err := epsearchast_v3.GetAst(jsonTxt) + astNode, err := epsearchast.GetAst(jsonTxt) require.NoError(t, err) - var qb epsearchast_v3.SemanticReducer[SubQuery] = &IntFieldQueryBuilder{} + var qb epsearchast.SemanticReducer[SubQuery] = &IntFieldQueryBuilder{} // Execute SUT - query, err := epsearchast_v3.SemanticReduceAst(astNode, qb) + query, err := epsearchast.SemanticReduceAst(astNode, qb) // Verification diff --git a/external/epsearchast/v3/identity_semantic_reduce.go b/identity_semantic_reduce.go similarity index 99% rename from external/epsearchast/v3/identity_semantic_reduce.go rename to identity_semantic_reduce.go index 502986c..c3c2392 100644 --- a/external/epsearchast/v3/identity_semantic_reduce.go +++ b/identity_semantic_reduce.go @@ -1,4 +1,4 @@ -package epsearchast_v3 +package epsearchast // IdentitySemanticReducer is a SemanticReducer that returns the same AstNode it is given. type IdentitySemanticReducer struct{} diff --git a/external/epsearchast/v3/identity_semantic_reduce_test.go b/identity_semantic_reduce_test.go similarity index 99% rename from external/epsearchast/v3/identity_semantic_reduce_test.go rename to identity_semantic_reduce_test.go index 9080823..d6711da 100644 --- a/external/epsearchast/v3/identity_semantic_reduce_test.go +++ b/identity_semantic_reduce_test.go @@ -1,4 +1,4 @@ -package epsearchast_v3 +package epsearchast import ( "encoding/json" diff --git a/mongo/mongo_atlas_search_query_builder.go b/mongo/mongo_atlas_search_query_builder.go new file mode 100644 index 0000000..2e0fba2 --- /dev/null +++ b/mongo/mongo_atlas_search_query_builder.go @@ -0,0 +1,220 @@ +package astmongo + +import ( + "fmt" + "strings" + + "github.com/elasticpath/epcc-search-ast-helper" + "go.mongodb.org/mongo-driver/v2/bson" +) + +type DefaultAtlasSearchQueryBuilder struct { + // Map from field name to multi-analyzer names for different operators + // If a field is not in this map, or if the analyzer name is "", + // the base path will be used without specifying a multi-analyzer + FieldToMultiAnalyzers map[string]*StringMultiAnalyzers +} + +type StringMultiAnalyzers struct { + // Multi-analyzer name for case-insensitive wildcard (ILIKE) + // If empty, will use: {"path": "field"} + // If set, will use: {"path": {"value": "field", "multi": "this_value"}} + WildcardCaseInsensitive string + + // Multi-analyzer name for case-sensitive wildcard (LIKE) + // If empty, will use: {"path": "field"} + // If set, will use: {"path": {"value": "field", "multi": "this_value"}} + WildcardCaseSensitive string +} + +var _ epsearchast.SemanticReducer[bson.D] = (*DefaultAtlasSearchQueryBuilder)(nil) + +func (d DefaultAtlasSearchQueryBuilder) PostVisitAnd(rs []*bson.D) (*bson.D, error) { + // https://www.mongodb.com/docs/atlas/atlas-search/compound/ + return &bson.D{ + {"compound", bson.D{ + {"must", rs}, + }}, + }, nil +} + +func (d DefaultAtlasSearchQueryBuilder) PostVisitOr(rs []*bson.D) (*bson.D, error) { + // https://www.mongodb.com/docs/atlas/atlas-search/compound/ + return &bson.D{ + {"compound", bson.D{ + {"should", rs}, + // minimumShouldMatch: 1 means at least one should clause must match + // https://www.mongodb.com/docs/atlas/atlas-search/compound/#std-label-compound-ref + {"minimumShouldMatch", 1}, + }}, + }, nil +} + +func (d DefaultAtlasSearchQueryBuilder) VisitText(first, second string) (*bson.D, error) { + // https://www.mongodb.com/docs/atlas/atlas-search/text/ + return &bson.D{ + {"text", bson.D{ + {"query", second}, + {"path", first}, + }}, + }, nil +} + +func (d DefaultAtlasSearchQueryBuilder) VisitIn(args ...string) (*bson.D, error) { + // https://www.mongodb.com/docs/atlas/atlas-search/in/ + if len(args) < 2 { + return nil, fmt.Errorf("IN operator requires at least 2 arguments (field and at least one value)") + } + + fieldName := args[0] + values := args[1:] + + return &bson.D{ + {"in", bson.D{ + {"path", fieldName}, + {"value", values}, + }}, + }, nil +} + +func (d DefaultAtlasSearchQueryBuilder) VisitEq(first, second string) (*bson.D, error) { + // https://www.mongodb.com/docs/atlas/atlas-search/equals/ + return &bson.D{ + {"equals", bson.D{ + {"path", first}, + {"value", second}, + }}, + }, nil +} + +func (d DefaultAtlasSearchQueryBuilder) VisitLe(first, second string) (*bson.D, error) { + // https://www.mongodb.com/docs/atlas/atlas-search/range/ + return &bson.D{ + {"range", bson.D{ + {"path", first}, + {"lte", second}, + }}, + }, nil +} + +func (d DefaultAtlasSearchQueryBuilder) VisitLt(first, second string) (*bson.D, error) { + // https://www.mongodb.com/docs/atlas/atlas-search/range/ + return &bson.D{ + {"range", bson.D{ + {"path", first}, + {"lt", second}, + }}, + }, nil +} + +func (d DefaultAtlasSearchQueryBuilder) VisitGe(first, second string) (*bson.D, error) { + // https://www.mongodb.com/docs/atlas/atlas-search/range/ + return &bson.D{ + {"range", bson.D{ + {"path", first}, + {"gte", second}, + }}, + }, nil +} + +func (d DefaultAtlasSearchQueryBuilder) VisitGt(first, second string) (*bson.D, error) { + // https://www.mongodb.com/docs/atlas/atlas-search/range/ + return &bson.D{ + {"range", bson.D{ + {"path", first}, + {"gt", second}, + }}, + }, nil +} + +func (d DefaultAtlasSearchQueryBuilder) VisitLike(first, second string) (*bson.D, error) { + // https://www.mongodb.com/docs/atlas/atlas-search/wildcard/ + // Case-sensitive wildcard matching (unlike ILIKE which is case-insensitive) + path := d.getWildcardPath(first, true) + + return &bson.D{ + {"wildcard", bson.D{ + {"path", path}, + {"query", d.ProcessWildcardString(second)}, + {"allowAnalyzedField", true}, + }}, + }, nil +} + +func (d DefaultAtlasSearchQueryBuilder) VisitILike(first, second string) (*bson.D, error) { + // https://www.mongodb.com/docs/atlas/atlas-search/wildcard/ + // Case-insensitive wildcard matching (uses allowAnalyzedField: true) + path := d.getWildcardPath(first, false) + + return &bson.D{ + {"wildcard", bson.D{ + {"path", path}, + {"query", d.ProcessWildcardString(second)}, + {"allowAnalyzedField", true}, + }}, + }, nil +} + +func (d DefaultAtlasSearchQueryBuilder) VisitContains(_, _ string) (*bson.D, error) { + return nil, fmt.Errorf("CONTAINS operator not yet implemented for Atlas Search") +} + +func (d DefaultAtlasSearchQueryBuilder) VisitContainsAny(_ ...string) (*bson.D, error) { + return nil, fmt.Errorf("CONTAINS_ANY operator not yet implemented for Atlas Search") +} + +func (d DefaultAtlasSearchQueryBuilder) VisitContainsAll(_ ...string) (*bson.D, error) { + return nil, fmt.Errorf("CONTAINS_ALL operator not yet implemented for Atlas Search") +} + +func (d DefaultAtlasSearchQueryBuilder) VisitIsNull(_ string) (*bson.D, error) { + return nil, fmt.Errorf("IS_NULL operator not yet implemented for Atlas Search") +} + +// ProcessWildcardString processes wildcard strings for Atlas Search wildcard queries +// Escapes special characters except * and ? at the beginning/end +func (d DefaultAtlasSearchQueryBuilder) ProcessWildcardString(s string) string { + // Atlas Search wildcard uses * and ? as wildcards, similar to ES + // Escape all wildcards first + str := strings.ReplaceAll(s, "?", `\?`) + str = strings.ReplaceAll(str, "*", `\*`) + + // Un-escape wildcards at the beginning + if strings.HasPrefix(str, `\*`) { + str = str[1:] + } + + // Un-escape wildcards at the end + if strings.HasSuffix(str, `\*`) { + str = str[:len(str)-2] + "*" + } + + return str +} + +// getWildcardPath returns the path configuration for wildcard queries (LIKE/ILIKE) +// If caseSensitive is true, uses WildcardCaseSensitive analyzer +// If caseSensitive is false, uses WildcardCaseInsensitive analyzer +// If no analyzer is configured (or is empty string), returns simple field name +func (d DefaultAtlasSearchQueryBuilder) getWildcardPath(fieldName string, caseSensitive bool) interface{} { + // Check if field has multi-analyzer configuration + if config, ok := d.FieldToMultiAnalyzers[fieldName]; ok && config != nil { + var analyzerName string + if caseSensitive { + analyzerName = config.WildcardCaseSensitive + } else { + analyzerName = config.WildcardCaseInsensitive + } + + // If analyzer name is specified, return path with multi + if analyzerName != "" { + return bson.D{ + {"value", fieldName}, + {"multi", analyzerName}, + } + } + } + + // Otherwise, return simple field name + return fieldName +} diff --git a/mongo/mongo_atlas_search_query_builder_int_test.go b/mongo/mongo_atlas_search_query_builder_int_test.go new file mode 100644 index 0000000..84db680 --- /dev/null +++ b/mongo/mongo_atlas_search_query_builder_int_test.go @@ -0,0 +1,660 @@ +package astmongo + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/elasticpath/epcc-search-ast-helper" + "go.mongodb.org/mongo-driver/v2/bson" + "go.mongodb.org/mongo-driver/v2/mongo" + "go.mongodb.org/mongo-driver/v2/mongo/options" +) + +func TestSmokeTestAtlasSearchWithFilters(t *testing.T) { + if testing.Short() { + t.Skip("Skipping integration test in short mode") + } + + // Connect to Atlas Search enabled MongoDB + ctx := context.Background() + atlasClient, err := mongo.Connect(options.Client().ApplyURI("mongodb://admin:admin@localhost:20004/?replicaSet=rs0&directConnection=true")) + if err != nil { + t.Fatalf("Failed to connect to MongoDB Atlas Search: %v", err) + } + defer func() { + if err := atlasClient.Disconnect(ctx); err != nil { + t.Logf("Failed to disconnect: %v", err) + } + }() + + documents := []interface{}{ + bson.M{ + "string_field": "test1 test1", + "array_field": []string{"a a", "b b"}, + "nullable_string_field": nil, + "text_field": "Developers like IDEs", + "uuid_field": "550e8400-e29b-41d4-a716-446655440001", + "date_field": time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), + }, + bson.M{ + "string_field": "test2 test2", + "array_field": []string{"c c", "d d"}, + "nullable_string_field": "yay yay", + "text_field": "I like Development Environments", + "uuid_field": "550e8400-e29b-41d4-a716-446655440002", + "date_field": time.Date(2024, 6, 15, 0, 0, 0, 0, time.UTC), + }, + bson.M{ + "string_field": "test3 test3", + "array_field": []string{"c c"}, + "text_field": "Vim is the best", + "uuid_field": "550e8400-e29b-41d4-a716-446655440003", + "date_field": time.Date(2024, 12, 31, 0, 0, 0, 0, time.UTC), + }, + } + + var testCases = []struct { + filter string + count int64 + }{ + { + //language=JSON + filter: `{ + "type": "TEXT", + "args": ["text_field", "like"] + }`, + count: 2, + }, + { + //language=JSON + filter: `{ + "type": "EQ", + "args": ["string_field", "test1 test1"] + }`, + count: 1, + }, + { + //language=JSON + filter: `{ + "type": "EQ", + "args": ["string_field", "test2 test2"] + }`, + count: 1, + }, + { + // Test that EQ does exact matching, not partial matching + //language=JSON + filter: `{ + "type": "EQ", + "args": ["string_field", "test"] + }`, + count: 0, + }, + { + // Test EQ on text_field - should match exact string, no stemming + //language=JSON + filter: `{ + "type": "EQ", + "args": ["text_field", "Developers like IDEs"] + }`, + count: 1, + }, + { + // Test EQ on text_field - should be case sensitive + //language=JSON + filter: `{ + "type": "EQ", + "args": ["text_field", "developers like ides"] + }`, + count: 0, + }, + { + // Test EQ on text_field - should not match partial/stemmed + //language=JSON + filter: `{ + "type": "EQ", + "args": ["text_field", "Developers"] + }`, + count: 0, + }, + { + // Test TEXT on text_field - should use stemming and match + //language=JSON + filter: `{ + "type": "TEXT", + "args": ["text_field", "developer"] + }`, + count: 2, + }, + { + //language=JSON + filter: `{ + "type": "ILIKE", + "args": ["string_field", "test*"] + }`, + count: 3, + }, + { + //language=JSON + filter: `{ + "type": "ILIKE", + "args": ["string_field", "Test*"] + }`, + count: 3, + }, + { + //language=JSON + filter: `{ + "type": "ILIKE", + "args": ["string_field", "*test1"] + }`, + count: 1, + }, + { + // Test ILIKE with wildcard matching across space - pattern with space and wildcard + //language=JSON + filter: `{ + "type": "ILIKE", + "args": ["string_field", "test1 *"] + }`, + count: 1, + }, + { + // Test ILIKE with wildcard matching across space - wildcard before space + //language=JSON + filter: `{ + "type": "ILIKE", + "args": ["string_field", "* test1"] + }`, + count: 1, + }, + { + // Test ILIKE with wildcard at both beginning and end - should match "test1 test1" + //language=JSON + filter: `{ + "type": "ILIKE", + "args": ["string_field", "*1 test*"] + }`, + count: 1, + }, + { + // Test LIKE (case-sensitive) with exact case match + //language=JSON + filter: `{ + "type": "LIKE", + "args": ["string_field", "test*"] + }`, + count: 3, + }, + { + // Test LIKE (case-sensitive) with wrong case - should NOT match + //language=JSON + filter: `{ + "type": "LIKE", + "args": ["string_field", "Test*"] + }`, + count: 0, + }, + { + // Test LIKE (case-sensitive) with wildcard at end + //language=JSON + filter: `{ + "type": "LIKE", + "args": ["string_field", "test1 *"] + }`, + count: 1, + }, + { + // Test LIKE (case-sensitive) with wildcard at beginning + //language=JSON + filter: `{ + "type": "LIKE", + "args": ["string_field", "*test1"] + }`, + count: 1, + }, + { + // Test LIKE (case-sensitive) with wildcard at both ends + //language=JSON + filter: `{ + "type": "LIKE", + "args": ["string_field", "*1 test*"] + }`, + count: 1, + }, + { + // Test IN operator - multiple values + //language=JSON + filter: `{ + "type": "IN", + "args": ["string_field", "test1 test1", "test2 test2", "test4"] + }`, + count: 2, + }, + { + // Test IN operator - single value + //language=JSON + filter: `{ + "type": "IN", + "args": ["string_field", "test3 test3"] + }`, + count: 1, + }, + { + // Test IN operator - no matches + //language=JSON + filter: `{ + "type": "IN", + "args": ["string_field", "test4", "test5"] + }`, + count: 0, + }, + { + // Test AND operator - two EQ conditions + //language=JSON + filter: `{ + "type": "AND", + "children": [ + {"type": "EQ", "args": ["string_field", "test1 test1"]}, + {"type": "EQ", "args": ["text_field", "Developers like IDEs"]} + ] + }`, + count: 1, + }, + { + // Test AND operator - EQ and TEXT + //language=JSON + filter: `{ + "type": "AND", + "children": [ + {"type": "EQ", "args": ["string_field", "test2 test2"]}, + {"type": "TEXT", "args": ["text_field", "Development"]} + ] + }`, + count: 1, + }, + { + // Test AND operator - no matches (impossible condition) + //language=JSON + filter: `{ + "type": "AND", + "children": [ + {"type": "EQ", "args": ["string_field", "test1 test1"]}, + {"type": "EQ", "args": ["string_field", "test2 test2"]} + ] + }`, + count: 0, + }, + { + // Test OR operator - two EQ conditions + //language=JSON + filter: `{ + "type": "OR", + "children": [ + {"type": "EQ", "args": ["string_field", "test1 test1"]}, + {"type": "EQ", "args": ["string_field", "test2 test2"]} + ] + }`, + count: 2, + }, + { + // Test OR operator - three conditions + //language=JSON + filter: `{ + "type": "OR", + "children": [ + {"type": "EQ", "args": ["string_field", "test1 test1"]}, + {"type": "EQ", "args": ["string_field", "test2 test2"]}, + {"type": "EQ", "args": ["string_field", "test3 test3"]} + ] + }`, + count: 3, + }, + { + // Test complex: (test1 OR test2) AND has "like" in text + //language=JSON + filter: `{ + "type": "AND", + "children": [ + { + "type": "OR", + "children": [ + {"type": "EQ", "args": ["string_field", "test1 test1"]}, + {"type": "EQ", "args": ["string_field", "test2 test2"]} + ] + }, + {"type": "TEXT", "args": ["text_field", "like"]} + ] + }`, + count: 2, + }, + { + // Test complex: (ILIKE wildcard) AND (IN multiple values) + //language=JSON + filter: `{ + "type": "AND", + "children": [ + {"type": "ILIKE", "args": ["string_field", "test*"]}, + {"type": "IN", "args": ["string_field", "test1 test1", "test2 test2"]} + ] + }`, + count: 2, + }, + { + // Test GT on string field - lexicographic comparison + //language=JSON + filter: `{ + "type": "GT", + "args": ["string_field", "test1 test1"] + }`, + count: 2, + }, + { + // Test GE on string field - lexicographic comparison + //language=JSON + filter: `{ + "type": "GE", + "args": ["string_field", "test2 test2"] + }`, + count: 2, + }, + { + // Test LT on string field - lexicographic comparison + //language=JSON + filter: `{ + "type": "LT", + "args": ["string_field", "test3 test3"] + }`, + count: 2, + }, + { + // Test LE on string field - lexicographic comparison + //language=JSON + filter: `{ + "type": "LE", + "args": ["string_field", "test2 test2"] + }`, + count: 2, + }, + { + // Test GT on string field - no matches + //language=JSON + filter: `{ + "type": "GT", + "args": ["string_field", "test3 test3"] + }`, + count: 0, + }, + { + // Test LT on string field - no matches + //language=JSON + filter: `{ + "type": "LT", + "args": ["string_field", "test1 test1"] + }`, + count: 0, + }, + } + + collection := SetupAtlasDB(t, ctx, atlasClient) + InsertDocumentsOrFail(t, collection, ctx, documents) + + // Create search index with explicit field mappings + // Index fields with multiple types (like ES multi-fields): + // - type: "string" for text/wildcard search + // - type: "token" for exact matching with equals operator + searchIndexModel := mongo.SearchIndexModel{ + Definition: bson.D{ + // Define custom analyzer for case-insensitive keyword matching + {"analyzers", bson.A{ + bson.D{ + {"name", "caseInsensitiveKeyword"}, + {"tokenizer", bson.D{ + {"type", "keyword"}, + }}, + {"tokenFilters", bson.A{ + bson.D{{"type", "lowercase"}}, + }}, + }, + }}, + {"mappings", bson.D{ + {"dynamic", false}, + {"fields", bson.D{ + // string_field: indexed as both string (for wildcard) and token (for equals) + {"string_field", bson.A{ + // String type with standard analyzer (for TEXT queries) and keyword multi-analyzers (for LIKE/ILIKE) + bson.D{ + {"type", "string"}, + {"analyzer", "lucene.standard"}, + {"multi", bson.D{ + {"keywordAnalyzer", bson.D{ + {"type", "string"}, + {"analyzer", "caseInsensitiveKeyword"}, + }}, + {"caseSensitiveKeywordAnalyzer", bson.D{ + {"type", "string"}, + {"analyzer", "lucene.keyword"}, + }}, + }}, + }, + // Token type for exact EQ/IN matching + bson.D{{"type", "token"}}, + }}, + {"array_field", bson.D{ + // String supports (moreLikeThis, phrase, queryString, regex, span, text, wildcard) + {"type", "string"}, + }}, + {"nullable_string_field", bson.A{ + // String type with standard analyzer (for TEXT queries) and keyword multi-analyzers (for LIKE/ILIKE) + bson.D{ + {"type", "string"}, + {"analyzer", "lucene.standard"}, + {"multi", bson.D{ + {"keywordAnalyzer", bson.D{ + {"type", "string"}, + {"analyzer", "caseInsensitiveKeyword"}, + }}, + {"caseSensitiveKeywordAnalyzer", bson.D{ + {"type", "string"}, + {"analyzer", "lucene.keyword"}, + }}, + }}, + }, + // Token type for exact EQ/IN matching + bson.D{{"type", "token"}}, + }}, + // text_field: indexed for both text search (with english analyzer) and exact matching + {"text_field", bson.A{ + bson.D{ + // String supports (moreLikeThis, phrase, queryString, regex, span, text, wildcard) + {"type", "string"}, + {"analyzer", "lucene.english"}, + }, + bson.D{ + // Token supports (equals, facet, in, range) + {"type", "token"}, + {"normalizer", "none"}, // case-sensitive exact matching + }, + }}, + // uuid_field: indexed as token for equals and in operations + {"uuid_field", bson.D{ + {"type", "uuid"}, + }}, + // date_field: indexed as token for range and equality operations + {"date_field", bson.D{ + {"type", "date"}, + }}, + }}, + }}, + }, + Options: nil, + } + + indexName, err := collection.SearchIndexes().CreateOne(ctx, searchIndexModel) + if err != nil { + t.Fatalf("Failed to create search index: %v", err) + } + t.Logf("Created search index: %s", indexName) + + // Wait for index to be ready by polling + err = waitForAtlasSearchIndex(ctx, collection, indexName, 30*time.Second) + if err != nil { + t.Fatalf("Failed to wait for search index: %v", err) + } + t.Logf("Search index %s is ready", indexName) + + for _, tc := range testCases { + t.Run(fmt.Sprintf("%s", tc.filter), func(t *testing.T) { + /* + Fixture Setup + */ + + /* + Execute SUT + */ + + // Create query builder + // Configure multi-analyzers for fields that support LIKE/ILIKE + var qb epsearchast.SemanticReducer[bson.D] = DefaultAtlasSearchQueryBuilder{ + FieldToMultiAnalyzers: map[string]*StringMultiAnalyzers{ + "string_field": { + WildcardCaseInsensitive: "keywordAnalyzer", + WildcardCaseSensitive: "caseSensitiveKeywordAnalyzer", + }, + "nullable_string_field": { + WildcardCaseInsensitive: "keywordAnalyzer", + WildcardCaseSensitive: "caseSensitiveKeywordAnalyzer", + }, + }, + } + + // Create Query Object + ast, err := epsearchast.GetAst(tc.filter) + if err != nil { + t.Fatalf("Failed to get filter: %v", err) + } + + query, err := epsearchast.SemanticReduceAst(ast, qb) + + if err != nil { + t.Fatalf("Failed to reduce AST: %v", err) + } + + /* + Verification + */ + + // Execute the search using aggregation pipeline + pipeline := mongo.Pipeline{ + {{Key: "$search", Value: query}}, + } + + cursor, err := collection.Aggregate(ctx, pipeline) + if err != nil { + t.Fatalf("Failed to execute search: %v", err) + } + defer cursor.Close(ctx) + + // Count results + var results []bson.M + err = cursor.All(ctx, &results) + if err != nil { + t.Fatalf("Failed to read results: %v", err) + } + + count := int64(len(results)) + + // Assert the expected count + expectedCount := tc.count + if count != expectedCount { + t.Errorf("Expected count %d, but got %d", expectedCount, count) + } + + fmt.Printf("Test passed. Documents matching filter: %d\n", count) + + }) + // Verification + } + +} + +func SetupAtlasDB(t *testing.T, ctx context.Context, atlasClient *mongo.Client) *mongo.Collection { + db := atlasClient.Database("testdb") + + collName := t.Name() + + if len(collName) > 64 { + collName = collName[0:64] + } + + collection := db.Collection(collName) + + collection.Drop(ctx) + + return collection +} + +// waitForAtlasSearchIndex polls until the search index appears in the list +// Note: MongoDB Community Search (used in docker) doesn't provide status like Atlas Cloud, +// so we just wait for the index to appear in the list and give it a moment to stabilize +func waitForAtlasSearchIndex(ctx context.Context, collection *mongo.Collection, indexName string, timeout time.Duration) error { + deadline := time.Now().Add(timeout) + pollInterval := 100 * time.Millisecond + indexFound := false + + for time.Now().Before(deadline) { + // List all search indexes + cursor, err := collection.SearchIndexes().List(ctx, options.SearchIndexes()) + if err != nil { + return fmt.Errorf("failed to list search indexes: %w", err) + } + + // Find the index we're waiting for + var indexes []bson.M + if err := cursor.All(ctx, &indexes); err != nil { + return fmt.Errorf("failed to read search indexes: %w", err) + } + + for _, index := range indexes { + name, ok := index["name"].(string) + if !ok { + continue + } + + if name == indexName { + // Check for status field (Atlas Cloud has this) + if status, ok := index["status"].(string); ok { + // If we have status, use it + if status == "READY" { + return nil + } + if status == "FAILED" { + return fmt.Errorf("search index %s failed to build", indexName) + } + // Index is still building (INITIAL or BUILDING) + indexFound = true + break + } else { + // No status field (MongoDB Community Search) + // Index exists in list, so it should be ready + // Wait a bit longer to let it stabilize + if !indexFound { + indexFound = true + time.Sleep(2 * time.Second) + } + return nil + } + } + } + + // Wait before polling again + time.Sleep(pollInterval) + } + + if indexFound { + return fmt.Errorf("search index %s found but never became ready", indexName) + } + return fmt.Errorf("timeout waiting for search index %s to appear", indexName) +} diff --git a/external/epsearchast/v3/mongo/mongo_query_builder.go b/mongo/mongo_query_builder.go similarity index 90% rename from external/epsearchast/v3/mongo/mongo_query_builder.go rename to mongo/mongo_query_builder.go index 4c79616..0839f90 100644 --- a/external/epsearchast/v3/mongo/mongo_query_builder.go +++ b/mongo/mongo_query_builder.go @@ -1,18 +1,19 @@ -package epsearchast_v3_mongo +package astmongo import ( "fmt" - epsearchast_v3 "github.com/elasticpath/epcc-search-ast-helper/external/epsearchast/v3" "regexp" "strings" + + "github.com/elasticpath/epcc-search-ast-helper" ) -import "go.mongodb.org/mongo-driver/bson" +import "go.mongodb.org/mongo-driver/v2/bson" type DefaultMongoQueryBuilder struct { - FieldTypes map[string]epsearchast_v3.FieldType + FieldTypes map[string]epsearchast.FieldType } -var _ epsearchast_v3.SemanticReducer[bson.D] = (*DefaultMongoQueryBuilder)(nil) +var _ epsearchast.SemanticReducer[bson.D] = (*DefaultMongoQueryBuilder)(nil) func (d DefaultMongoQueryBuilder) PostVisitAnd(rs []*bson.D) (*bson.D, error) { // https://www.mongodb.com/docs/manual/reference/operator/query/and/ @@ -89,7 +90,7 @@ func (d DefaultMongoQueryBuilder) VisitGt(first, second string) (*bson.D, error) func (d DefaultMongoQueryBuilder) VisitLike(first, second string) (*bson.D, error) { if v, ok := d.FieldTypes[first]; ok { - if v != epsearchast_v3.String { + if v != epsearchast.String { return nil, fmt.Errorf("like() operator is only supported for string fields, and [%s] is not a string", first) } } @@ -99,7 +100,7 @@ func (d DefaultMongoQueryBuilder) VisitLike(first, second string) (*bson.D, erro func (d DefaultMongoQueryBuilder) VisitILike(first, second string) (*bson.D, error) { if v, ok := d.FieldTypes[first]; ok { - if v != epsearchast_v3.String { + if v != epsearchast.String { return nil, fmt.Errorf("ilike() operator is only supported for string fields, and [%s] is not a string", first) } } @@ -154,7 +155,7 @@ func (d DefaultMongoQueryBuilder) VisitContainsAll(args ...string) (*bson.D, err func (d DefaultMongoQueryBuilder) VisitText(first, second string) (*bson.D, error) { if v, ok := d.FieldTypes[first]; ok { - if v != epsearchast_v3.String { + if v != epsearchast.String { return nil, fmt.Errorf("text() operator is only supported for string fields, and [%s] is not a string", first) } } @@ -198,7 +199,7 @@ func (d DefaultMongoQueryBuilder) ProcessLikeWildcards(valString string) string func (d DefaultMongoQueryBuilder) ValidateValue(fieldName string, v string) error { if fieldType, ok := d.FieldTypes[fieldName]; ok { - return epsearchast_v3.ValidateValue(fieldType, v) + return epsearchast.ValidateValue(fieldType, v) } return nil @@ -206,7 +207,7 @@ func (d DefaultMongoQueryBuilder) ValidateValue(fieldName string, v string) erro func (d DefaultMongoQueryBuilder) ValidateValues(fieldName string, v ...string) error { if fieldType, ok := d.FieldTypes[fieldName]; ok { - return epsearchast_v3.ValidateAllValues(fieldType, v...) + return epsearchast.ValidateAllValues(fieldType, v...) } else { return nil } @@ -215,7 +216,7 @@ func (d DefaultMongoQueryBuilder) ValidateValues(fieldName string, v ...string) func (d DefaultMongoQueryBuilder) ConvertValue(fieldName string, v string) interface{} { if fieldType, ok := d.FieldTypes[fieldName]; ok { - v, _ := epsearchast_v3.Convert(fieldType, v) + v, _ := epsearchast.Convert(fieldType, v) return v } @@ -225,12 +226,12 @@ func (d DefaultMongoQueryBuilder) ConvertValue(fieldName string, v string) inter func (d DefaultMongoQueryBuilder) ConvertValues(fieldName string, v ...string) []interface{} { if fieldType, ok := d.FieldTypes[fieldName]; ok { - v, _ := epsearchast_v3.ConvertAll(fieldType, v...) + v, _ := epsearchast.ConvertAll(fieldType, v...) return v } else { // We need to do the conversion to string, because we got a []string in, and need to // return a []interface{} - v, _ := epsearchast_v3.ConvertAll(epsearchast_v3.String, v...) + v, _ := epsearchast.ConvertAll(epsearchast.String, v...) return v } } diff --git a/external/epsearchast/v3/mongo/mongo_query_builder_int_test.go b/mongo/mongo_query_builder_int_test.go similarity index 95% rename from external/epsearchast/v3/mongo/mongo_query_builder_int_test.go rename to mongo/mongo_query_builder_int_test.go index 22b9ece..4b7b296 100644 --- a/external/epsearchast/v3/mongo/mongo_query_builder_int_test.go +++ b/mongo/mongo_query_builder_int_test.go @@ -1,16 +1,17 @@ -package epsearchast_v3_mongo +package astmongo import ( "context" "fmt" - epsearchast_v3 "github.com/elasticpath/epcc-search-ast-helper/external/epsearchast/v3" - "go.mongodb.org/mongo-driver/bson" - "go.mongodb.org/mongo-driver/mongo" - "go.mongodb.org/mongo-driver/mongo/options" "log" "os" "testing" "time" + + "github.com/elasticpath/epcc-search-ast-helper" + "go.mongodb.org/mongo-driver/v2/bson" + "go.mongodb.org/mongo-driver/v2/mongo" + "go.mongodb.org/mongo-driver/v2/mongo/options" ) var client *mongo.Client @@ -20,7 +21,7 @@ func TestMain(m *testing.M) { defer cancel() var err error - client, err = mongo.Connect(ctx, options.Client().ApplyURI("mongodb://admin:admin@localhost:20002")) + client, err = mongo.Connect(options.Client().ApplyURI("mongodb://admin:admin@localhost:20002")) if err != nil { log.Fatalf("Failed to connect to MongoDB: %v", err) } @@ -642,12 +643,7 @@ func TestSmokeTestMongoWithFilters(t *testing.T) { } for _, tc := range testCases { - ast, err := epsearchast_v3.GetAst(tc.filter) - if err != nil { - t.Fatalf("Failed to get filter: %v", err) - } - - t.Run(fmt.Sprintf("%s", ast.AsFilter()), func(t *testing.T) { + t.Run(fmt.Sprintf("%s", tc.filter), func(t *testing.T) { /* Fixture Setup */ @@ -662,15 +658,16 @@ func TestSmokeTestMongoWithFilters(t *testing.T) { // Perform a count query with a filter // Create query builder - var qb epsearchast_v3.SemanticReducer[bson.D] = DefaultMongoQueryBuilder{} + var qb epsearchast.SemanticReducer[bson.D] = DefaultMongoQueryBuilder{} // Create Query Object - ast, err := epsearchast_v3.GetAst(tc.filter) + ast, err := epsearchast.GetAst(tc.filter) if err != nil { t.Fatalf("Failed to get filter: %v", err) } - query, err := epsearchast_v3.SemanticReduceAst(ast, qb) + fmt.Printf("Filter: %s", ast.AsFilter()) + query, err := epsearchast.SemanticReduceAst(ast, qb) if err != nil { t.Fatalf("Failed to get filter: %v", err) diff --git a/external/epsearchast/v3/mongo/mongo_query_builder_test.go b/mongo/mongo_query_builder_test.go similarity index 74% rename from external/epsearchast/v3/mongo/mongo_query_builder_test.go rename to mongo/mongo_query_builder_test.go index 5bfe575..3b177b2 100644 --- a/external/epsearchast/v3/mongo/mongo_query_builder_test.go +++ b/mongo/mongo_query_builder_test.go @@ -1,11 +1,11 @@ -package epsearchast_v3_mongo +package astmongo import ( "encoding/json" "fmt" - epsearchast_v3 "github.com/elasticpath/epcc-search-ast-helper/external/epsearchast/v3" + "github.com/elasticpath/epcc-search-ast-helper" "github.com/stretchr/testify/require" - "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/v2/bson" "strings" "testing" ) @@ -46,14 +46,14 @@ func TestSimpleBinaryOperatorFiltersGeneratesCorrectFilter(t *testing.T) { "args": [ "amount", "5"] }`, binOp.AstOp) - astNode, err := epsearchast_v3.GetAst(astJson) + astNode, err := epsearchast.GetAst(astJson) - var qb epsearchast_v3.SemanticReducer[bson.D] = DefaultMongoQueryBuilder{} + var qb epsearchast.SemanticReducer[bson.D] = DefaultMongoQueryBuilder{} expectedSearchJson := fmt.Sprintf(`{"amount":{"%s":"5"}}`, binOp.MongoOp) // Execute SUT - queryObj, err := epsearchast_v3.SemanticReduceAst(astNode, qb) + queryObj, err := epsearchast.SemanticReduceAst(astNode, qb) // Verification @@ -78,15 +78,15 @@ func TestSimpleBinaryOperatorFiltersGeneratesCorrectFilterWithInt64TypeConversio "args": [ "amount", "5"] }`, binOp.AstOp) - astNode, err := epsearchast_v3.GetAst(astJson) + astNode, err := epsearchast.GetAst(astJson) - var qb epsearchast_v3.SemanticReducer[bson.D] = DefaultMongoQueryBuilder{FieldTypes: map[string]epsearchast_v3.FieldType{"amount": epsearchast_v3.Int64}} + var qb epsearchast.SemanticReducer[bson.D] = DefaultMongoQueryBuilder{FieldTypes: map[string]epsearchast.FieldType{"amount": epsearchast.Int64}} // https://www.mongodb.com/docs/manual/reference/mongodb-extended-json/#mongodb-bsontype-Int64 expectedSearchJson := fmt.Sprintf(`{"amount":{"%s":{"$numberLong":"5"}}}`, binOp.MongoOp) // Execute SUT - queryObj, err := epsearchast_v3.SemanticReduceAst(astNode, qb) + queryObj, err := epsearchast.SemanticReduceAst(astNode, qb) // Verification @@ -111,15 +111,15 @@ func TestSimpleBinaryOperatorFiltersGeneratesCorrectFilterWithFloat64TypeConvers "args": [ "amount", "5"] }`, binOp.AstOp) - astNode, err := epsearchast_v3.GetAst(astJson) + astNode, err := epsearchast.GetAst(astJson) - var qb epsearchast_v3.SemanticReducer[bson.D] = DefaultMongoQueryBuilder{FieldTypes: map[string]epsearchast_v3.FieldType{"amount": epsearchast_v3.Float64}} + var qb epsearchast.SemanticReducer[bson.D] = DefaultMongoQueryBuilder{FieldTypes: map[string]epsearchast.FieldType{"amount": epsearchast.Float64}} // https://www.mongodb.com/docs/manual/reference/mongodb-extended-json/#mongodb-bsontype-Int64 expectedSearchJson := fmt.Sprintf(`{"amount":{"%s":{"$numberDouble":"5.0"}}}`, binOp.MongoOp) // Execute SUT - queryObj, err := epsearchast_v3.SemanticReduceAst(astNode, qb) + queryObj, err := epsearchast.SemanticReduceAst(astNode, qb) // Verification @@ -144,15 +144,15 @@ func TestSimpleBinaryOperatorFiltersGeneratesCorrectFilterWithBooleanTypeConvers "args": [ "paid", "true"] }`, binOp.AstOp) - astNode, err := epsearchast_v3.GetAst(astJson) + astNode, err := epsearchast.GetAst(astJson) - var qb epsearchast_v3.SemanticReducer[bson.D] = DefaultMongoQueryBuilder{FieldTypes: map[string]epsearchast_v3.FieldType{"paid": epsearchast_v3.Boolean}} + var qb epsearchast.SemanticReducer[bson.D] = DefaultMongoQueryBuilder{FieldTypes: map[string]epsearchast.FieldType{"paid": epsearchast.Boolean}} // https://www.mongodb.com/docs/manual/reference/mongodb-extended-json/#mongodb-bsontype-Int64 expectedSearchJson := fmt.Sprintf(`{"paid":{"%s":true}}`, binOp.MongoOp) // Execute SUT - queryObj, err := epsearchast_v3.SemanticReduceAst(astNode, qb) + queryObj, err := epsearchast.SemanticReduceAst(astNode, qb) // Verification @@ -167,7 +167,7 @@ func TestSimpleBinaryOperatorFiltersGeneratesCorrectFilterWithBooleanTypeConvers } func TestSimpleBinaryOperatorFiltersGeneratesErrorWhenValueCantBeConverted(t *testing.T) { - for _, fieldType := range []epsearchast_v3.FieldType{epsearchast_v3.Int64, epsearchast_v3.Float64, epsearchast_v3.Boolean} { + for _, fieldType := range []epsearchast.FieldType{epsearchast.Int64, epsearchast.Float64, epsearchast.Boolean} { for _, binOp := range binOps { t.Run(fmt.Sprintf("%s %s", fieldType, binOp.AstOp), func(t *testing.T) { //Fixture Setup @@ -178,12 +178,12 @@ func TestSimpleBinaryOperatorFiltersGeneratesErrorWhenValueCantBeConverted(t *te "args": [ "foo", "Hello World!"] }`, binOp.AstOp) - astNode, err := epsearchast_v3.GetAst(astJson) + astNode, err := epsearchast.GetAst(astJson) - var qb epsearchast_v3.SemanticReducer[bson.D] = DefaultMongoQueryBuilder{FieldTypes: map[string]epsearchast_v3.FieldType{"foo": fieldType}} + var qb epsearchast.SemanticReducer[bson.D] = DefaultMongoQueryBuilder{FieldTypes: map[string]epsearchast.FieldType{"foo": fieldType}} // Execute SUT - _, err = epsearchast_v3.SemanticReduceAst(astNode, qb) + _, err = epsearchast.SemanticReduceAst(astNode, qb) // Verification errStr := fmt.Sprintf("invalid value for %s", fieldType) @@ -202,14 +202,14 @@ func TestTextBinaryOperatorFiltersGeneratesCorrectFilter(t *testing.T) { "args": [ "*", "computer"] }`, "TEXT") - astNode, err := epsearchast_v3.GetAst(astJson) + astNode, err := epsearchast.GetAst(astJson) - var qb epsearchast_v3.SemanticReducer[bson.D] = DefaultMongoQueryBuilder{} + var qb epsearchast.SemanticReducer[bson.D] = DefaultMongoQueryBuilder{} expectedSearchJson := `{"$text":{"$search":"computer"}}` // Execute SUT - queryObj, err := epsearchast_v3.SemanticReduceAst(astNode, qb) + queryObj, err := epsearchast.SemanticReduceAst(astNode, qb) // Verification @@ -231,14 +231,14 @@ func TestTextBinaryOperatorFiltersGeneratesErrorWhenNotAStringType(t *testing.T) "args": [ "*", "computer"] }`, "TEXT") - astNode, err := epsearchast_v3.GetAst(astJson) + astNode, err := epsearchast.GetAst(astJson) - var qb epsearchast_v3.SemanticReducer[bson.D] = DefaultMongoQueryBuilder{FieldTypes: map[string]epsearchast_v3.FieldType{ - "*": epsearchast_v3.Int64, + var qb epsearchast.SemanticReducer[bson.D] = DefaultMongoQueryBuilder{FieldTypes: map[string]epsearchast.FieldType{ + "*": epsearchast.Int64, }} // Execute SUT - _, err = epsearchast_v3.SemanticReduceAst(astNode, qb) + _, err = epsearchast.SemanticReduceAst(astNode, qb) // Verification require.ErrorContains(t, err, "text() operator is only supported for string fields") @@ -254,14 +254,14 @@ func TestLikeBinaryOperatorFiltersGeneratesErrorWhenNotAStringType(t *testing.T) "args": [ "foo", "52"] }`, "LIKE") - astNode, err := epsearchast_v3.GetAst(astJson) + astNode, err := epsearchast.GetAst(astJson) - var qb epsearchast_v3.SemanticReducer[bson.D] = DefaultMongoQueryBuilder{FieldTypes: map[string]epsearchast_v3.FieldType{ - "foo": epsearchast_v3.Int64, + var qb epsearchast.SemanticReducer[bson.D] = DefaultMongoQueryBuilder{FieldTypes: map[string]epsearchast.FieldType{ + "foo": epsearchast.Int64, }} // Execute SUT - _, err = epsearchast_v3.SemanticReduceAst(astNode, qb) + _, err = epsearchast.SemanticReduceAst(astNode, qb) // Verification require.ErrorContains(t, err, "like() operator is only supported for string fields") @@ -277,14 +277,14 @@ func TestILikeBinaryOperatorFiltersGeneratesErrorWhenNotAStringType(t *testing.T "args": [ "foo", "52"] }`, "ILIKE") - astNode, err := epsearchast_v3.GetAst(astJson) + astNode, err := epsearchast.GetAst(astJson) - var qb epsearchast_v3.SemanticReducer[bson.D] = DefaultMongoQueryBuilder{FieldTypes: map[string]epsearchast_v3.FieldType{ - "foo": epsearchast_v3.Int64, + var qb epsearchast.SemanticReducer[bson.D] = DefaultMongoQueryBuilder{FieldTypes: map[string]epsearchast.FieldType{ + "foo": epsearchast.Int64, }} // Execute SUT - _, err = epsearchast_v3.SemanticReduceAst(astNode, qb) + _, err = epsearchast.SemanticReduceAst(astNode, qb) // Verification require.ErrorContains(t, err, "like() operator is only supported for string fields") @@ -303,15 +303,15 @@ func TestILikeOperatorFiltersGeneratesCorrectFilter(t *testing.T) { "args": [ "amount", "5"] }`, astOp) - astNode, err := epsearchast_v3.GetAst(astJson) + astNode, err := epsearchast.GetAst(astJson) require.NoError(t, err) - var qb epsearchast_v3.SemanticReducer[bson.D] = DefaultMongoQueryBuilder{} + var qb epsearchast.SemanticReducer[bson.D] = DefaultMongoQueryBuilder{} expectedSearchJson := fmt.Sprintf(`{"amount":{"%s":"^5$","$options":"i"}}`, mongoOp) // Execute SUT - queryObj, err := epsearchast_v3.SemanticReduceAst(astNode, qb) + queryObj, err := epsearchast.SemanticReduceAst(astNode, qb) // Verification @@ -333,16 +333,16 @@ func TestContainsOperatorFiltersGeneratesCorrectFilter(t *testing.T) { "args": [ "favourite_colors", "red"] }` - astNode, err := epsearchast_v3.GetAst(astJson) + astNode, err := epsearchast.GetAst(astJson) require.NoError(t, err) - var qb epsearchast_v3.SemanticReducer[bson.D] = DefaultMongoQueryBuilder{} + var qb epsearchast.SemanticReducer[bson.D] = DefaultMongoQueryBuilder{} expectedSearchJson := `{"favourite_colors":{"$elemMatch":{"$eq":"red"}}}` // Execute SUT - queryObj, err := epsearchast_v3.SemanticReduceAst(astNode, qb) + queryObj, err := epsearchast.SemanticReduceAst(astNode, qb) // Verification @@ -365,14 +365,14 @@ func TestSimpleUnaryOperatorFiltersGeneratesCorrectFilter(t *testing.T) { "args": [ "amount"] }`, unaryOp.AstOp) - astNode, err := epsearchast_v3.GetAst(astJson) + astNode, err := epsearchast.GetAst(astJson) - var qb epsearchast_v3.SemanticReducer[bson.D] = DefaultMongoQueryBuilder{} + var qb epsearchast.SemanticReducer[bson.D] = DefaultMongoQueryBuilder{} expectedSearchJson := fmt.Sprintf(`{"amount":{%s}}`, unaryOp.MongoOp) // Execute SUT - queryObj, err := epsearchast_v3.SemanticReduceAst(astNode, qb) + queryObj, err := epsearchast.SemanticReduceAst(astNode, qb) // Verification @@ -397,16 +397,16 @@ func TestSimpleVariableOperatorFiltersGeneratesCorrectFilter(t *testing.T) { "args": [ "amount", "5", "6", "7"] }`, varOp.AstOp) - astNode, err := epsearchast_v3.GetAst(astJson) + astNode, err := epsearchast.GetAst(astJson) require.NoError(t, err) - var qb epsearchast_v3.SemanticReducer[bson.D] = DefaultMongoQueryBuilder{} + var qb epsearchast.SemanticReducer[bson.D] = DefaultMongoQueryBuilder{} expectedSearchJson := fmt.Sprintf(`{"amount":{"%s":["5","6","7"]}}`, varOp.MongoOp) // Execute SUT - queryObj, err := epsearchast_v3.SemanticReduceAst(astNode, qb) + queryObj, err := epsearchast.SemanticReduceAst(astNode, qb) // Verification @@ -431,16 +431,16 @@ func TestSimpleVariableOperatorFiltersGeneratesCorrectFilterWithInt64(t *testing "args": [ "amount", "5", "6", "7"] }`, varOp.AstOp) - astNode, err := epsearchast_v3.GetAst(astJson) + astNode, err := epsearchast.GetAst(astJson) require.NoError(t, err) - var qb epsearchast_v3.SemanticReducer[bson.D] = DefaultMongoQueryBuilder{FieldTypes: map[string]epsearchast_v3.FieldType{"amount": epsearchast_v3.Int64}} + var qb epsearchast.SemanticReducer[bson.D] = DefaultMongoQueryBuilder{FieldTypes: map[string]epsearchast.FieldType{"amount": epsearchast.Int64}} expectedSearchJson := fmt.Sprintf(`{"amount":{"%s":[{"$numberLong":"5"},{"$numberLong":"6"},{"$numberLong":"7"}]}}`, varOp.MongoOp) // Execute SUT - queryObj, err := epsearchast_v3.SemanticReduceAst(astNode, qb) + queryObj, err := epsearchast.SemanticReduceAst(astNode, qb) // Verification @@ -465,16 +465,16 @@ func TestSimpleVariableOperatorFiltersGeneratesCorrectFilterWithFloat64(t *testi "args": [ "amount", "5", "6", "7"] }`, varOp.AstOp) - astNode, err := epsearchast_v3.GetAst(astJson) + astNode, err := epsearchast.GetAst(astJson) require.NoError(t, err) - var qb epsearchast_v3.SemanticReducer[bson.D] = DefaultMongoQueryBuilder{FieldTypes: map[string]epsearchast_v3.FieldType{"amount": epsearchast_v3.Float64}} + var qb epsearchast.SemanticReducer[bson.D] = DefaultMongoQueryBuilder{FieldTypes: map[string]epsearchast.FieldType{"amount": epsearchast.Float64}} expectedSearchJson := fmt.Sprintf(`{"amount":{"%s":[{"$numberDouble":"5.0"},{"$numberDouble":"6.0"},{"$numberDouble":"7.0"}]}}`, varOp.MongoOp) // Execute SUT - queryObj, err := epsearchast_v3.SemanticReduceAst(astNode, qb) + queryObj, err := epsearchast.SemanticReduceAst(astNode, qb) // Verification @@ -502,16 +502,16 @@ func TestSimpleVariableOperatorFiltersGeneratesCorrectFilterWithBoolean(t *testi "args": [ "paid", "true", "false", "true", "true", "false"] }`, varOp.AstOp) - astNode, err := epsearchast_v3.GetAst(astJson) + astNode, err := epsearchast.GetAst(astJson) require.NoError(t, err) - var qb epsearchast_v3.SemanticReducer[bson.D] = DefaultMongoQueryBuilder{FieldTypes: map[string]epsearchast_v3.FieldType{"paid": epsearchast_v3.Boolean}} + var qb epsearchast.SemanticReducer[bson.D] = DefaultMongoQueryBuilder{FieldTypes: map[string]epsearchast.FieldType{"paid": epsearchast.Boolean}} expectedSearchJson := fmt.Sprintf(`{"paid":{"%s":[true,false,true,true,false]}}`, varOp.MongoOp) // Execute SUT - queryObj, err := epsearchast_v3.SemanticReduceAst(astNode, qb) + queryObj, err := epsearchast.SemanticReduceAst(astNode, qb) // Verification @@ -526,7 +526,7 @@ func TestSimpleVariableOperatorFiltersGeneratesCorrectFilterWithBoolean(t *testi } func TestSimpleVariableOperatorFiltersGeneratesErrorIfInvalidValue(t *testing.T) { - for _, fieldType := range []epsearchast_v3.FieldType{epsearchast_v3.Int64, epsearchast_v3.Float64, epsearchast_v3.Boolean} { + for _, fieldType := range []epsearchast.FieldType{epsearchast.Int64, epsearchast.Float64, epsearchast.Boolean} { for _, varOp := range varOps { t.Run(fmt.Sprintf("%s %s", fieldType, varOp.AstOp), func(t *testing.T) { // Yes also this test case is kind of silly, for booleans. @@ -539,14 +539,14 @@ func TestSimpleVariableOperatorFiltersGeneratesErrorIfInvalidValue(t *testing.T) "args": [ "amount", "Nothing!", "5", "7"] }`, varOp.AstOp) - astNode, err := epsearchast_v3.GetAst(astJson) + astNode, err := epsearchast.GetAst(astJson) require.NoError(t, err) - var qb epsearchast_v3.SemanticReducer[bson.D] = DefaultMongoQueryBuilder{FieldTypes: map[string]epsearchast_v3.FieldType{"amount": fieldType}} + var qb epsearchast.SemanticReducer[bson.D] = DefaultMongoQueryBuilder{FieldTypes: map[string]epsearchast.FieldType{"amount": fieldType}} // Execute SUT - _, err = epsearchast_v3.SemanticReduceAst(astNode, qb) + _, err = epsearchast.SemanticReduceAst(astNode, qb) // Verification @@ -574,10 +574,10 @@ func TestLikeFilterWildCards(t *testing.T) { "args": [ "status", "%s"] }`, astOp, astLiteral) - astNode, err := epsearchast_v3.GetAst(astJson) + astNode, err := epsearchast.GetAst(astJson) require.NoError(t, err) - var qb epsearchast_v3.SemanticReducer[bson.D] = DefaultMongoQueryBuilder{} + var qb epsearchast.SemanticReducer[bson.D] = DefaultMongoQueryBuilder{} jsonMongoRegexLiteral, err := json.Marshal(mongoRegexLiteral) require.NoError(t, err) @@ -585,7 +585,7 @@ func TestLikeFilterWildCards(t *testing.T) { expectedSearchJson := fmt.Sprintf(`{"status":{"%s":%s}}`, mongoOp, jsonMongoRegexLiteral) // Execute SUT - queryObj, err := epsearchast_v3.SemanticReduceAst(astNode, qb) + queryObj, err := epsearchast.SemanticReduceAst(astNode, qb) // Verification @@ -625,10 +625,10 @@ func TestILikeFilterWildCards(t *testing.T) { "args": [ "status", "%s"] }`, astOp, astLiteral) - astNode, err := epsearchast_v3.GetAst(astJson) + astNode, err := epsearchast.GetAst(astJson) require.NoError(t, err) - var qb epsearchast_v3.SemanticReducer[bson.D] = DefaultMongoQueryBuilder{} + var qb epsearchast.SemanticReducer[bson.D] = DefaultMongoQueryBuilder{} jsonMongoRegexLiteral, err := json.Marshal(mongoRegexLiteral) require.NoError(t, err) @@ -636,7 +636,7 @@ func TestILikeFilterWildCards(t *testing.T) { expectedSearchJson := fmt.Sprintf(`{"status":{"%s":%s,"$options":"i"}}`, mongoOp, jsonMongoRegexLiteral) // Execute SUT - queryObj, err := epsearchast_v3.SemanticReduceAst(astNode, qb) + queryObj, err := epsearchast.SemanticReduceAst(astNode, qb) // Verification @@ -701,13 +701,13 @@ func TestSimpleRecursiveStructure(t *testing.T) { } `, "\n ") - astNode, err := epsearchast_v3.GetAst(jsonTxt) + astNode, err := epsearchast.GetAst(jsonTxt) require.NoError(t, err) - var qb epsearchast_v3.SemanticReducer[bson.D] = DefaultMongoQueryBuilder{} + var qb epsearchast.SemanticReducer[bson.D] = DefaultMongoQueryBuilder{} // Execute SUT - queryObj, err := epsearchast_v3.SemanticReduceAst(astNode, qb) + queryObj, err := epsearchast.SemanticReduceAst(astNode, qb) // Verification @@ -761,12 +761,12 @@ func TestSimpleRecursiveStructureWithOverrideStruct(t *testing.T) { } `, "\n ") - astNode, err := epsearchast_v3.GetAst(jsonTxt) + astNode, err := epsearchast.GetAst(jsonTxt) require.NoError(t, err) // Execute SUT - var qb epsearchast_v3.SemanticReducer[bson.D] = &LowerCaseEmail{} - queryObj, err := epsearchast_v3.SemanticReduceAst(astNode, qb) + var qb epsearchast.SemanticReducer[bson.D] = &LowerCaseEmail{} + queryObj, err := epsearchast.SemanticReduceAst(astNode, qb) // Verification diff --git a/external/epsearchast/v3/reduce.go b/reduce.go similarity index 91% rename from external/epsearchast/v3/reduce.go rename to reduce.go index a2d0636..c0e49df 100644 --- a/external/epsearchast/v3/reduce.go +++ b/reduce.go @@ -1,11 +1,11 @@ -package epsearchast_v3 +package epsearchast // ReduceAst is a generic function that can be used to compute or build "something" about an AST. // // This function recursively calls the supplied f on each node of the tree, passing in the return value of all // child nodes as an argument. // -// Depending on what you are doing you may find that [epsearchast_v3.SemanticReduceAst] to be simpler. +// Depending on what you are doing you may find that [epsearchast.SemanticReduceAst] to be simpler. func ReduceAst[T any](a *AstNode, f func(*AstNode, []*T) (*T, error)) (*T, error) { if a == nil { return nil, nil diff --git a/external/epsearchast/v3/reduce_test.go b/reduce_test.go similarity index 93% rename from external/epsearchast/v3/reduce_test.go rename to reduce_test.go index aba3242..b385142 100644 --- a/external/epsearchast/v3/reduce_test.go +++ b/reduce_test.go @@ -1,4 +1,4 @@ -package epsearchast_v3 +package epsearchast import ( "github.com/stretchr/testify/require" diff --git a/external/epsearchast/v3/semantic_reduce.go b/semantic_reduce.go similarity index 90% rename from external/epsearchast/v3/semantic_reduce.go rename to semantic_reduce.go index 029ad10..835ec3e 100644 --- a/external/epsearchast/v3/semantic_reduce.go +++ b/semantic_reduce.go @@ -1,8 +1,8 @@ -package epsearchast_v3 +package epsearchast import "fmt" -// A SemanticReducer is essentially collection of functions that make it easier to reduce things that working with [epsearchast_v3.AstNode]'s directly. +// A SemanticReducer is essentially collection of functions that make it easier to reduce things that working with [epsearchast.AstNode]'s directly. // // It provides an individual method for each allowed keyword in the AST, which can make some transforms easier. In particular // only conjunction operators are required to handle the child arguments, and most other types have there arguments passed in the right type. @@ -24,7 +24,7 @@ type SemanticReducer[R any] interface { VisitIsNull(first string) (*R, error) } -// SemanticReduceAst adapts an epsearchast_v3.SemanticReducer for use with the epsearchast_v3.ReduceAst function. +// SemanticReduceAst adapts an epsearchast.SemanticReducer for use with the epsearchast.ReduceAst function. func SemanticReduceAst[T any](a *AstNode, v SemanticReducer[T]) (*T, error) { f := func(a *AstNode, t []*T) (*T, error) { switch a.NodeType { diff --git a/external/epsearchast/v3/semantic_reduce_test.go b/semantic_reduce_test.go similarity index 98% rename from external/epsearchast/v3/semantic_reduce_test.go rename to semantic_reduce_test.go index 803f964..51ea111 100644 --- a/external/epsearchast/v3/semantic_reduce_test.go +++ b/semantic_reduce_test.go @@ -1,4 +1,4 @@ -package epsearchast_v3 +package epsearchast import ( "github.com/stretchr/testify/require" diff --git a/external/epsearchast/v3/type_conversions.go b/type_conversions.go similarity index 98% rename from external/epsearchast/v3/type_conversions.go rename to type_conversions.go index 0dbb135..7252e22 100644 --- a/external/epsearchast/v3/type_conversions.go +++ b/type_conversions.go @@ -1,4 +1,4 @@ -package epsearchast_v3 +package epsearchast import ( "fmt" diff --git a/external/epsearchast/v3/util.go b/util.go similarity index 99% rename from external/epsearchast/v3/util.go rename to util.go index 794d3cf..c1207b0 100644 --- a/external/epsearchast/v3/util.go +++ b/util.go @@ -1,4 +1,4 @@ -package epsearchast_v3 +package epsearchast import ( "fmt" diff --git a/external/epsearchast/v3/util_test.go b/util_test.go similarity index 99% rename from external/epsearchast/v3/util_test.go rename to util_test.go index e82c523..6938a7f 100644 --- a/external/epsearchast/v3/util_test.go +++ b/util_test.go @@ -1,4 +1,4 @@ -package epsearchast_v3 +package epsearchast import ( "fmt" diff --git a/external/epsearchast/v3/validate.go b/validate.go similarity index 99% rename from external/epsearchast/v3/validate.go rename to validate.go index 6abd543..23938ef 100644 --- a/external/epsearchast/v3/validate.go +++ b/validate.go @@ -1,4 +1,4 @@ -package epsearchast_v3 +package epsearchast import "fmt" diff --git a/external/epsearchast/v3/validate_test.go b/validate_test.go similarity index 99% rename from external/epsearchast/v3/validate_test.go rename to validate_test.go index 96aa334..3511e43 100644 --- a/external/epsearchast/v3/validate_test.go +++ b/validate_test.go @@ -1,4 +1,4 @@ -package epsearchast_v3 +package epsearchast import ( "fmt" diff --git a/external/epsearchast/v3/validating_visitor.go b/validating_visitor.go similarity index 99% rename from external/epsearchast/v3/validating_visitor.go rename to validating_visitor.go index 165602f..177fa40 100644 --- a/external/epsearchast/v3/validating_visitor.go +++ b/validating_visitor.go @@ -1,4 +1,4 @@ -package epsearchast_v3 +package epsearchast import ( "fmt"