diff --git a/server/lib/api-tests/api-tests.cabal b/server/lib/api-tests/api-tests.cabal index 80dd8d3db7f26..2c507d09c4ed6 100644 --- a/server/lib/api-tests/api-tests.cabal +++ b/server/lib/api-tests/api-tests.cabal @@ -137,6 +137,7 @@ library Test.DataConnector.QuerySpec Test.DataConnector.SelectPermissionsSpec Test.Databases.BigQuery.Queries.SpatialTypesSpec + Test.Databases.BigQuery.Queries.TextFunctionsSpec Test.Databases.BigQuery.Queries.TypeInterpretationSpec Test.Databases.BigQuery.Schema.ComputedFields.TableSpec Test.Databases.Postgres.ArraySpec diff --git a/server/lib/api-tests/src/Test/Databases/BigQuery/Queries/TextFunctionsSpec.hs b/server/lib/api-tests/src/Test/Databases/BigQuery/Queries/TextFunctionsSpec.hs new file mode 100644 index 0000000000000..2d9fe57c88a81 --- /dev/null +++ b/server/lib/api-tests/src/Test/Databases/BigQuery/Queries/TextFunctionsSpec.hs @@ -0,0 +1,217 @@ +{-# LANGUAGE QuasiQuotes #-} + +-- | Test text search functions in BigQuery +module Test.Databases.BigQuery.Queries.TextFunctionsSpec (spec) where + +import Data.Aeson (Value) +import Data.List.NonEmpty qualified as NE +import Harness.Backend.BigQuery qualified as BigQuery +import Harness.GraphqlEngine (postGraphql) +import Harness.Quoter.Graphql (graphql) +import Harness.Quoter.Yaml (interpolateYaml) +import Harness.Schema qualified as Schema +import Harness.Test.Fixture qualified as Fixture +import Harness.TestEnvironment (GlobalTestEnvironment, TestEnvironment (..)) +import Harness.Yaml (shouldReturnYaml) +import Hasura.Prelude +import Test.Hspec (SpecWith, describe, it) + +spec :: SpecWith GlobalTestEnvironment +spec = + Fixture.run + ( NE.fromList + [ (Fixture.fixture $ Fixture.Backend BigQuery.backendTypeMetadata) + { Fixture.setupTeardown = \(testEnvironment, _) -> + [ BigQuery.setupTablesAction schema testEnvironment + ] + } + ] + ) + tests + +-------------------------------------------------------------------------------- +-- Schema + +schema :: [Schema.Table] +schema = + [ (Schema.table "languages") + { Schema.tableColumns = + [ Schema.column "name" Schema.TStr + ], + Schema.tablePrimaryKey = [], + Schema.tableData = + [ [Schema.VStr "Python"], + [Schema.VStr "C"], + [Schema.VStr "C++"], + [Schema.VStr "Java"], + [Schema.VStr "C#"], + [Schema.VStr "JavaScript"], + [Schema.VStr "PHP"], + [Schema.VStr "Visual Basic"], + [Schema.VStr "SQL"], + [Schema.VStr "Scratch"], + [Schema.VStr "Go"], + [Schema.VStr "Fortran"], + [Schema.VStr "Delphi"], + [Schema.VStr "MATLAB"], + [Schema.VStr "Assembly"], + [Schema.VStr "Swift"], + [Schema.VStr "Kotlin"], + [Schema.VStr "Ruby"], + [Schema.VStr "Rust"], + [Schema.VStr "COBOL"] + ] + } + ] + +-------------------------------------------------------------------------------- +-- Tests + +tests :: SpecWith TestEnvironment +tests = do + describe "Text predicates" do + it "ilike" \testEnvironment -> do + let schemaName :: Schema.SchemaName + schemaName = Schema.getSchemaName testEnvironment + + let expected :: Value + expected = + [interpolateYaml| + data: + #{schemaName}_languages: + - name: Assembly + - name: Fortran + - name: Java + - name: JavaScript + - name: MATLAB + - name: Scratch + - name: Visual Basic + |] + + actual :: IO Value + actual = + postGraphql + testEnvironment + [graphql| + query { + #{schemaName}_languages ( + order_by: { name: asc }, + where: { name: { _ilike: "%a%" } } + ) { + name + } + } + |] + + shouldReturnYaml testEnvironment actual expected + + it "like" \testEnvironment -> do + let schemaName = Schema.getSchemaName testEnvironment + let expected :: Value + expected = + [interpolateYaml| + data: + #{schemaName}_languages: + - name: Fortran + - name: Java + - name: JavaScript + - name: Scratch + - name: Visual Basic + |] + + actual :: IO Value + actual = + postGraphql + testEnvironment + [graphql| + query { + #{schemaName}_languages ( + order_by: { name: asc }, + where: { name: { _like: "%a%" } } + ) { + name + } + } + |] + + shouldReturnYaml testEnvironment actual expected + + it "nlike" \testEnvironment -> do + let schemaName = Schema.getSchemaName testEnvironment + let expected :: Value + expected = + [interpolateYaml| + data: + #{schemaName}_languages: + - name: Assembly + - name: C + - name: C# + - name: C++ + - name: COBOL + - name: Delphi + - name: Go + - name: Kotlin + - name: MATLAB + - name: PHP + - name: Python + - name: Ruby + - name: Rust + - name: SQL + - name: Swift + |] + + actual :: IO Value + actual = + postGraphql + testEnvironment + [graphql| + query { + #{schemaName}_languages ( + order_by: { name: asc }, + where: { name: { _nlike: "%a%" } } + ) { + name + } + } + |] + + shouldReturnYaml testEnvironment actual expected + + it "nilike" \testEnvironment -> do + let schemaName = Schema.getSchemaName testEnvironment + let expected :: Value + expected = + [interpolateYaml| + data: + #{schemaName}_languages: + - name: C + - name: C# + - name: C++ + - name: COBOL + - name: Delphi + - name: Go + - name: Kotlin + - name: PHP + - name: Python + - name: Ruby + - name: Rust + - name: SQL + - name: Swift + |] + + actual :: IO Value + actual = + postGraphql + testEnvironment + [graphql| + query { + #{schemaName}_languages ( + order_by: { name: asc }, + where: { name: { _nilike: "%a%" } } + ) { + name + } + } + |] + + shouldReturnYaml testEnvironment actual expected diff --git a/server/src-lib/Hasura/Backends/BigQuery/DDL/BoolExp.hs b/server/src-lib/Hasura/Backends/BigQuery/DDL/BoolExp.hs index 34384d2b9f7c3..4f63eeb8db221 100644 --- a/server/src-lib/Hasura/Backends/BigQuery/DDL/BoolExp.hs +++ b/server/src-lib/Hasura/Backends/BigQuery/DDL/BoolExp.hs @@ -7,7 +7,7 @@ import Data.Aeson qualified as J import Data.Aeson.Key qualified as K import Data.Aeson.KeyMap qualified as KM import Data.Text.Extended -import Hasura.Backends.BigQuery.Types (ScalarType (StringScalarType)) +import Hasura.Backends.BigQuery.Types (BooleanOperators (..), ScalarType (StringScalarType)) import Hasura.Base.Error import Hasura.Prelude import Hasura.RQL.IR.BoolExp @@ -50,10 +50,14 @@ parseBoolExpOperations rhsParser _rootTableFieldInfoMap _fields columnRef value "$gte" -> parseGte "_lte" -> parseLte "$lte" -> parseLte - "_like" -> parseLike "$like" -> parseLike - "_nlike" -> parseNlike - "$nlike" -> parseNlike + "_like" -> parseLike + "$nlike" -> parseNLike + "_nlike" -> parseNLike + "$ilike" -> parseILike + "_ilike" -> parseILike + "$nilike" -> parseNILike + "_nilike" -> parseNILike "_in" -> parseIn "$in" -> parseIn "_nin" -> parseNin @@ -75,7 +79,9 @@ parseBoolExpOperations rhsParser _rootTableFieldInfoMap _fields columnRef value parseGte = AGTE <$> parseOne parseLte = ALTE <$> parseOne parseLike = guardType StringScalarType >> ALIKE <$> parseOne - parseNlike = guardType StringScalarType >> ANLIKE <$> parseOne + parseILike = guardType StringScalarType >> ABackendSpecific . ASTILike <$> parseOne + parseNLike = guardType StringScalarType >> ANLIKE <$> parseOne + parseNILike = guardType StringScalarType >> ABackendSpecific . ASTNILike <$> parseOne parseIn = AIN <$> parseManyWithType colTy parseNin = ANIN <$> parseManyWithType colTy parseIsNull = bool ANISNOTNULL ANISNULL <$> decodeValue val diff --git a/server/src-lib/Hasura/Backends/BigQuery/FromIr.hs b/server/src-lib/Hasura/Backends/BigQuery/FromIr.hs index d38b8b2276961..f29e9cb0ced1a 100644 --- a/server/src-lib/Hasura/Backends/BigQuery/FromIr.hs +++ b/server/src-lib/Hasura/Backends/BigQuery/FromIr.hs @@ -1810,6 +1810,10 @@ fromBackendSpecificOpExpG expression op = BigQuery.ASTIntersects v -> func "ST_INTERSECTS" v BigQuery.ASTDWithin (Ir.DWithinGeogOp r v sph) -> FunctionExpression (FunctionName "ST_DWITHIN" Nothing) [expression, v, r, sph] + BigQuery.ASTILike v -> + OpExpression ILikeOp (FunctionExpression (FunctionName "LOWER" Nothing) [expression]) v + BigQuery.ASTNILike v -> + OpExpression NotILikeOp (FunctionExpression (FunctionName "LOWER" Nothing) [expression]) v nullableBoolEquality :: Expression -> Expression -> Expression nullableBoolEquality x y = diff --git a/server/src-lib/Hasura/Backends/BigQuery/Instances/Schema.hs b/server/src-lib/Hasura/Backends/BigQuery/Instances/Schema.hs index d979ba1a274bc..3062263d9b2cf 100644 --- a/server/src-lib/Hasura/Backends/BigQuery/Instances/Schema.hs +++ b/server/src-lib/Hasura/Backends/BigQuery/Instances/Schema.hs @@ -303,7 +303,19 @@ bqComparisonExps = P.memoize 'comparisonExps $ \columnType -> do collapseIfNull (C.fromAutogeneratedName Name.__nlike) (Just "does the column NOT match the given pattern") - (ANLIKE . IR.mkParameter <$> typedParser) + (ANLIKE . IR.mkParameter <$> typedParser), + mkBoolOperator + tCase + collapseIfNull + (C.fromAutogeneratedName Name.__ilike) + (Just "does the column match the given case-insensitive pattern") + (ABackendSpecific . BigQuery.ASTILike . IR.mkParameter <$> typedParser), + mkBoolOperator + tCase + collapseIfNull + (C.fromAutogeneratedName Name.__nilike) + (Just "does the column NOT match the given case-insensitive pattern") + (ABackendSpecific . BigQuery.ASTNILike . IR.mkParameter <$> typedParser) ], -- Ops for Bytes type guard (isScalarColumnWhere (== BigQuery.BytesScalarType) columnType) diff --git a/server/src-lib/Hasura/Backends/BigQuery/ToQuery.hs b/server/src-lib/Hasura/Backends/BigQuery/ToQuery.hs index 436e17f0cef35..bdc28ae80c95a 100644 --- a/server/src-lib/Hasura/Backends/BigQuery/ToQuery.hs +++ b/server/src-lib/Hasura/Backends/BigQuery/ToQuery.hs @@ -153,6 +153,11 @@ fromOp = NotInOp -> "NOT IN" LikeOp -> "LIKE" NotLikeOp -> "NOT LIKE" + -- BigQuery doesn't have case-insensitive versions of this operator, but + -- that's ok: by this point, we'll have built a version of the query that + -- works case insensitively. + ILikeOp -> "LIKE" + NotILikeOp -> "NOT LIKE" fromPath :: JsonPath -> Printer fromPath path = diff --git a/server/src-lib/Hasura/Backends/BigQuery/Types.hs b/server/src-lib/Hasura/Backends/BigQuery/Types.hs index e7075a3c6a2f2..211ca5e756b0e 100644 --- a/server/src-lib/Hasura/Backends/BigQuery/Types.hs +++ b/server/src-lib/Hasura/Backends/BigQuery/Types.hs @@ -501,9 +501,9 @@ data Op | NotInOp | LikeOp | NotLikeOp - -- | SNE - -- | SILIKE - -- | SNILIKE + | -- | SNE + ILikeOp + | NotILikeOp -- | SSIMILAR -- | SNSIMILAR -- | SGTE @@ -790,6 +790,8 @@ data BooleanOperators a | ASTWithin a | ASTIntersects a | ASTDWithin (DWithinGeogOp a) + | ASTILike a + | ASTNILike a deriving stock (Eq, Generic, Foldable, Functor, Traversable, Show) instance (NFData a) => NFData (BooleanOperators a) @@ -804,6 +806,8 @@ instance (ToJSON a) => J.ToJSONKeyValue (BooleanOperators a) where ASTTouches a -> ("_st_touches", J.toJSON a) ASTWithin a -> ("_st_within", J.toJSON a) ASTDWithin a -> ("_st_dwithin", J.toJSON a) + ASTILike a -> ("_st_ilike", J.toJSON a) + ASTNILike a -> ("_st_nilike", J.toJSON a) data FunctionName = FunctionName { functionName :: Text,