diff --git a/.dockerignore b/.dockerignore index 848e92f..45b252c 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,10 +2,11 @@ .github/** ci/** docs/** -testdata/** +**/testdata/** Makefile README.md LICENSE.txt CHANGELOG.md Dockerfile -docker-compose.yaml \ No newline at end of file +docker-compose.yaml +*_test.go \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a78be7..5addc62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +- Add nested filtering support for nested flattened mappings ([#85](https://github.com/hasura/ndc-elasticsearch/pull/85)) + ## [1.8.0] - Add basic query (no operator) support for unsupported object types ([#83](https://github.com/hasura/ndc-elasticsearch/pull/83)) diff --git a/connector/connector.go b/connector/connector.go index d883294..c9243bc 100644 --- a/connector/connector.go +++ b/connector/connector.go @@ -72,6 +72,9 @@ func (c *Connector) GetCapabilities(configuration *types.Configuration) schema.C FilterBy: schema.LeafCapability{}, Aggregates: schema.LeafCapability{}, }, + Exists: schema.ExistsCapabilities{ + NestedCollections: schema.LeafCapability{}, + }, }, }, } diff --git a/connector/filter.go b/connector/filter.go index bc2cf4b..37fa641 100644 --- a/connector/filter.go +++ b/connector/filter.go @@ -1,6 +1,7 @@ package connector import ( + "fmt" "strings" "github.com/hasura/ndc-elasticsearch/internal" @@ -11,10 +12,16 @@ import ( // prepareFilterQuery prepares a filter query based on the given expression. func prepareFilterQuery(expression schema.Expression, state *types.State, collection string) (map[string]interface{}, error) { filter := make(map[string]interface{}) - switch expr := expression.Interface().(type) { + columnPath, predicate := getPredicate(expression) + + switch expr := predicate.Interface().(type) { case *schema.ExpressionUnaryComparisonOperator: + fieldPath := strings.Split(columnPath, ".") + expr.Column.FieldPath = fieldPath return handleExpressionUnaryComparisonOperator(expr, state, collection) case *schema.ExpressionBinaryComparisonOperator: + fieldPath := strings.Split(columnPath, ".") + expr.Column.FieldPath = fieldPath return handleExpressionBinaryComparisonOperator(expr, state, collection) case *schema.ExpressionAnd: queries := make([]map[string]interface{}, 0) @@ -59,10 +66,52 @@ func prepareFilterQuery(expression schema.Expression, state *types.State, collec } } +// getPredicate checks if a schema.Expression has nested filtering +// if it does, it traverses the schema.Expression recursively until it finds a non-nested query predicate +func getPredicate(expression schema.Expression) (string, schema.Expression) { + if nested, fieldName := requiresNestedFiltering(expression); nested { + expressionPredicate, ok := expression["predicate"].(schema.Expression) + if !ok { + return "", nil + } + + columnPathPostfix, predicate := getPredicate(expressionPredicate) + return fmt.Sprintf("%s.%s", fieldName, columnPathPostfix), predicate + } + switch expr := expression.Interface().(type) { + case *schema.ExpressionUnaryComparisonOperator: + return expr.Column.Name, expression + case *schema.ExpressionBinaryComparisonOperator: + return expr.Column.Name, expression + } + + return "", expression +} + +func requiresNestedFiltering(predicate schema.Expression) (requiresNestedFiltering bool, nestedFieldName string) { + inCollection, ok := predicate["in_collection"].(schema.ExistsInCollection) + if !ok { + return false, "" + } + collection, err := inCollection.AsNestedCollection() + if err != nil { + return false, "" + } + if collection.Type == "nested_collection" { + return true, collection.ColumnName + } + return false, "" +} + // handleExpressionUnaryComparisonOperator processes the unary comparison operator expression. func handleExpressionUnaryComparisonOperator(expr *schema.ExpressionUnaryComparisonOperator, state *types.State, collection string) (map[string]interface{}, error) { if expr.Operator == "is_null" { - fieldName, _ := joinFieldPath(state, expr.Column.FieldPath, expr.Column.Name, collection) + if len(expr.Column.FieldPath) == 0 || expr.Column.FieldPath[len(expr.Column.FieldPath)-1] != expr.Column.Name { + // if the column name is not the last element in fieldPath, we'll add it so that the fieldpath is complete + expr.Column.FieldPath = append(expr.Column.FieldPath, expr.Column.Name) + } + + fieldName := strings.Join(expr.Column.FieldPath, ".") value := map[string]interface{}{ "field": fieldName, } @@ -85,7 +134,12 @@ func handleExpressionBinaryComparisonOperator( state *types.State, collection string, ) (map[string]interface{}, error) { - fieldPath, nestedPath := joinFieldPath(state, expr.Column.FieldPath, expr.Column.Name, collection) + if len(expr.Column.FieldPath) == 0 || expr.Column.FieldPath[len(expr.Column.FieldPath)-1] != expr.Column.Name { + // if the column name is not the last element in fieldPath, we'll add it so that the fieldpath is complete + expr.Column.FieldPath = append(expr.Column.FieldPath, expr.Column.Name) + } + + fieldPath := strings.Join(expr.Column.FieldPath, ".") fieldType, fieldSubTypes, _, err := state.Configuration.GetFieldProperties(collection, fieldPath) if err != nil { return nil, schema.UnprocessableContentError("unable to get field types", map[string]any{ @@ -110,9 +164,10 @@ func handleExpressionBinaryComparisonOperator( expr.Operator: value, } - if nestedPath != "" { - filter = prepareNestedQuery(state, expr.Operator, value, fieldPath, len(expr.Column.FieldPath), collection) - } + // TOOD: re-enable + // if nestedPath != "" { + // filter = prepareNestedQuery(state, expr.Operator, value, fieldPath, len(expr.Column.FieldPath), collection) + // } return filter, nil } diff --git a/connector/filter_test.go b/connector/filter_test.go new file mode 100644 index 0000000..a253368 --- /dev/null +++ b/connector/filter_test.go @@ -0,0 +1,124 @@ +package connector + +import ( + "encoding/json" + "os" + "path/filepath" + "testing" + + "github.com/hasura/ndc-sdk-go/schema" + "github.com/stretchr/testify/assert" +) + +const filterTestsPath = "./testdata/filter_tests/" + +func TestGetPredicate(t *testing.T) { + tests := []struct { + name string + gotExpression string + expectedColumnPath string + wantPredicate string + }{ + { + name: "nested_001", + expectedColumnPath: "route.departure_airport.location.state", + }, + { + name: "nested_and_002", + expectedColumnPath: "", + }, + { + name: "003", + expectedColumnPath: "route.arrival_airport.location.coordinates.elevation", + }, + { + name: "004", + expectedColumnPath: "route.arrival_airport.terminals", + }, + { + name: "aggregations_005", + expectedColumnPath: "metric_value", + }, + { + name: "006", + expectedColumnPath: "name", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // setup test data + gotB, err := os.ReadFile(filepath.Join(filterTestsPath, "get_predicate", tt.name, "got.json")) + assert.NoError(t, err, "Error reading got.json file") + tt.gotExpression = string(gotB) + + wantB, err := os.ReadFile(filepath.Join(filterTestsPath, "get_predicate", tt.name, "want.json")) + assert.NoError(t, err, "Error reading want.json file") + tt.wantPredicate = string(wantB) + + // Convert tt.expression from JSON string to schema.Expression + var expression schema.Expression + err = json.Unmarshal([]byte(tt.gotExpression), &expression) + assert.NoError(t, err, "Error unmarshalling expression JSON") + + // Convert tt.expectedPredicate from JSON string to schema.Expression + var expectedPredicate schema.Expression + err = json.Unmarshal([]byte(tt.wantPredicate), &expectedPredicate) + assert.NoError(t, err, "Error unmarshalling expectedPredicate JSON") + + // Call getPredicate and validate results + path, result := getPredicate(expression) + assert.Equal(t, tt.expectedColumnPath, path) + assert.Equal(t, expectedPredicate, result) + + // uncomment to update want file + // err = os.WriteFile(filepath.Join(filterTestsPath, "get_predicate", tt.name, "want.json"), []byte(tt.wantPredicate), 0644) + // assert.NoError(t, err, "Error writing want file") + }) + } +} + +func TestRequiresNestedFiltering(t *testing.T) { + tests := []struct { + name string + predicate schema.Expression + expectedNested bool + expectedNestedField string + }{ + { + name: "Valid nested collection", + predicate: schema.Expression{ + "in_collection": schema.ExistsInCollection{ + "column_name": "my_nested_field", + "type": "nested_collection", + }, + }, + expectedNested: true, + expectedNestedField: "my_nested_field", + }, + { + name: "Missing in_collection key", + predicate: schema.Expression{ + "some_other_key": "some_value", + }, + expectedNested: false, + expectedNestedField: "", + }, + { + name: "Invalid in_collection type", + predicate: schema.Expression{ + "in_collection": "invalid_type", + }, + expectedNested: false, + expectedNestedField: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + nested, fieldName := requiresNestedFiltering(tt.predicate) + assert.Equal(t, tt.expectedNested, nested) + assert.Equal(t, tt.expectedNestedField, fieldName) + }) + } +} diff --git a/connector/query_test.go b/connector/query_test.go index 8c2a42a..39ab93b 100644 --- a/connector/query_test.go +++ b/connector/query_test.go @@ -141,6 +141,22 @@ var tests = []test{ group: "payments", name: "search_after_multiple_sorts", }, + { + group: "flights_nested_flattened", + name: "nested_filtering", + }, + { + group: "flights_nested_flattened", + name: "nested_filtering_and", + }, + { + group: "flights_nested_flattened", + name: "nested_filtering_range", + }, + { + group: "flights_nested_flattened", + name: "nested_filtering_or", + }, } func TestPrepareElasticsearchQuery(t *testing.T) { diff --git a/connector/testdata/filter_tests/get_predicate/003/got.json b/connector/testdata/filter_tests/get_predicate/003/got.json new file mode 100644 index 0000000..92d9ec4 --- /dev/null +++ b/connector/testdata/filter_tests/get_predicate/003/got.json @@ -0,0 +1,46 @@ +{ + "in_collection": { + "column_name": "route", + "type": "nested_collection" + }, + "predicate": { + "in_collection": { + "column_name": "arrival_airport", + "type": "nested_collection" + }, + "predicate": { + "in_collection": { + "column_name": "location", + "type": "nested_collection" + }, + "predicate": { + "in_collection": { + "column_name": "coordinates", + "type": "nested_collection" + }, + "predicate": { + "column": { + "type": "column", + "name": "elevation" + }, + "operator": "range", + "type": "binary_comparison_operator", + "value": { + "type": "scalar", + "value": { + "boost": "", + "gt": "", + "gte": "200", + "lt": "", + "lte": "" + } + } + }, + "type": "exists" + }, + "type": "exists" + }, + "type": "exists" + }, + "type": "exists" + } \ No newline at end of file diff --git a/connector/testdata/filter_tests/get_predicate/003/want.json b/connector/testdata/filter_tests/get_predicate/003/want.json new file mode 100644 index 0000000..2422168 --- /dev/null +++ b/connector/testdata/filter_tests/get_predicate/003/want.json @@ -0,0 +1,18 @@ +{ + "column": { + "type": "column", + "name": "elevation" + }, + "operator": "range", + "type": "binary_comparison_operator", + "value": { + "type": "scalar", + "value": { + "boost": "", + "gt": "", + "gte": "200", + "lt": "", + "lte": "" + } + } + } \ No newline at end of file diff --git a/connector/testdata/filter_tests/get_predicate/004/got.json b/connector/testdata/filter_tests/get_predicate/004/got.json new file mode 100644 index 0000000..0037541 --- /dev/null +++ b/connector/testdata/filter_tests/get_predicate/004/got.json @@ -0,0 +1,26 @@ +{ + "in_collection": { + "column_name": "route", + "type": "nested_collection" + }, + "predicate": { + "in_collection": { + "column_name": "arrival_airport", + "type": "nested_collection" + }, + "predicate": { + "column": { + "type": "column", + "name": "terminals" + }, + "operator": "match", + "type": "binary_comparison_operator", + "value": { + "type": "scalar", + "value": "2" + } + }, + "type": "exists" + }, + "type": "exists" + } \ No newline at end of file diff --git a/connector/testdata/filter_tests/get_predicate/004/want.json b/connector/testdata/filter_tests/get_predicate/004/want.json new file mode 100644 index 0000000..0daac01 --- /dev/null +++ b/connector/testdata/filter_tests/get_predicate/004/want.json @@ -0,0 +1,12 @@ +{ + "column": { + "type": "column", + "name": "terminals" + }, + "operator": "match", + "type": "binary_comparison_operator", + "value": { + "type": "scalar", + "value": "2" + } + } \ No newline at end of file diff --git a/connector/testdata/filter_tests/get_predicate/006/got.json b/connector/testdata/filter_tests/get_predicate/006/got.json new file mode 100644 index 0000000..e6d01c7 --- /dev/null +++ b/connector/testdata/filter_tests/get_predicate/006/got.json @@ -0,0 +1,12 @@ +{ + "column": { + "type": "column", + "name": "name" + }, + "operator": "prefix", + "type": "binary_comparison_operator", + "value": { + "type": "scalar", + "value": "J" + } + } \ No newline at end of file diff --git a/connector/testdata/filter_tests/get_predicate/006/want.json b/connector/testdata/filter_tests/get_predicate/006/want.json new file mode 100644 index 0000000..e6d01c7 --- /dev/null +++ b/connector/testdata/filter_tests/get_predicate/006/want.json @@ -0,0 +1,12 @@ +{ + "column": { + "type": "column", + "name": "name" + }, + "operator": "prefix", + "type": "binary_comparison_operator", + "value": { + "type": "scalar", + "value": "J" + } + } \ No newline at end of file diff --git a/connector/testdata/filter_tests/get_predicate/aggregations_005/got.json b/connector/testdata/filter_tests/get_predicate/aggregations_005/got.json new file mode 100644 index 0000000..c360240 --- /dev/null +++ b/connector/testdata/filter_tests/get_predicate/aggregations_005/got.json @@ -0,0 +1,18 @@ +{ + "column": { + "type": "column", + "name": "metric_value" + }, + "operator": "range", + "type": "binary_comparison_operator", + "value": { + "type": "scalar", + "value": { + "boost": "", + "gt": "", + "gte": "", + "lt": "75", + "lte": "" + } + } + } \ No newline at end of file diff --git a/connector/testdata/filter_tests/get_predicate/aggregations_005/want.json b/connector/testdata/filter_tests/get_predicate/aggregations_005/want.json new file mode 100644 index 0000000..c360240 --- /dev/null +++ b/connector/testdata/filter_tests/get_predicate/aggregations_005/want.json @@ -0,0 +1,18 @@ +{ + "column": { + "type": "column", + "name": "metric_value" + }, + "operator": "range", + "type": "binary_comparison_operator", + "value": { + "type": "scalar", + "value": { + "boost": "", + "gt": "", + "gte": "", + "lt": "75", + "lte": "" + } + } + } \ No newline at end of file diff --git a/connector/testdata/filter_tests/get_predicate/nested_001/got.json b/connector/testdata/filter_tests/get_predicate/nested_001/got.json new file mode 100644 index 0000000..94ed3c3 --- /dev/null +++ b/connector/testdata/filter_tests/get_predicate/nested_001/got.json @@ -0,0 +1,33 @@ +{ + "in_collection": { + "column_name": "route", + "type": "nested_collection" + }, + "predicate": { + "in_collection": { + "column_name": "departure_airport", + "type": "nested_collection" + }, + "predicate": { + "in_collection": { + "column_name": "location", + "type": "nested_collection" + }, + "predicate": { + "column": { + "type": "column", + "name": "state" + }, + "operator": "prefix", + "type": "binary_comparison_operator", + "value": { + "type": "scalar", + "value": "T" + } + }, + "type": "exists" + }, + "type": "exists" + }, + "type": "exists" + } \ No newline at end of file diff --git a/connector/testdata/filter_tests/get_predicate/nested_001/want.json b/connector/testdata/filter_tests/get_predicate/nested_001/want.json new file mode 100644 index 0000000..4c09697 --- /dev/null +++ b/connector/testdata/filter_tests/get_predicate/nested_001/want.json @@ -0,0 +1,12 @@ +{ + "column": { + "type": "column", + "name": "state" + }, + "operator": "prefix", + "type": "binary_comparison_operator", + "value": { + "type": "scalar", + "value": "T" + } + } \ No newline at end of file diff --git a/connector/testdata/filter_tests/get_predicate/nested_and_002/got.json b/connector/testdata/filter_tests/get_predicate/nested_and_002/got.json new file mode 100644 index 0000000..fb32723 --- /dev/null +++ b/connector/testdata/filter_tests/get_predicate/nested_and_002/got.json @@ -0,0 +1,102 @@ +{ + "expressions": [ + { + "in_collection": { + "column_name": "route", + "type": "nested_collection" + }, + "predicate": { + "in_collection": { + "column_name": "arrival_airport", + "type": "nested_collection" + }, + "predicate": { + "in_collection": { + "column_name": "location", + "type": "nested_collection" + }, + "predicate": { + "in_collection": { + "column_name": "coordinates", + "type": "nested_collection" + }, + "predicate": { + "column": { + "type": "column", + "name": "elevation" + }, + "operator": "range", + "type": "binary_comparison_operator", + "value": { + "type": "scalar", + "value": { + "boost": "", + "gt": "", + "gte": "200", + "lt": "", + "lte": "" + } + } + }, + "type": "exists" + }, + "type": "exists" + }, + "type": "exists" + }, + "type": "exists" + }, + { + "in_collection": { + "column_name": "route", + "type": "nested_collection" + }, + "predicate": { + "in_collection": { + "column_name": "arrival_airport", + "type": "nested_collection" + }, + "predicate": { + "column": { + "type": "column", + "name": "terminals" + }, + "operator": "match", + "type": "binary_comparison_operator", + "value": { + "type": "scalar", + "value": "2" + } + }, + "type": "exists" + }, + "type": "exists" + }, + { + "in_collection": { + "column_name": "route", + "type": "nested_collection" + }, + "predicate": { + "column": { + "type": "column", + "name": "travel_time" + }, + "operator": "range", + "type": "binary_comparison_operator", + "value": { + "type": "scalar", + "value": { + "boost": "", + "gt": "", + "gte": "", + "lt": "300", + "lte": "" + } + } + }, + "type": "exists" + } + ], + "type": "and" + } \ No newline at end of file diff --git a/connector/testdata/filter_tests/get_predicate/nested_and_002/want.json b/connector/testdata/filter_tests/get_predicate/nested_and_002/want.json new file mode 100644 index 0000000..fb32723 --- /dev/null +++ b/connector/testdata/filter_tests/get_predicate/nested_and_002/want.json @@ -0,0 +1,102 @@ +{ + "expressions": [ + { + "in_collection": { + "column_name": "route", + "type": "nested_collection" + }, + "predicate": { + "in_collection": { + "column_name": "arrival_airport", + "type": "nested_collection" + }, + "predicate": { + "in_collection": { + "column_name": "location", + "type": "nested_collection" + }, + "predicate": { + "in_collection": { + "column_name": "coordinates", + "type": "nested_collection" + }, + "predicate": { + "column": { + "type": "column", + "name": "elevation" + }, + "operator": "range", + "type": "binary_comparison_operator", + "value": { + "type": "scalar", + "value": { + "boost": "", + "gt": "", + "gte": "200", + "lt": "", + "lte": "" + } + } + }, + "type": "exists" + }, + "type": "exists" + }, + "type": "exists" + }, + "type": "exists" + }, + { + "in_collection": { + "column_name": "route", + "type": "nested_collection" + }, + "predicate": { + "in_collection": { + "column_name": "arrival_airport", + "type": "nested_collection" + }, + "predicate": { + "column": { + "type": "column", + "name": "terminals" + }, + "operator": "match", + "type": "binary_comparison_operator", + "value": { + "type": "scalar", + "value": "2" + } + }, + "type": "exists" + }, + "type": "exists" + }, + { + "in_collection": { + "column_name": "route", + "type": "nested_collection" + }, + "predicate": { + "column": { + "type": "column", + "name": "travel_time" + }, + "operator": "range", + "type": "binary_comparison_operator", + "value": { + "type": "scalar", + "value": { + "boost": "", + "gt": "", + "gte": "", + "lt": "300", + "lte": "" + } + } + }, + "type": "exists" + } + ], + "type": "and" + } \ No newline at end of file diff --git a/testdata/unit_tests/query_tests/flights_nested_flattened/configuration.json b/testdata/unit_tests/query_tests/flights_nested_flattened/configuration.json new file mode 100644 index 0000000..e87ccf2 --- /dev/null +++ b/testdata/unit_tests/query_tests/flights_nested_flattened/configuration.json @@ -0,0 +1,105 @@ +{ + "indices": { + "flights": { + "mappings": { + "properties": { + "aircraft": { + "type": "keyword" + }, + "code": { + "type": "keyword" + }, + "passengers": { + "type": "integer" + }, + "route": { + "properties": { + "arrival_airport": { + "properties": { + "code": { + "type": "keyword" + }, + "location": { + "properties": { + "coordinates": { + "properties": { + "elevation": { + "type": "float" + }, + "latitude": { + "type": "float" + }, + "longitude": { + "type": "float" + } + } + }, + "country": { + "type": "keyword" + }, + "state": { + "type": "keyword" + } + } + }, + "name": { + "type": "text" + }, + "runways": { + "type": "integer" + }, + "terminals": { + "type": "integer" + } + } + }, + "departure_airport": { + "properties": { + "code": { + "type": "keyword" + }, + "location": { + "properties": { + "coordinates": { + "properties": { + "elevation": { + "type": "float" + }, + "latitude": { + "type": "float" + }, + "longitude": { + "type": "float" + } + } + }, + "country": { + "type": "keyword" + }, + "state": { + "type": "keyword" + } + } + }, + "name": { + "type": "text" + }, + "runways": { + "type": "integer" + }, + "terminals": { + "type": "integer" + } + } + }, + "travel_time": { + "type": "integer" + } + } + } + } + } + } + }, + "queries": {} +} \ No newline at end of file diff --git a/testdata/unit_tests/query_tests/flights_nested_flattened/nested_filtering/ndc_request.json b/testdata/unit_tests/query_tests/flights_nested_flattened/nested_filtering/ndc_request.json new file mode 100644 index 0000000..b1e49ba --- /dev/null +++ b/testdata/unit_tests/query_tests/flights_nested_flattened/nested_filtering/ndc_request.json @@ -0,0 +1,218 @@ +{ + "arguments": {}, + "collection": "flights", + "collection_relationships": {}, + "query": { + "fields": { + "aircraft": { + "column": "aircraft", + "type": "column" + }, + "code": { + "column": "code", + "type": "column" + }, + "id": { + "column": "_id", + "type": "column" + }, + "passengers": { + "column": "passengers", + "type": "column" + }, + "route": { + "column": "route", + "fields": { + "fields": { + "fields": { + "arrivalAirport": { + "column": "arrival_airport", + "fields": { + "fields": { + "fields": { + "code": { + "column": "code", + "type": "column" + }, + "location": { + "column": "location", + "fields": { + "fields": { + "fields": { + "coordinates": { + "column": "coordinates", + "fields": { + "fields": { + "fields": { + "elevation": { + "column": "elevation", + "type": "column" + }, + "latitude": { + "column": "latitude", + "type": "column" + }, + "longitude": { + "column": "longitude", + "type": "column" + } + }, + "type": "object" + }, + "type": "array" + }, + "type": "column" + }, + "country": { + "column": "country", + "type": "column" + }, + "state": { + "column": "state", + "type": "column" + } + }, + "type": "object" + }, + "type": "array" + }, + "type": "column" + }, + "name": { + "column": "name", + "type": "column" + }, + "runways": { + "column": "runways", + "type": "column" + }, + "terminals": { + "column": "terminals", + "type": "column" + } + }, + "type": "object" + }, + "type": "array" + }, + "type": "column" + }, + "departureAirport": { + "column": "departure_airport", + "fields": { + "fields": { + "fields": { + "code": { + "column": "code", + "type": "column" + }, + "location": { + "column": "location", + "fields": { + "fields": { + "fields": { + "coordinates": { + "column": "coordinates", + "fields": { + "fields": { + "fields": { + "elevation": { + "column": "elevation", + "type": "column" + }, + "latitude": { + "column": "latitude", + "type": "column" + }, + "longitude": { + "column": "longitude", + "type": "column" + } + }, + "type": "object" + }, + "type": "array" + }, + "type": "column" + }, + "country": { + "column": "country", + "type": "column" + }, + "state": { + "column": "state", + "type": "column" + } + }, + "type": "object" + }, + "type": "array" + }, + "type": "column" + }, + "name": { + "column": "name", + "type": "column" + }, + "runways": { + "column": "runways", + "type": "column" + }, + "terminals": { + "column": "terminals", + "type": "column" + } + }, + "type": "object" + }, + "type": "array" + }, + "type": "column" + }, + "travelTime": { + "column": "travel_time", + "type": "column" + } + }, + "type": "object" + }, + "type": "array" + }, + "type": "column" + } + }, + "predicate": { + "in_collection": { + "column_name": "route", + "type": "nested_collection" + }, + "predicate": { + "in_collection": { + "column_name": "departure_airport", + "type": "nested_collection" + }, + "predicate": { + "in_collection": { + "column_name": "location", + "type": "nested_collection" + }, + "predicate": { + "column": { + "type": "column", + "name": "state" + }, + "operator": "prefix", + "type": "binary_comparison_operator", + "value": { + "type": "scalar", + "value": "T" + } + }, + "type": "exists" + }, + "type": "exists" + }, + "type": "exists" + } + } +} \ No newline at end of file diff --git a/testdata/unit_tests/query_tests/flights_nested_flattened/nested_filtering/query.graphql b/testdata/unit_tests/query_tests/flights_nested_flattened/nested_filtering/query.graphql new file mode 100644 index 0000000..6bbe037 --- /dev/null +++ b/testdata/unit_tests/query_tests/flights_nested_flattened/nested_filtering/query.graphql @@ -0,0 +1,46 @@ +query MyQuery { + flights( + args: {} + where: { + route: { departureAirport: { location: { state: { prefix: "T" } } } } + } + ) { + route { + arrivalAirport { + location { + coordinates { + elevation + longitude + latitude + } + country + state + } + code + name + runways + terminals + } + departureAirport { + location { + coordinates { + latitude + longitude + elevation + } + country + state + } + code + terminals + runways + name + } + travelTime + } + code + aircraft + id + passengers + } +} diff --git a/testdata/unit_tests/query_tests/flights_nested_flattened/nested_filtering/want.json b/testdata/unit_tests/query_tests/flights_nested_flattened/nested_filtering/want.json new file mode 100644 index 0000000..f873446 --- /dev/null +++ b/testdata/unit_tests/query_tests/flights_nested_flattened/nested_filtering/want.json @@ -0,0 +1,33 @@ +{ + "_source": [ + "_id", + "aircraft", + "code", + "passengers", + "route.arrival_airport.code", + "route.arrival_airport.location.coordinates.elevation", + "route.arrival_airport.location.coordinates.latitude", + "route.arrival_airport.location.coordinates.longitude", + "route.arrival_airport.location.country", + "route.arrival_airport.location.state", + "route.arrival_airport.name", + "route.arrival_airport.runways", + "route.arrival_airport.terminals", + "route.departure_airport.code", + "route.departure_airport.location.coordinates.elevation", + "route.departure_airport.location.coordinates.latitude", + "route.departure_airport.location.coordinates.longitude", + "route.departure_airport.location.country", + "route.departure_airport.location.state", + "route.departure_airport.name", + "route.departure_airport.runways", + "route.departure_airport.terminals", + "route.travel_time" + ], + "query": { + "prefix": { + "route.departure_airport.location.state": "T" + } + }, + "size": 10000 +} \ No newline at end of file diff --git a/testdata/unit_tests/query_tests/flights_nested_flattened/nested_filtering_and/ndc_request.json b/testdata/unit_tests/query_tests/flights_nested_flattened/nested_filtering_and/ndc_request.json new file mode 100644 index 0000000..839c5d8 --- /dev/null +++ b/testdata/unit_tests/query_tests/flights_nested_flattened/nested_filtering_and/ndc_request.json @@ -0,0 +1,187 @@ +{ + "arguments": {}, + "collection": "flights", + "collection_relationships": {}, + "query": { + "fields": { + "aircraft": { + "column": "aircraft", + "type": "column" + }, + "code": { + "column": "code", + "type": "column" + }, + "route": { + "column": "route", + "fields": { + "fields": { + "fields": { + "arrivalAirport": { + "column": "arrival_airport", + "fields": { + "fields": { + "fields": { + "location": { + "column": "location", + "fields": { + "fields": { + "fields": { + "coordinates": { + "column": "coordinates", + "fields": { + "fields": { + "fields": { + "elevation": { + "column": "elevation", + "type": "column" + } + }, + "type": "object" + }, + "type": "array" + }, + "type": "column" + } + }, + "type": "object" + }, + "type": "array" + }, + "type": "column" + }, + "name": { + "column": "name", + "type": "column" + }, + "runways": { + "column": "runways", + "type": "column" + }, + "terminals": { + "column": "terminals", + "type": "column" + } + }, + "type": "object" + }, + "type": "array" + }, + "type": "column" + }, + "travelTime": { + "column": "travel_time", + "type": "column" + } + }, + "type": "object" + }, + "type": "array" + }, + "type": "column" + } + }, + "predicate": { + "expressions": [ + { + "in_collection": { + "column_name": "route", + "type": "nested_collection" + }, + "predicate": { + "in_collection": { + "column_name": "arrival_airport", + "type": "nested_collection" + }, + "predicate": { + "in_collection": { + "column_name": "location", + "type": "nested_collection" + }, + "predicate": { + "in_collection": { + "column_name": "coordinates", + "type": "nested_collection" + }, + "predicate": { + "column": { + "type": "column", + "name": "elevation" + }, + "operator": "range", + "type": "binary_comparison_operator", + "value": { + "type": "scalar", + "value": { + "boost": "", + "gt": "", + "gte": "200", + "lt": "", + "lte": "" + } + } + }, + "type": "exists" + }, + "type": "exists" + }, + "type": "exists" + }, + "type": "exists" + }, + { + "in_collection": { + "column_name": "route", + "type": "nested_collection" + }, + "predicate": { + "in_collection": { + "column_name": "arrival_airport", + "type": "nested_collection" + }, + "predicate": { + "column": { + "type": "column", + "name": "terminals" + }, + "operator": "match", + "type": "binary_comparison_operator", + "value": { + "type": "scalar", + "value": "2" + } + }, + "type": "exists" + }, + "type": "exists" + }, + { + "in_collection": { + "column_name": "route", + "type": "nested_collection" + }, + "predicate": { + "column": { + "type": "column", + "name": "travel_time" + }, + "operator": "range", + "type": "binary_comparison_operator", + "value": { + "type": "scalar", + "value": { + "boost": "", + "gt": "", + "gte": "", + "lt": "300", + "lte": "" + } + } + }, + "type": "exists" + } + ], + "type": "and" + } + } +} \ No newline at end of file diff --git a/testdata/unit_tests/query_tests/flights_nested_flattened/nested_filtering_and/query.graphql b/testdata/unit_tests/query_tests/flights_nested_flattened/nested_filtering_and/query.graphql new file mode 100644 index 0000000..99f3772 --- /dev/null +++ b/testdata/unit_tests/query_tests/flights_nested_flattened/nested_filtering_and/query.graphql @@ -0,0 +1,48 @@ +query MyQuery { + flights( + args: {} + where: { + _and: [ + { + route: { + arrivalAirport: { + location: { + coordinates: { + elevation: { + range: { gte: "200", lt: "", lte: "", boost: "", gt: "" } + } + } + } + } + } + } + { + route: { arrivalAirport: { location: {}, terminals: { match: "2" } } } + } + { + route: { + travelTime: { + range: { boost: "", gt: "", gte: "", lt: "300", lte: "" } + } + } + } + ] + } + ) { + route { + arrivalAirport { + location { + coordinates { + elevation + } + } + name + runways + terminals + } + travelTime + } + code + aircraft + } +} diff --git a/testdata/unit_tests/query_tests/flights_nested_flattened/nested_filtering_and/want.json b/testdata/unit_tests/query_tests/flights_nested_flattened/nested_filtering_and/want.json new file mode 100644 index 0000000..0283bc7 --- /dev/null +++ b/testdata/unit_tests/query_tests/flights_nested_flattened/nested_filtering_and/want.json @@ -0,0 +1,37 @@ +{ + "_source": [ + "aircraft", + "code", + "route.arrival_airport.location.coordinates.elevation", + "route.arrival_airport.name", + "route.arrival_airport.runways", + "route.arrival_airport.terminals", + "route.travel_time" + ], + "query": { + "bool": { + "must": [ + { + "range": { + "route.arrival_airport.location.coordinates.elevation": { + "gte": "200" + } + } + }, + { + "match": { + "route.arrival_airport.terminals": "2" + } + }, + { + "range": { + "route.travel_time": { + "lt": "300" + } + } + } + ] + } + }, + "size": 10000 +} \ No newline at end of file diff --git a/testdata/unit_tests/query_tests/flights_nested_flattened/nested_filtering_or/ndc_request.json b/testdata/unit_tests/query_tests/flights_nested_flattened/nested_filtering_or/ndc_request.json new file mode 100644 index 0000000..3773c9e --- /dev/null +++ b/testdata/unit_tests/query_tests/flights_nested_flattened/nested_filtering_or/ndc_request.json @@ -0,0 +1,175 @@ +{ + "arguments": {}, + "collection": "flights", + "collection_relationships": {}, + "query": { + "fields": { + "aircraft": { + "column": "aircraft", + "type": "column" + }, + "code": { + "column": "code", + "type": "column" + }, + "route": { + "column": "route", + "fields": { + "fields": { + "fields": { + "arrivalAirport": { + "column": "arrival_airport", + "fields": { + "fields": { + "fields": { + "location": { + "column": "location", + "fields": { + "fields": { + "fields": { + "coordinates": { + "column": "coordinates", + "fields": { + "fields": { + "fields": { + "elevation": { + "column": "elevation", + "type": "column" + } + }, + "type": "object" + }, + "type": "array" + }, + "type": "column" + }, + "country": { + "column": "country", + "type": "column" + }, + "state": { + "column": "state", + "type": "column" + } + }, + "type": "object" + }, + "type": "array" + }, + "type": "column" + }, + "name": { + "column": "name", + "type": "column" + }, + "runways": { + "column": "runways", + "type": "column" + }, + "terminals": { + "column": "terminals", + "type": "column" + } + }, + "type": "object" + }, + "type": "array" + }, + "type": "column" + }, + "travelTime": { + "column": "travel_time", + "type": "column" + } + }, + "type": "object" + }, + "type": "array" + }, + "type": "column" + } + }, + "predicate": { + "expressions": [ + { + "in_collection": { + "column_name": "route", + "type": "nested_collection" + }, + "predicate": { + "in_collection": { + "column_name": "arrival_airport", + "type": "nested_collection" + }, + "predicate": { + "column": { + "type": "column", + "name": "terminals" + }, + "operator": "match", + "type": "binary_comparison_operator", + "value": { + "type": "scalar", + "value": "4" + } + }, + "type": "exists" + }, + "type": "exists" + }, + { + "in_collection": { + "column_name": "route", + "type": "nested_collection" + }, + "predicate": { + "in_collection": { + "column_name": "arrival_airport", + "type": "nested_collection" + }, + "predicate": { + "column": { + "type": "column", + "name": "terminals" + }, + "operator": "match", + "type": "binary_comparison_operator", + "value": { + "type": "scalar", + "value": "2" + } + }, + "type": "exists" + }, + "type": "exists" + }, + { + "in_collection": { + "column_name": "route", + "type": "nested_collection" + }, + "predicate": { + "column": { + "type": "column", + "name": "travel_time" + }, + "operator": "range", + "type": "binary_comparison_operator", + "value": { + "type": "scalar", + "value": { + "boost": "", + "gt": "", + "gte": "", + "lt": "0", + "lte": "" + } + } + }, + "type": "exists" + } + ], + "type": "or" + } + } +} \ No newline at end of file diff --git a/testdata/unit_tests/query_tests/flights_nested_flattened/nested_filtering_or/query.graphql b/testdata/unit_tests/query_tests/flights_nested_flattened/nested_filtering_or/query.graphql new file mode 100644 index 0000000..1d4ecad --- /dev/null +++ b/testdata/unit_tests/query_tests/flights_nested_flattened/nested_filtering_or/query.graphql @@ -0,0 +1,40 @@ +query MyQuery { + flights( + args: {} + where: { + _or: [ + { + route: { arrivalAirport: { location: {}, terminals: { match: "4" } } } + } + { + route: { arrivalAirport: { location: {}, terminals: { match: "2" } } } + } + { + route: { + travelTime: { + range: { boost: "", gt: "", gte: "", lt: "0", lte: "" } + } + } + } + ] + } + ) { + route { + arrivalAirport { + location { + coordinates { + elevation + } + country + state + } + name + runways + terminals + } + travelTime + } + code + aircraft + } +} diff --git a/testdata/unit_tests/query_tests/flights_nested_flattened/nested_filtering_or/want.json b/testdata/unit_tests/query_tests/flights_nested_flattened/nested_filtering_or/want.json new file mode 100644 index 0000000..fda5fc5 --- /dev/null +++ b/testdata/unit_tests/query_tests/flights_nested_flattened/nested_filtering_or/want.json @@ -0,0 +1,37 @@ +{ + "_source": [ + "aircraft", + "code", + "route.arrival_airport.location.coordinates.elevation", + "route.arrival_airport.location.country", + "route.arrival_airport.location.state", + "route.arrival_airport.name", + "route.arrival_airport.runways", + "route.arrival_airport.terminals", + "route.travel_time" + ], + "query": { + "bool": { + "should": [ + { + "match": { + "route.arrival_airport.terminals": "4" + } + }, + { + "match": { + "route.arrival_airport.terminals": "2" + } + }, + { + "range": { + "route.travel_time": { + "lt": "0" + } + } + } + ] + } + }, + "size": 10000 +} \ No newline at end of file diff --git a/testdata/unit_tests/query_tests/flights_nested_flattened/nested_filtering_range/ndc_request.json b/testdata/unit_tests/query_tests/flights_nested_flattened/nested_filtering_range/ndc_request.json new file mode 100644 index 0000000..1ae36e8 --- /dev/null +++ b/testdata/unit_tests/query_tests/flights_nested_flattened/nested_filtering_range/ndc_request.json @@ -0,0 +1,83 @@ +{ + "arguments": {}, + "collection": "flights", + "collection_relationships": {}, + "query": { + "fields": { + "route": { + "column": "route", + "fields": { + "fields": { + "fields": { + "arrivalAirport": { + "column": "arrival_airport", + "fields": { + "fields": { + "fields": { + "name": { + "column": "name", + "type": "column" + } + }, + "type": "object" + }, + "type": "array" + }, + "type": "column" + }, + "departureAirport": { + "column": "departure_airport", + "fields": { + "fields": { + "fields": { + "name": { + "column": "name", + "type": "column" + } + }, + "type": "object" + }, + "type": "array" + }, + "type": "column" + }, + "travelTime": { + "column": "travel_time", + "type": "column" + } + }, + "type": "object" + }, + "type": "array" + }, + "type": "column" + } + }, + "limit": 100, + "predicate": { + "in_collection": { + "column_name": "route", + "type": "nested_collection" + }, + "predicate": { + "column": { + "type": "column", + "name": "travel_time" + }, + "operator": "range", + "type": "binary_comparison_operator", + "value": { + "type": "scalar", + "value": { + "boost": "", + "gt": "", + "gte": "100", + "lt": "", + "lte": "200" + } + } + }, + "type": "exists" + } + } +} \ No newline at end of file diff --git a/testdata/unit_tests/query_tests/flights_nested_flattened/nested_filtering_range/query.graphql b/testdata/unit_tests/query_tests/flights_nested_flattened/nested_filtering_range/query.graphql new file mode 100644 index 0000000..e911872 --- /dev/null +++ b/testdata/unit_tests/query_tests/flights_nested_flattened/nested_filtering_range/query.graphql @@ -0,0 +1,23 @@ +query MyQuery { + flights( + args: {} + limit: 100 + where: { + route: { + travelTime: { + range: { boost: "", gt: "", gte: "100", lt: "", lte: "200" } + } + } + } + ) { + route { + travelTime + arrivalAirport { + name + } + departureAirport { + name + } + } + } +} diff --git a/testdata/unit_tests/query_tests/flights_nested_flattened/nested_filtering_range/want.json b/testdata/unit_tests/query_tests/flights_nested_flattened/nested_filtering_range/want.json new file mode 100644 index 0000000..10a7d0f --- /dev/null +++ b/testdata/unit_tests/query_tests/flights_nested_flattened/nested_filtering_range/want.json @@ -0,0 +1,16 @@ +{ + "_source": [ + "route.arrival_airport.name", + "route.departure_airport.name", + "route.travel_time" + ], + "query": { + "range": { + "route.travel_time": { + "gte": "100", + "lte": "200" + } + } + }, + "size": 100 +} \ No newline at end of file