server: support ltree operators (close #625)

GitOrigin-RevId: fb6a3eb8cbe4604789938bcbc78916fbcd1af515
This commit is contained in:
Abby Sassel 2021-02-25 11:05:51 +00:00 committed by hasura-bot
parent f55bbe96ef
commit fb1a0d286d
31 changed files with 762 additions and 24 deletions

View File

@ -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)

View File

@ -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 =

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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
}
}

View File

@ -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

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View 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

View File

@ -0,0 +1,7 @@
type: bulk
args:
- type: run_sql
args:
sql: |
drop table tree

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View 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

View File

@ -0,0 +1,7 @@
type: bulk
args:
- type: run_sql
args:
sql: |
drop table tree

View File

@ -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'

View File

@ -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'