mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-09-20 15:09:02 +03:00
server: support ltree operators (close #625)
GitOrigin-RevId: fb6a3eb8cbe4604789938bcbc78916fbcd1af515
This commit is contained in:
parent
f55bbe96ef
commit
fb1a0d286d
44
CHANGELOG.md
44
CHANGELOG.md
@ -101,6 +101,49 @@ do this anymore, so function permissions are introduced which will explicitly gr
|
||||
a function for a given role. A pre-requisite to adding a function permission is that the role should
|
||||
have select permissions to the target table of the function.
|
||||
|
||||
### `ltree` comparison operators
|
||||
|
||||
Comparison operators on columns with ``ltree``, ``lquery`` or ``ltxtquery`` types are now supported, for searching through data stored in a hierarchical tree-like structure.
|
||||
|
||||
See the documentation at `graphql/core/queries/query-filters` more details on the currently supported ``ltree`` operators.
|
||||
|
||||
**Example query:** Select ancestors of an `ltree` argument
|
||||
|
||||
```
|
||||
query {
|
||||
tree (
|
||||
where: {path: {_ancestor: "Tree.Collections.Pictures.Astronomy.Astronauts"}}
|
||||
) {
|
||||
path
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Example response:**
|
||||
```
|
||||
{
|
||||
"data": {
|
||||
"tree": [
|
||||
{
|
||||
"path": "Tree"
|
||||
},
|
||||
{
|
||||
"path": "Tree.Collections"
|
||||
},
|
||||
{
|
||||
"path": "Tree.Collections.Pictures"
|
||||
},
|
||||
{
|
||||
"path": "Tree.Collections.Pictures.Astronomy"
|
||||
},
|
||||
{
|
||||
"path": "Tree.Collections.Pictures.Astronomy.Astronauts"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Breaking changes
|
||||
|
||||
- This release contains the [PDV refactor (#4111)](https://github.com/hasura/graphql-engine/pull/4111), a significant rewrite of the internals of the server, which did include some breaking changes:
|
||||
@ -151,6 +194,7 @@ have select permissions to the target table of the function.
|
||||
- server: support tracking of functions that return a single row (fix #4299)
|
||||
- server: reduce memory usage consumption of the schema cache structures, and fix a memory leak
|
||||
- server: add source name in livequery logs
|
||||
- server: support ltree comparison operators (close #625)
|
||||
- server: support parsing JWT from cookie header (fix #2183)
|
||||
- console: allow user to cascade Postgres dependencies when dropping Postgres objects (close #5109) (#5248)
|
||||
- console: mark inconsistent remote schemas in the UI (close #5093) (#5181)
|
||||
|
@ -860,6 +860,13 @@ fromOpExpG expression op =
|
||||
IR.CLT _rhsCol -> refute (pure (UnsupportedOpExpG op)) -- S.BECompare S.SLT lhs $ mkQCol rhsCol
|
||||
IR.CGTE _rhsCol -> refute (pure (UnsupportedOpExpG op)) -- S.BECompare S.SGTE lhs $ mkQCol rhsCol
|
||||
IR.CLTE _rhsCol -> refute (pure (UnsupportedOpExpG op)) -- S.BECompare S.SLTE lhs $ mkQCol rhsCol
|
||||
IR.AAncestor _ -> refute (pure (UnsupportedOpExpG op)) -- See Hierarchical Data (SQL Server)
|
||||
IR.AAncestorAny _ -> refute (pure (UnsupportedOpExpG op)) -- See Hierarchical Data (SQL Server)
|
||||
IR.ADescendant _ -> refute (pure (UnsupportedOpExpG op)) -- See Hierarchical Data (SQL Server)
|
||||
IR.ADescendantAny _ -> refute (pure (UnsupportedOpExpG op)) -- See Hierarchical Data (SQL Server)
|
||||
IR.AMatches _ -> refute (pure (UnsupportedOpExpG op)) -- See Hierarchical Data (SQL Server)
|
||||
IR.AMatchesAny _ -> refute (pure (UnsupportedOpExpG op)) -- See Hierarchical Data (SQL Server)
|
||||
IR.AMatchesFulltext _ -> refute (pure (UnsupportedOpExpG op)) -- See Hierarchical Data (SQL Server)
|
||||
|
||||
nullableBoolEquality :: Expression -> Expression -> Expression
|
||||
nullableBoolEquality x y =
|
||||
|
@ -174,6 +174,22 @@ parseBoolExpOperations rhsParser fim columnInfo value = do
|
||||
"$clte" -> parseClte
|
||||
"_clte" -> parseClte
|
||||
|
||||
-- ltree types
|
||||
"_ancestor" -> guardType [PGLtree] >> AAncestor <$> parseOne
|
||||
"$ancestor" -> guardType [PGLtree] >> AAncestor <$> parseOne
|
||||
"_ancestor_any" -> guardType [PGLtree] >> AAncestorAny <$> parseManyWithType (ColumnScalar PGLtree)
|
||||
"$ancestor_any" -> guardType [PGLtree] >> AAncestorAny <$> parseManyWithType (ColumnScalar PGLtree)
|
||||
"_descendant" -> guardType [PGLtree] >> ADescendant <$> parseOne
|
||||
"$descendant" -> guardType [PGLtree] >> ADescendant <$> parseOne
|
||||
"_descendant_any" -> guardType [PGLtree] >> ADescendantAny <$> parseManyWithType (ColumnScalar PGLtree)
|
||||
"$descendant_any" -> guardType [PGLtree] >> ADescendantAny <$> parseManyWithType (ColumnScalar PGLtree)
|
||||
"_matches" -> guardType [PGLtree] >> AMatches <$> parseWithTy (ColumnScalar PGLquery)
|
||||
"$matches" -> guardType [PGLtree] >> AMatches <$> parseWithTy (ColumnScalar PGLquery)
|
||||
"_matches_any" -> guardType [PGLtree] >> AMatchesAny <$> parseManyWithType (ColumnScalar PGLquery)
|
||||
"$matches_any" -> guardType [PGLtree] >> AMatchesAny <$> parseManyWithType (ColumnScalar PGLquery)
|
||||
"_matches_fulltext" -> guardType [PGLtree] >> AMatchesFulltext <$> parseWithTy (ColumnScalar PGLtxtquery)
|
||||
"$matches_fulltext" -> guardType [PGLtree] >> AMatchesFulltext <$> parseWithTy (ColumnScalar PGLtxtquery)
|
||||
|
||||
x -> throw400 UnexpectedPayload $ "Unknown operator : " <> x
|
||||
where
|
||||
colTy = columnReferenceType column
|
||||
|
@ -238,6 +238,10 @@ comparisonExps = P.memoize 'comparisonExps \columnType -> do
|
||||
typedParser <- columnParser columnType (G.Nullability False)
|
||||
nullableTextParser <- columnParser (ColumnScalar PGText) (G.Nullability True)
|
||||
textParser <- columnParser (ColumnScalar PGText) (G.Nullability False)
|
||||
-- `lquery` represents a regular-expression-like pattern for matching `ltree` values.
|
||||
lqueryParser <- columnParser (ColumnScalar PGLquery) (G.Nullability False)
|
||||
-- `ltxtquery` represents a full-text-search-like pattern for matching `ltree` values.
|
||||
ltxtqueryParser <- columnParser (ColumnScalar PGLtxtquery) (G.Nullability False)
|
||||
maybeCastParser <- castExp columnType
|
||||
let name = P.getName typedParser <> $$(G.litName "_comparison_exp")
|
||||
desc = G.Description $ "Boolean expression to compare columns of type "
|
||||
@ -362,6 +366,31 @@ comparisonExps = P.memoize 'comparisonExps \columnType -> do
|
||||
(Just "is the column within a given distance from the given geometry value")
|
||||
(ASTDWithinGeom <$> geomInputParser)
|
||||
]
|
||||
|
||||
-- Ops for Ltree type
|
||||
, guard (isScalarColumnWhere (== PGLtree) columnType) *>
|
||||
[ P.fieldOptional $$(G.litName "_ancestor")
|
||||
(Just "is the left argument an ancestor of right (or equal)?")
|
||||
(AAncestor . mkParameter <$> typedParser)
|
||||
, P.fieldOptional $$(G.litName "_ancestor_any")
|
||||
(Just "does array contain an ancestor of `ltree`?")
|
||||
(AAncestorAny . mkListLiteral columnType <$> columnListParser)
|
||||
, P.fieldOptional $$(G.litName "_descendant")
|
||||
(Just "is the left argument a descendant of right (or equal)?")
|
||||
(ADescendant . mkParameter <$> typedParser)
|
||||
, P.fieldOptional $$(G.litName "_descendant_any")
|
||||
(Just "does array contain a descendant of `ltree`?")
|
||||
(ADescendantAny . mkListLiteral columnType <$> columnListParser)
|
||||
, P.fieldOptional $$(G.litName "_matches")
|
||||
(Just "does `ltree` match `lquery`?")
|
||||
(AMatches . mkParameter <$> lqueryParser)
|
||||
, P.fieldOptional $$(G.litName "_matches_any")
|
||||
(Just "does `ltree` match any `lquery` in array?")
|
||||
(AMatchesAny . mkListLiteral (ColumnScalar PGLquery) <$> textListParser)
|
||||
, P.fieldOptional $$(G.litName "_matches_fulltext")
|
||||
(Just "does `ltree` match `ltxtquery`?")
|
||||
(AMatchesFulltext . mkParameter <$> ltxtqueryParser)
|
||||
]
|
||||
]
|
||||
where
|
||||
mkListLiteral :: ColumnType 'Postgres -> [ColumnValue 'Postgres] -> UnpreparedValue 'Postgres
|
||||
|
@ -740,6 +740,7 @@ data CompareOp
|
||||
| SHasKey
|
||||
| SHasKeysAny
|
||||
| SHasKeysAll
|
||||
| SMatchesFulltext
|
||||
deriving (Eq, Generic, Data)
|
||||
instance NFData CompareOp
|
||||
instance Cacheable CompareOp
|
||||
@ -770,6 +771,7 @@ instance Show CompareOp where
|
||||
SHasKey -> "?"
|
||||
SHasKeysAny -> "?|"
|
||||
SHasKeysAll -> "?&"
|
||||
SMatchesFulltext -> "@"
|
||||
|
||||
instance ToSQL CompareOp where
|
||||
toSQL = fromString . show
|
||||
|
@ -280,6 +280,9 @@ data PGScalarType
|
||||
| PGGeography
|
||||
| PGRaster
|
||||
| PGUUID
|
||||
| PGLtree
|
||||
| PGLquery
|
||||
| PGLtxtquery
|
||||
| PGUnknown !Text
|
||||
deriving (Show, Eq, Ord, Generic, Data)
|
||||
instance NFData PGScalarType
|
||||
@ -312,6 +315,9 @@ instance ToSQL PGScalarType where
|
||||
PGGeography -> "geography"
|
||||
PGRaster -> "raster"
|
||||
PGUUID -> "uuid"
|
||||
PGLtree -> "ltree"
|
||||
PGLquery -> "lquery"
|
||||
PGLtxtquery -> "ltxtquery"
|
||||
PGUnknown t -> TB.text t
|
||||
|
||||
instance ToJSON PGScalarType where
|
||||
@ -386,6 +392,10 @@ pgScalarTranslations =
|
||||
|
||||
, ("raster" , PGRaster)
|
||||
, ("uuid" , PGUUID)
|
||||
|
||||
, ("ltree" , PGLtree)
|
||||
, ("lquery" , PGLquery)
|
||||
, ("ltxtquery" , PGLtxtquery)
|
||||
]
|
||||
|
||||
instance FromJSON PGScalarType where
|
||||
|
@ -55,6 +55,22 @@ instance FromJSON RasterWKB where
|
||||
instance ToJSON RasterWKB where
|
||||
toJSON = toJSON . TC.toText . getRasterWKB
|
||||
|
||||
newtype Ltree = Ltree Text
|
||||
deriving (Show, Eq)
|
||||
|
||||
instance ToJSON Ltree where
|
||||
toJSON (Ltree t) = toJSON t
|
||||
|
||||
instance FromJSON Ltree where
|
||||
parseJSON = \case
|
||||
String t ->
|
||||
if any T.null $ T.splitOn (T.pack ".") t
|
||||
then fail message
|
||||
else pure $ Ltree t
|
||||
_ -> fail message
|
||||
where
|
||||
message = "Expecting label path: a sequence of zero or more labels separated by dots, for example L1.L2.L3"
|
||||
|
||||
-- Binary value. Used in prepared sq
|
||||
data PGScalarValue
|
||||
= PGValInteger !Int32
|
||||
@ -79,6 +95,9 @@ data PGScalarValue
|
||||
| PGValGeo !GeometryWithCRS
|
||||
| PGValRaster !RasterWKB
|
||||
| PGValUUID !UUID.UUID
|
||||
| PGValLtree !Ltree
|
||||
| PGValLquery !Text
|
||||
| PGValLtxtquery !Text
|
||||
| PGValUnknown !Text
|
||||
deriving (Show, Eq)
|
||||
|
||||
@ -109,6 +128,9 @@ pgScalarValueToJson = \case
|
||||
PGValGeo o -> toJSON o
|
||||
PGValRaster r -> toJSON r
|
||||
PGValUUID u -> toJSON u
|
||||
PGValLtree t -> toJSON t
|
||||
PGValLquery t -> toJSON t
|
||||
PGValLtxtquery t -> toJSON t
|
||||
PGValUnknown t -> toJSON t
|
||||
|
||||
withConstructorFn :: PGScalarType -> S.SQLExp -> S.SQLExp
|
||||
@ -138,6 +160,7 @@ parsePGValue ty val = case (ty, val) of
|
||||
(_ , Null) -> pure $ PGNull ty
|
||||
(PGUnknown _, String t) -> pure $ PGValUnknown t
|
||||
(PGRaster , _) -> parseTyped -- strictly parse raster value
|
||||
(PGLtree , _) -> parseTyped
|
||||
(_ , String t) -> parseTyped <|> pure (PGValUnknown t)
|
||||
(_ , _) -> parseTyped
|
||||
where
|
||||
@ -172,6 +195,9 @@ parsePGValue ty val = case (ty, val) of
|
||||
PGGeography -> PGValGeo <$> parseJSON val
|
||||
PGRaster -> PGValRaster <$> parseJSON val
|
||||
PGUUID -> PGValUUID <$> parseJSON val
|
||||
PGLtree -> PGValLtree <$> parseJSON val
|
||||
PGLquery -> PGValLquery <$> parseJSON val
|
||||
PGLtxtquery -> PGValLtxtquery <$> parseJSON val
|
||||
PGUnknown tyName ->
|
||||
fail $ "A string is expected for type: " ++ T.unpack tyName
|
||||
|
||||
@ -225,6 +251,9 @@ txtEncodedPGVal = \case
|
||||
AE.encodeToLazyText o
|
||||
PGValRaster r -> TELit $ TC.toText $ getRasterWKB r
|
||||
PGValUUID u -> TELit $ UUID.toText u
|
||||
PGValLtree (Ltree t) -> TELit t
|
||||
PGValLquery t -> TELit t
|
||||
PGValLtxtquery t -> TELit t
|
||||
PGValUnknown t -> TELit t
|
||||
|
||||
pgTypeOid :: PGScalarType -> PQ.Oid
|
||||
@ -253,6 +282,9 @@ pgTypeOid = \case
|
||||
PGGeography -> PTI.text
|
||||
PGRaster -> PTI.text -- we are using the ST_RastFromHexWKB($i) instead of $i
|
||||
PGUUID -> PTI.uuid
|
||||
PGLtree -> PTI.text
|
||||
PGLquery -> PTI.text
|
||||
PGLtxtquery -> PTI.text
|
||||
(PGUnknown _) -> PTI.auto
|
||||
|
||||
binEncoder :: PGScalarValue -> Q.PrepArg
|
||||
@ -279,6 +311,9 @@ binEncoder = \case
|
||||
PGValGeo o -> Q.toPrepVal $ TL.toStrict $ AE.encodeToLazyText o
|
||||
PGValRaster r -> Q.toPrepVal $ TC.toText $ getRasterWKB r
|
||||
PGValUUID u -> Q.toPrepVal u
|
||||
PGValLtree (Ltree t) -> Q.toPrepVal t
|
||||
PGValLquery t -> Q.toPrepVal t
|
||||
PGValLtxtquery t -> Q.toPrepVal t
|
||||
PGValUnknown t -> (PTI.auto, Just (TE.encodeUtf8 t, PQ.Text))
|
||||
|
||||
txtEncoder :: PGScalarValue -> S.SQLExp
|
||||
|
@ -173,6 +173,14 @@ mkFieldCompExp qual lhsField = mkCompExp (mkQField lhsField)
|
||||
AHasKeysAny val -> S.BECompare S.SHasKeysAny lhs val
|
||||
AHasKeysAll val -> S.BECompare S.SHasKeysAll lhs val
|
||||
|
||||
AAncestor val -> S.BECompare S.SContains lhs val
|
||||
AAncestorAny val -> S.BECompare S.SContains lhs val
|
||||
ADescendant val -> S.BECompare S.SContainedIn lhs val
|
||||
ADescendantAny val -> S.BECompare S.SContainedIn lhs val
|
||||
AMatches val -> S.BECompare S.SREGEX lhs val
|
||||
AMatchesAny val -> S.BECompare S.SHasKey lhs val
|
||||
AMatchesFulltext val -> S.BECompare S.SMatchesFulltext lhs val
|
||||
|
||||
ASTContains val -> mkGeomOpBe "ST_Contains" val
|
||||
ASTCrosses val -> mkGeomOpBe "ST_Crosses" val
|
||||
ASTEquals val -> mkGeomOpBe "ST_Equals" val
|
||||
|
@ -272,6 +272,14 @@ data OpExpG (b :: BackendType) a
|
||||
| ANISNULL -- IS NULL
|
||||
| ANISNOTNULL -- IS NOT NULL
|
||||
|
||||
| AAncestor !a
|
||||
| AAncestorAny !a
|
||||
| ADescendant !a
|
||||
| ADescendantAny !a
|
||||
| AMatches !a
|
||||
| AMatchesAny !a
|
||||
| AMatchesFulltext !a
|
||||
|
||||
| CEQ !(Column b)
|
||||
| CNE !(Column b)
|
||||
| CGT !(Column b)
|
||||
@ -345,6 +353,14 @@ opExpToJPair f = \case
|
||||
ASTIntersectsNbandGeom a -> ("_st_intersects_nband_geom", toJSON $ f <$> a)
|
||||
ASTIntersectsGeomNband a -> ("_st_intersects_geom_nband", toJSON $ f <$> a)
|
||||
|
||||
AAncestor a -> ("_ancestor", f a)
|
||||
AAncestorAny a -> ("_ancestor_any", f a)
|
||||
ADescendant a -> ("_descendant", f a)
|
||||
ADescendantAny a -> ("_descendant_any", f a)
|
||||
AMatches a -> ("_matches", f a)
|
||||
AMatchesAny a -> ("_matches_any", f a)
|
||||
AMatchesFulltext a -> ("_matches_fulltext", f a)
|
||||
|
||||
ANISNULL -> ("_is_null", toJSON True)
|
||||
ANISNOTNULL -> ("_is_null", toJSON False)
|
||||
|
||||
|
@ -116,6 +116,7 @@ response:
|
||||
c43_range_timestamptz: '("2011-02-05 12:03:00+00","2012-03-04 16:40:04+00"]'
|
||||
c44_xml: '<foo>bar</foo>'
|
||||
c45_money: $123.45
|
||||
c46_ltree: 'A.B.C'
|
||||
query:
|
||||
query: |
|
||||
query {
|
||||
@ -185,5 +186,6 @@ query:
|
||||
c43_range_timestamptz
|
||||
c44_xml
|
||||
c45_money
|
||||
c46_ltree
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ args:
|
||||
- type: run_sql
|
||||
args:
|
||||
sql: |
|
||||
CREATE EXTENSION IF NOT EXISTS ltree;
|
||||
CREATE TYPE complex AS (
|
||||
r double precision,
|
||||
i double precision
|
||||
@ -61,7 +62,8 @@ args:
|
||||
c42_range_timestamp tsrange,
|
||||
c43_range_timestamptz tstzrange,
|
||||
c44_xml xml,
|
||||
c45_money money
|
||||
c45_money money,
|
||||
c46_ltree ltree
|
||||
);
|
||||
create table author(
|
||||
id serial primary key,
|
||||
@ -119,6 +121,7 @@ args:
|
||||
, c43_range_timestamptz
|
||||
, c44_xml
|
||||
, c45_money
|
||||
, c46_ltree
|
||||
)
|
||||
values
|
||||
( 32767 -- c1_smallint
|
||||
@ -163,6 +166,7 @@ args:
|
||||
, '(2011-02-05T12:03:00+00:00, 2012-03-04T16:40:04+00:00]' -- c43_range_timestamptz
|
||||
, '<foo>bar</foo>' -- c44_xml
|
||||
, 123.45 -- c45_money
|
||||
, 'A.B.C' -- c46_ltree
|
||||
);
|
||||
insert into author (name, "createdAt")
|
||||
values
|
||||
|
@ -0,0 +1,40 @@
|
||||
- description: Select ancestors of an `ltree` argument
|
||||
url: /v1/graphql
|
||||
status: 200
|
||||
response:
|
||||
data:
|
||||
tree:
|
||||
- path: Tree
|
||||
- path: Tree.Collections
|
||||
- path: Tree.Collections.Pictures
|
||||
- path: Tree.Collections.Pictures.Astronomy
|
||||
- path: Tree.Collections.Pictures.Astronomy.Astronauts
|
||||
query:
|
||||
query: |
|
||||
query {
|
||||
tree (
|
||||
where: {path: {_ancestor: "Tree.Collections.Pictures.Astronomy.Astronauts"}}
|
||||
) {
|
||||
path
|
||||
}
|
||||
}
|
||||
|
||||
- description: Select ancestors of an invalid `ltree` argument
|
||||
url: /v1/graphql
|
||||
status: 200
|
||||
response:
|
||||
errors:
|
||||
- extensions:
|
||||
path: $.selectionSet.tree.args.where.path._ancestor
|
||||
code: parse-failed
|
||||
message: 'Expecting label path: a sequence of zero or more labels separated by
|
||||
dots, for example L1.L2.L3'
|
||||
query:
|
||||
query: |
|
||||
query {
|
||||
tree (
|
||||
where: {path: {_ancestor: "Tree.Collections.Pictures.Astronomy.Astronauts."}}
|
||||
) {
|
||||
path
|
||||
}
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
- description: Select ancestors of an array of `ltree` arguments
|
||||
url: /v1/graphql
|
||||
status: 200
|
||||
response:
|
||||
data:
|
||||
tree:
|
||||
- path: Tree
|
||||
- path: Tree.Hobbies
|
||||
- path: Tree.Collections
|
||||
- path: Tree.Collections.Pictures
|
||||
query:
|
||||
query: |
|
||||
query {
|
||||
tree (
|
||||
where: {path: {_ancestor_any: ["Tree.Collections.Pictures", "Tree.Hobbies"]}}
|
||||
) {
|
||||
path
|
||||
}
|
||||
}
|
||||
|
||||
- description: Select ancestors of an array of invalid `ltree` arguments
|
||||
url: /v1/graphql
|
||||
status: 200
|
||||
response:
|
||||
errors:
|
||||
- extensions:
|
||||
path: $.selectionSet.tree.args.where.path._ancestor_any[0]
|
||||
code: parse-failed
|
||||
message: 'Expecting label path: a sequence of zero or more labels separated by
|
||||
dots, for example L1.L2.L3'
|
||||
query:
|
||||
query: |
|
||||
query {
|
||||
tree (
|
||||
where: {path: {_ancestor_any: ["Tree.Collections.Pictures."]}}
|
||||
) {
|
||||
path
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
description: Select descendants of an `ltree` argument
|
||||
url: /v1/graphql
|
||||
status: 200
|
||||
response:
|
||||
data:
|
||||
tree:
|
||||
- path: "Tree.Science"
|
||||
- path: "Tree.Science.Astronomy"
|
||||
- path: "Tree.Science.Astronomy.Astrophysics"
|
||||
- path: "Tree.Science.Astronomy.Cosmology"
|
||||
query:
|
||||
query: |
|
||||
query {
|
||||
tree (
|
||||
where: {path: {_descendant: "Tree.Science"}}
|
||||
) {
|
||||
path
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
description: Select descendants of an array of `ltree` arguments
|
||||
url: /v1/graphql
|
||||
status: 200
|
||||
response:
|
||||
data:
|
||||
tree:
|
||||
- path: Tree.Science.Astronomy
|
||||
- path: Tree.Science.Astronomy.Astrophysics
|
||||
- path: Tree.Science.Astronomy.Cosmology
|
||||
- path: Tree.Hobbies
|
||||
- path: Tree.Hobbies.Amateurs_Astronomy
|
||||
query:
|
||||
query: |
|
||||
query {
|
||||
tree (
|
||||
where: {path: {_descendant_any: ["Tree.Science.Astronomy", "Tree.Hobbies"]}}
|
||||
) {
|
||||
path
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
- description: Select `ltree` paths matching an `lquery` regex
|
||||
url: /v1/graphql
|
||||
status: 200
|
||||
response:
|
||||
data:
|
||||
tree:
|
||||
- path: "Tree.Science.Astronomy"
|
||||
- path: "Tree.Science.Astronomy.Astrophysics"
|
||||
- path: "Tree.Science.Astronomy.Cosmology"
|
||||
- path: "Tree.Collections.Pictures.Astronomy"
|
||||
- path: "Tree.Collections.Pictures.Astronomy.Stars"
|
||||
- path: "Tree.Collections.Pictures.Astronomy.Galaxies"
|
||||
- path: "Tree.Collections.Pictures.Astronomy.Astronauts"
|
||||
query:
|
||||
query: |
|
||||
query {
|
||||
tree (
|
||||
where: {path: {_matches: "*.Astronomy.*"}}
|
||||
) {
|
||||
path
|
||||
}
|
||||
}
|
||||
|
||||
- description: Select `ltree` paths matching an invalid `lquery` regex
|
||||
url: /v1/graphql
|
||||
status: 200
|
||||
response:
|
||||
errors:
|
||||
- extensions:
|
||||
path: $.query
|
||||
code: validation-failed
|
||||
message: not a valid graphql query
|
||||
query:
|
||||
query: |
|
||||
query {
|
||||
tree (
|
||||
where: {path: {_matches: "*.Astronomy.*\"}}
|
||||
) {
|
||||
path
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
- description: Select `ltree` paths matching any `lquery` regex in an array
|
||||
url: /v1/graphql
|
||||
status: 200
|
||||
response:
|
||||
data:
|
||||
tree:
|
||||
- path: Tree.Hobbies
|
||||
- path: Tree.Hobbies.Amateurs_Astronomy
|
||||
- path: Tree.Collections.Pictures
|
||||
- path: Tree.Collections.Pictures.Astronomy
|
||||
- path: Tree.Collections.Pictures.Astronomy.Stars
|
||||
- path: Tree.Collections.Pictures.Astronomy.Galaxies
|
||||
- path: Tree.Collections.Pictures.Astronomy.Astronauts
|
||||
query:
|
||||
query: |
|
||||
query {
|
||||
tree (
|
||||
where: {path: {_matches_any: ["*.Pictures.*", "*.Hobbies.*"]}}
|
||||
) {
|
||||
path
|
||||
}
|
||||
}
|
||||
|
||||
- description: Select `ltree` paths matching invalid `lquery` regexes in an array
|
||||
url: /v1/graphql
|
||||
status: 200
|
||||
response:
|
||||
errors:
|
||||
- extensions:
|
||||
path: $.query
|
||||
code: validation-failed
|
||||
message: not a valid graphql query
|
||||
query:
|
||||
query: |
|
||||
query {
|
||||
tree (
|
||||
where: {path: {_matches_any: ["*.Pictures.*\", "*.Hobbies.*\"]}}
|
||||
) {
|
||||
path
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
- description: Select `ltree` paths matching an `ltxtquery` full text query
|
||||
url: /v1/graphql
|
||||
status: 200
|
||||
response:
|
||||
data:
|
||||
tree:
|
||||
- path: Tree.Science.Astronomy
|
||||
- path: Tree.Science.Astronomy.Astrophysics
|
||||
- path: Tree.Science.Astronomy.Cosmology
|
||||
- path: Tree.Hobbies.Amateurs_Astronomy
|
||||
query:
|
||||
query: |
|
||||
query {
|
||||
tree (
|
||||
where: {path: {_matches_fulltext: "Astro*% & !pictures@"}}
|
||||
) {
|
||||
path
|
||||
}
|
||||
}
|
||||
|
||||
- description: Select `ltree` paths matching an invalid `ltxtquery` full text query
|
||||
url: /v1/graphql
|
||||
status: 200
|
||||
response:
|
||||
errors:
|
||||
- extensions:
|
||||
path: $.query
|
||||
code: validation-failed
|
||||
message: not a valid graphql query
|
||||
query:
|
||||
query: |
|
||||
query {
|
||||
tree (
|
||||
where: {path: {_matches_fulltext: "Astro*% & !pictures@\"}}
|
||||
) {
|
||||
path
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
type: bulk
|
||||
args:
|
||||
|
||||
|
||||
# Tree table
|
||||
- type: run_sql
|
||||
args:
|
||||
sql: |
|
||||
CREATE EXTENSION IF NOT EXISTS ltree;
|
||||
CREATE TABLE tree (path ltree);
|
||||
INSERT INTO tree VALUES
|
||||
('Tree'),
|
||||
('Tree.Science'),
|
||||
('Tree.Science.Astronomy'),
|
||||
('Tree.Science.Astronomy.Astrophysics'),
|
||||
('Tree.Science.Astronomy.Cosmology'),
|
||||
('Tree.Hobbies'),
|
||||
('Tree.Hobbies.Amateurs_Astronomy'),
|
||||
('Tree.Collections'),
|
||||
('Tree.Collections.Pictures'),
|
||||
('Tree.Collections.Pictures.Astronomy'),
|
||||
('Tree.Collections.Pictures.Astronomy.Stars'),
|
||||
('Tree.Collections.Pictures.Astronomy.Galaxies'),
|
||||
('Tree.Collections.Pictures.Astronomy.Astronauts');
|
||||
CREATE INDEX path_gist_idx ON tree USING GIST (path);
|
||||
CREATE INDEX path_idx ON tree USING BTREE (path);
|
||||
|
||||
- type: track_table
|
||||
args:
|
||||
schema: public
|
||||
name: tree
|
@ -0,0 +1,7 @@
|
||||
type: bulk
|
||||
args:
|
||||
|
||||
- type: run_sql
|
||||
args:
|
||||
sql: |
|
||||
drop table tree
|
@ -0,0 +1,36 @@
|
||||
- description: Select ancestors of an `ltree` argument
|
||||
url: /v1/query
|
||||
status: 200
|
||||
response:
|
||||
- path: Tree
|
||||
- path: Tree.Collections
|
||||
- path: Tree.Collections.Pictures
|
||||
- path: Tree.Collections.Pictures.Astronomy
|
||||
- path: Tree.Collections.Pictures.Astronomy.Astronauts
|
||||
query:
|
||||
type: select
|
||||
args:
|
||||
table: tree
|
||||
where:
|
||||
path:
|
||||
$ancestor: "Tree.Collections.Pictures.Astronomy.Astronauts"
|
||||
columns:
|
||||
- path
|
||||
|
||||
- description: Select ancestors of an invalid `ltree` argument
|
||||
url: /v1/query
|
||||
status: 400
|
||||
response:
|
||||
path: $.args.where.path['$ancestor']
|
||||
error: 'Expecting label path: a sequence of zero or more labels separated by dots,
|
||||
for example L1.L2.L3'
|
||||
code: parse-failed
|
||||
query:
|
||||
type: select
|
||||
args:
|
||||
table: tree
|
||||
where:
|
||||
path:
|
||||
$ancestor: "Tree.Collections.Pictures.Astronomy.Astronauts."
|
||||
columns:
|
||||
- path
|
@ -0,0 +1,35 @@
|
||||
- description: Select ancestors of an array of `ltree` arguments
|
||||
url: /v1/query
|
||||
status: 200
|
||||
response:
|
||||
- path: Tree
|
||||
- path: Tree.Hobbies
|
||||
- path: Tree.Collections
|
||||
- path: Tree.Collections.Pictures
|
||||
query:
|
||||
type: select
|
||||
args:
|
||||
table: tree
|
||||
where:
|
||||
path:
|
||||
$ancestor_any: ["Tree.Collections.Pictures", "Tree.Hobbies"]
|
||||
columns:
|
||||
- path
|
||||
|
||||
- description: Select ancestors of an array of invalid `ltree` arguments
|
||||
url: /v1/query
|
||||
status: 400
|
||||
response:
|
||||
path: $.args.where.path['$ancestor_any'][0]
|
||||
error: 'Expecting label path: a sequence of zero or more labels separated by dots,
|
||||
for example L1.L2.L3'
|
||||
code: parse-failed
|
||||
query:
|
||||
type: select
|
||||
args:
|
||||
table: tree
|
||||
where:
|
||||
path:
|
||||
$ancestor_any: ["Tree.Collections.Pictures."]
|
||||
columns:
|
||||
- path
|
@ -0,0 +1,17 @@
|
||||
description: Select descendants of an `ltree` argument
|
||||
url: /v1/query
|
||||
status: 200
|
||||
response:
|
||||
- path: "Tree.Science"
|
||||
- path: "Tree.Science.Astronomy"
|
||||
- path: "Tree.Science.Astronomy.Astrophysics"
|
||||
- path: "Tree.Science.Astronomy.Cosmology"
|
||||
query:
|
||||
type: select
|
||||
args:
|
||||
table: tree
|
||||
where:
|
||||
path:
|
||||
$descendant: "Tree.Science"
|
||||
columns:
|
||||
- path
|
@ -0,0 +1,18 @@
|
||||
description: Select descendants of an array of `ltree` arguments
|
||||
url: /v1/query
|
||||
status: 200
|
||||
response:
|
||||
- path: Tree.Science.Astronomy
|
||||
- path: Tree.Science.Astronomy.Astrophysics
|
||||
- path: Tree.Science.Astronomy.Cosmology
|
||||
- path: Tree.Hobbies
|
||||
- path: Tree.Hobbies.Amateurs_Astronomy
|
||||
query:
|
||||
type: select
|
||||
args:
|
||||
table: tree
|
||||
where:
|
||||
path:
|
||||
$descendant_any: ["Tree.Science.Astronomy", "Tree.Hobbies"]
|
||||
columns:
|
||||
- path
|
@ -0,0 +1,20 @@
|
||||
- description: Select `ltree` paths matching an `lquery` regex
|
||||
url: /v1/query
|
||||
status: 200
|
||||
response:
|
||||
- path: "Tree.Science.Astronomy"
|
||||
- path: "Tree.Science.Astronomy.Astrophysics"
|
||||
- path: "Tree.Science.Astronomy.Cosmology"
|
||||
- path: "Tree.Collections.Pictures.Astronomy"
|
||||
- path: "Tree.Collections.Pictures.Astronomy.Stars"
|
||||
- path: "Tree.Collections.Pictures.Astronomy.Galaxies"
|
||||
- path: "Tree.Collections.Pictures.Astronomy.Astronauts"
|
||||
query:
|
||||
type: select
|
||||
args:
|
||||
table: tree
|
||||
where:
|
||||
path:
|
||||
$matches: "*.Astronomy.*"
|
||||
columns:
|
||||
- path
|
@ -0,0 +1,20 @@
|
||||
- description: Select `ltree` paths matching any `lquery` regex in an array
|
||||
url: /v1/query
|
||||
status: 200
|
||||
response:
|
||||
- path: Tree.Hobbies
|
||||
- path: Tree.Hobbies.Amateurs_Astronomy
|
||||
- path: Tree.Collections.Pictures
|
||||
- path: Tree.Collections.Pictures.Astronomy
|
||||
- path: Tree.Collections.Pictures.Astronomy.Stars
|
||||
- path: Tree.Collections.Pictures.Astronomy.Galaxies
|
||||
- path: Tree.Collections.Pictures.Astronomy.Astronauts
|
||||
query:
|
||||
type: select
|
||||
args:
|
||||
table: tree
|
||||
where:
|
||||
path:
|
||||
$matches_any: ["*.Pictures.*", "*.Hobbies.*"]
|
||||
columns:
|
||||
- path
|
@ -0,0 +1,49 @@
|
||||
- description: Select `ltree` paths matching an `ltxtquery` full text query
|
||||
url: /v1/query
|
||||
status: 200
|
||||
response:
|
||||
- path: Tree.Science.Astronomy
|
||||
- path: Tree.Science.Astronomy.Astrophysics
|
||||
- path: Tree.Science.Astronomy.Cosmology
|
||||
- path: Tree.Hobbies.Amateurs_Astronomy
|
||||
query:
|
||||
type: select
|
||||
args:
|
||||
table: tree
|
||||
where:
|
||||
path:
|
||||
$matches_fulltext: "Astro*% & !pictures@"
|
||||
columns:
|
||||
- path
|
||||
|
||||
- description: Select `ltree` paths matching an invalid `ltxtquery` full text query
|
||||
url: /v1/query
|
||||
status: 500
|
||||
response:
|
||||
internal:
|
||||
statement: "SELECT coalesce(json_agg(\"root\" ), '[]' ) AS \"root\" FROM (SELECT\
|
||||
\ row_to_json((SELECT \"_1_e\" FROM (SELECT \"_0_root.base\".\"path\" AS\
|
||||
\ \"path\" ) AS \"_1_e\" ) ) AS \"root\" FROM (SELECT * FROM \"\
|
||||
public\".\"tree\" WHERE ((\"public\".\"tree\".\"path\") @ (($1)::ltxtquery))\
|
||||
\ ) AS \"_0_root.base\" ) AS \"_2_root\" "
|
||||
prepared: true
|
||||
error:
|
||||
exec_status: FatalError
|
||||
hint:
|
||||
message: syntax error
|
||||
status_code: '42601'
|
||||
description:
|
||||
arguments:
|
||||
- (Oid 25,Just ("Astro*% & !pictures@\\",Binary))
|
||||
path: $.args
|
||||
error: database query error
|
||||
code: unexpected
|
||||
query:
|
||||
type: select
|
||||
args:
|
||||
table: tree
|
||||
where:
|
||||
path:
|
||||
$matches_fulltext: "Astro*% & !pictures@\\"
|
||||
columns:
|
||||
- path
|
31
server/tests-py/queries/v1/select/boolexp/ltree/setup.yaml
Normal file
31
server/tests-py/queries/v1/select/boolexp/ltree/setup.yaml
Normal file
@ -0,0 +1,31 @@
|
||||
type: bulk
|
||||
args:
|
||||
|
||||
|
||||
# Tree table
|
||||
- type: run_sql
|
||||
args:
|
||||
sql: |
|
||||
CREATE EXTENSION IF NOT EXISTS ltree;
|
||||
CREATE TABLE tree (path ltree);
|
||||
INSERT INTO tree VALUES
|
||||
('Tree'),
|
||||
('Tree.Science'),
|
||||
('Tree.Science.Astronomy'),
|
||||
('Tree.Science.Astronomy.Astrophysics'),
|
||||
('Tree.Science.Astronomy.Cosmology'),
|
||||
('Tree.Hobbies'),
|
||||
('Tree.Hobbies.Amateurs_Astronomy'),
|
||||
('Tree.Collections'),
|
||||
('Tree.Collections.Pictures'),
|
||||
('Tree.Collections.Pictures.Astronomy'),
|
||||
('Tree.Collections.Pictures.Astronomy.Stars'),
|
||||
('Tree.Collections.Pictures.Astronomy.Galaxies'),
|
||||
('Tree.Collections.Pictures.Astronomy.Astronauts');
|
||||
CREATE INDEX path_gist_idx ON tree USING GIST (path);
|
||||
CREATE INDEX path_idx ON tree USING BTREE (path);
|
||||
|
||||
- type: track_table
|
||||
args:
|
||||
schema: public
|
||||
name: tree
|
@ -0,0 +1,7 @@
|
||||
type: bulk
|
||||
args:
|
||||
|
||||
- type: run_sql
|
||||
args:
|
||||
sql: |
|
||||
drop table tree
|
@ -830,3 +830,31 @@ class TestGraphQLQueryFunctionPermissions:
|
||||
st_code, resp = hge_ctx.v1metadataq_f(self.dir() + 'add_function_permission_get_articles.yaml')
|
||||
assert st_code == 200, resp
|
||||
check_query_f(hge_ctx, self.dir() + 'get_articles_with_permission_configured.yaml')
|
||||
|
||||
@pytest.mark.parametrize('transport', ['http', 'websocket'])
|
||||
@usefixtures('per_class_tests_db_state')
|
||||
class TestGraphQLQueryBoolExpLtree:
|
||||
def test_select_path_where_ancestor(self, hge_ctx, transport):
|
||||
check_query_f(hge_ctx, self.dir() + '/select_path_where_ancestor.yaml')
|
||||
|
||||
def test_select_path_where_ancestor_array(self, hge_ctx, transport):
|
||||
check_query_f(hge_ctx, self.dir() + '/select_path_where_ancestor_array.yaml')
|
||||
|
||||
def test_select_path_where_descendant(self, hge_ctx, transport):
|
||||
check_query_f(hge_ctx, self.dir() + '/select_path_where_descendant.yaml')
|
||||
|
||||
def test_select_path_where_descendant_array(self, hge_ctx, transport):
|
||||
check_query_f(hge_ctx, self.dir() + '/select_path_where_descendant_array.yaml')
|
||||
|
||||
def test_select_path_where_matches(self, hge_ctx, transport):
|
||||
check_query_f(hge_ctx, self.dir() + '/select_path_where_matches.yaml')
|
||||
|
||||
def test_select_path_where_matches_array(self, hge_ctx, transport):
|
||||
check_query_f(hge_ctx, self.dir() + '/select_path_where_matches_array.yaml')
|
||||
|
||||
def test_select_path_where_matches_ltxtquery(self, hge_ctx, transport):
|
||||
check_query_f(hge_ctx, self.dir() + '/select_path_where_matches_ltxtquery.yaml')
|
||||
|
||||
@classmethod
|
||||
def dir(cls):
|
||||
return 'queries/graphql_query/boolexp/ltree'
|
||||
|
@ -775,3 +775,31 @@ class TestBulkQuery:
|
||||
|
||||
def test_run_bulk_with_select_and_reads(self, hge_ctx):
|
||||
check_query_f(hge_ctx, self.dir() + '/select_with_reads.yaml')
|
||||
|
||||
@pytest.mark.parametrize('transport', ['http', 'websocket'])
|
||||
@usefixtures('per_class_tests_db_state')
|
||||
class TestGraphQLQueryBoolExpLtree:
|
||||
def test_select_path_where_ancestor(self, hge_ctx, transport):
|
||||
check_query_f(hge_ctx, self.dir() + '/select_path_where_ancestor.yaml')
|
||||
|
||||
def test_select_path_where_ancestor_array(self, hge_ctx, transport):
|
||||
check_query_f(hge_ctx, self.dir() + '/select_path_where_ancestor_array.yaml')
|
||||
|
||||
def test_select_path_where_descendant(self, hge_ctx, transport):
|
||||
check_query_f(hge_ctx, self.dir() + '/select_path_where_descendant.yaml')
|
||||
|
||||
def test_select_path_where_descendant_array(self, hge_ctx, transport):
|
||||
check_query_f(hge_ctx, self.dir() + '/select_path_where_descendant_array.yaml')
|
||||
|
||||
def test_select_path_where_matches(self, hge_ctx, transport):
|
||||
check_query_f(hge_ctx, self.dir() + '/select_path_where_matches.yaml')
|
||||
|
||||
def test_select_path_where_matches_array(self, hge_ctx, transport):
|
||||
check_query_f(hge_ctx, self.dir() + '/select_path_where_matches_array.yaml')
|
||||
|
||||
def test_select_path_where_matches_ltxtquery(self, hge_ctx, transport):
|
||||
check_query_f(hge_ctx, self.dir() + '/select_path_where_matches_ltxtquery.yaml')
|
||||
|
||||
@classmethod
|
||||
def dir(cls):
|
||||
return 'queries/v1/select/boolexp/ltree'
|
||||
|
Loading…
Reference in New Issue
Block a user