Support Postgres POSIX regex operators (close #4317) (#119)

Co-authored-by: christophediprima <dipdipdip84@gmail.com>
Co-authored-by: dip <dipdipdip84@gmail.com>
Co-authored-by: Auke Booij <auke@hasura.io>
Co-authored-by: Antoine Leblanc <antoine@hasura.io>
GITHUB_PR_NUMBER: 6172
GITHUB_PR_URL: https://github.com/hasura/graphql-engine/pull/6172
GitOrigin-RevId: 5192d238b527cd21b6efb2f74e279ecc34756c29
This commit is contained in:
hasura-bot 2020-11-27 16:23:58 +05:30
parent 86aef93d31
commit 7b31ff99d1
38 changed files with 416 additions and 20 deletions

View File

@ -62,6 +62,7 @@ This release contains the [PDV refactor (#4111)](https://github.com/hasura/graph
- server: validate remote schema queries (fixes #4143)
- server: introduce optional custom table name in table configuration to track the table according to the custom name. The `set_table_custom_fields` API has been deprecated, A new API `set_table_customization` has been added to set the configuration. (#3811)
- server: support joining Int or String scalar types to ID scalar type in remote relationship
- server: add support for POSIX operators (close #4317) (#6172)
- 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)
- console: remove ONLY as default for ALTER TABLE in column alter operations (close #5512) #5706
@ -122,7 +123,7 @@ For a more comprehensive overview, please see [the readme located here](./contri
**Sample Code**
```ts
import { TableEntry } from "../generated/HasuraMetadataV2"
import { TableEntry } from "../generated/HasuraMetadataV2";
const newTable: TableEntry = {
table: { schema: "public", name: "user" },
@ -140,7 +141,7 @@ const newTable: TableEntry = {
},
},
],
}
};
```
**IntelliSense Example**
@ -178,14 +179,14 @@ arguments.
- server: allow configuring timeouts for actions (fixes #4966)
- server: fix bug which arised when renaming a table which had a manual relationship defined (close #4158)
- server: limit the length of event trigger names (close #5786)
**NOTE:** If you have event triggers with names greater than 42 chars, then you should update their names to avoid running into Postgres identifier limit bug (#5786)
**NOTE:** If you have event triggers with names greater than 42 chars, then you should update their names to avoid running into Postgres identifier limit bug (#5786)
- server: enable HASURA_GRAPHQL_PG_CONN_LIFETIME by default to reclaim memory
- server: fix issue with tracking custom functions that return `SETOF` materialized view (close #5294) (#5945)
- server: allow remote relationships with union, interface and enum type fields as well (fixes #5875) (#6080)
- server: Fix fine-grained incremental cache invalidation (fix #6027)
This issue could cause enum table values to sometimes not be properly reloaded without restarting `graphql-engine`. Now a `reload_metadata` API call (or clicking “Reload enum values” in the console) should consistently force a reload of all enum table values.
- server: fix event trigger cleanup on deletion via replace_metadata (fix #5461) (#6137)
**WARNING**: This can cause significant load on PG on startup if you have lots of event triggers. Delay in starting up is expected.
**WARNING**: This can cause significant load on PG on startup if you have lots of event triggers. Delay in starting up is expected.
- console: add notifications (#5070)
- cli: fix bug in metadata apply which made the server aquire some redundant and unnecessary locks (close #6115)
- cli: fix cli-migrations-v2 image failing to run as a non root user (close #4651, close #5333)
@ -195,7 +196,6 @@ arguments.
- docs: add postgres concepts page to docs (close #4440) (#4471)
- docs: add guides on connecting hasura cloud to pg databases of different cloud vendors (#5948)
## `v1.3.2`
### Bug fixes and improvements

View File

@ -350,6 +350,42 @@ const toPayload = {
"ofType": null
},
"description": null
}, {
"name": "_nregex",
"defaultValue": null,
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"description": null
}, {
"name": "_regex",
"defaultValue": null,
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"description": null
}, {
"name": "_niregex",
"defaultValue": null,
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"description": null
}, {
"name": "_iregex",
"defaultValue": null,
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"description": null
}],
"kind": "INPUT_OBJECT",
"possibleTypes": null,

View File

@ -581,6 +581,10 @@ input String_comparison_exp {
_nlike: String
_nsimilar: String
_similar: String
_niregex: String
_iregex: String
_nregex: String
_regex: String
}
# subscription root
@ -654,4 +658,3 @@ input uuid_comparison_exp {
_neq: uuid
_nin: [uuid!]
}

View File

@ -19,7 +19,11 @@ export type Operator =
| '$ilike'
| '$nilike'
| '$similar'
| '$nsimilar';
| '$nsimilar'
| '$regex'
| '$iregex'
| '$nregex'
| '$niregex';
// Operator with names and aliases
export type OperatorDef = {

View File

@ -30,6 +30,22 @@ export const allOperators: OperatorDef[] = [
},
{ name: 'similar', operator: '$similar', alias: '_similar' },
{ name: 'not similar', operator: '$nsimilar', alias: '_nsimilar' },
{ name: '~', operator: '$regex', alias: '_regex' },
{
name: '~*',
operator: '$iregex',
alias: '_iregex',
},
{
name: '!~',
operator: '$nregex',
alias: '_nregex',
},
{
name: '!~*',
operator: '$niregex',
alias: '_niregex',
},
];
export const getOperatorDefaultValue = (op: Operator) => {

View File

@ -154,6 +154,22 @@ const columnOperatorsInfo = {
type: 'pattern_match',
inputStructure: 'object',
},
_regex: {
type: 'pattern_match',
inputStructure: 'object',
},
_iregex: {
type: 'pattern_match',
inputStructure: 'object',
},
_nregex: {
type: 'pattern_match',
inputStructure: 'object',
},
_niregex: {
type: 'pattern_match',
inputStructure: 'object',
},
_contains: {
type: 'jsonb',
inputStructure: 'object',

View File

@ -28,6 +28,27 @@ export const Operators = [
},
{ name: 'similar', value: '$similar', graphqlOp: '_similar' },
{ name: 'not similar', value: '$nsimilar', graphqlOp: '_nsimilar' },
{
name: '~',
value: '$regex',
graphqlOp: '_regex',
},
{
name: '~*',
value: '$iregex',
graphqlOp: '_iregex',
},
{
name: '!~',
value: '$nregex',
graphqlOp: '_nregex',
},
{
name: '!~*',
value: '$niregex',
graphqlOp: '_niregex',
},
];
export const Integers = [

View File

@ -82,6 +82,22 @@
"type": "string",
"title": "_similar"
},
"_regex": {
"type": "string",
"title": "_regex"
},
"_iregex": {
"type": "string",
"title": "_iregex"
},
"_nregex": {
"type": "string",
"title": "_nregex"
},
"_niregex": {
"type": "string",
"title": "_niregex"
},
"_eq": {
"type": "string",
"title": "_eq"

View File

@ -24,6 +24,10 @@ interface StringOperator extends GenericOperator<string> {
_nlike?: string
_nsimilar?: string
_similar?: string
_regex?: string
_iregex?: string
_nregex?: string
_niregex?: string
}
/** expression to compare columns of type json. All fields are combined with logical 'AND'. */

View File

@ -717,6 +717,10 @@ Operator
- ``_nilike``
- ``_similar``
- ``_nsimilar``
- ``_iregex``
- ``_niregex``
- ``_regex``
- ``_nregex``
**Checking for NULL values:**

View File

@ -605,6 +605,14 @@ Operator
- ``SIMILAR TO``
* - ``_nsimilar``
- ``NOT SIMILAR TO``
* - ``_regex``
- ``~``
* - ``_iregex``
- ``~*``
* - ``_nregex``
- ``!~``
* - ``_niregex``
- ``!~*``
(For more details on text related operators, refer to the `Postgres docs <https://www.postgresql.org/docs/current/functions-matching.html>`__.)

View File

@ -333,6 +333,15 @@ Operator
- ``SIMILAR TO``
* - ``"$nsimilar"``
- ``NOT SIMILAR TO``
* - ``$regex``
- ``~``
* - ``$iregex``
- ``~*``
* - ``$nregex``
- ``!~``
* - ``$niregex``
- ``!~*``
(For more details on text related operators, refer to the `Postgres docs <https://www.postgresql.org/docs/current/functions-matching.html>`__.)

View File

@ -425,7 +425,7 @@ Fetch a list of those authors whose names are NOT part of a list:
Text search or pattern matching operators (_like, _similar, etc.)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The ``_like``, ``_nlike``, ``_ilike``, ``_nilike``, ``_similar``, ``_nsimilar`` operators are used for
The ``_like``, ``_nlike``, ``_ilike``, ``_nilike``, ``_similar``, ``_nsimilar``, ``_regex``, ``_nregex``, ``_iregex``, ``_niregex`` operators are used for
pattern matching on string/text fields.
For more details on text search operators and Postgres equivalents, refer to the :ref:`API reference <text_operators>`.

View File

@ -725,15 +725,19 @@ data CompareOp
| SLT
| SIN
| SNE
| SGTE
| SLTE
| SNIN
| SLIKE
| SNLIKE
| SILIKE
| SNILIKE
| SSIMILAR
| SNSIMILAR
| SGTE
| SLTE
| SNIN
| SREGEX
| SIREGEX
| SNREGEX
| SNIREGEX
| SContains
| SContainedIn
| SHasKey
@ -760,6 +764,10 @@ instance Show CompareOp where
SNILIKE -> "NOT ILIKE"
SSIMILAR -> "SIMILAR TO"
SNSIMILAR -> "NOT SIMILAR TO"
SREGEX -> "~"
SIREGEX -> "~*"
SNREGEX -> "!~"
SNIREGEX -> "!~*"
SContains -> "@>"
SContainedIn -> "<@"
SHasKey -> "?"

View File

@ -107,6 +107,15 @@ parseOperationsExpression rhsParser fim columnInfo =
"$nsimilar" -> parseNsimilar
"_nsimilar" -> parseNsimilar
"$regex" -> parseRegex
"_regex" -> parseRegex
"$iregex" -> parseIRegex
"_iregex" -> parseIRegex
"$nregex" -> parseNRegex
"_nregex" -> parseNRegex
"$niregex" -> parseNIRegex
"_niregex" -> parseNIRegex
"$is_null" -> parseIsNull
"_is_null" -> parseIsNull
@ -180,6 +189,10 @@ parseOperationsExpression rhsParser fim columnInfo =
parseNilike = guardType stringTypes >> ANILIKE () <$> parseOne
parseSimilar = guardType stringTypes >> ASIMILAR <$> parseOne
parseNsimilar = guardType stringTypes >> ANSIMILAR <$> parseOne
parseRegex = guardType stringTypes >> AREGEX <$> parseOne
parseIRegex = guardType stringTypes >> AIREGEX <$> parseOne
parseNRegex = guardType stringTypes >> ANREGEX <$> parseOne
parseNIRegex = guardType stringTypes >> ANIREGEX <$> parseOne
parseIsNull = bool ANISNOTNULL ANISNULL -- is null
<$> parseVal
@ -404,6 +417,10 @@ mkFieldCompExp qual lhsField = mkCompExp (mkQField lhsField)
ANILIKE _ val -> S.BECompare S.SNILIKE lhs val
ASIMILAR val -> S.BECompare S.SSIMILAR lhs val
ANSIMILAR val -> S.BECompare S.SNSIMILAR lhs val
AREGEX val -> S.BECompare S.SREGEX lhs val
AIREGEX val -> S.BECompare S.SIREGEX lhs val
ANREGEX val -> S.BECompare S.SNREGEX lhs val
ANIREGEX val -> S.BECompare S.SNIREGEX lhs val
AContains val -> S.BECompare S.SContains lhs val
AContainedIn val -> S.BECompare S.SContainedIn lhs val
AHasKey val -> S.BECompare S.SHasKey lhs val

View File

@ -150,6 +150,18 @@ comparisonExps = P.memoize 'comparisonExps \columnType -> do
, P.fieldOptional $$(G.litName "_nsimilar")
(Just "does the column NOT match the given SQL regular expression")
(ANSIMILAR . mkParameter <$> columnParser)
, P.fieldOptional $$(G.litName "_regex")
(Just "does the column match the given POSIX regular expression, case sensitive")
(AREGEX . mkParameter <$> columnParser)
, P.fieldOptional $$(G.litName "_iregex")
(Just "does the column match the given POSIX regular expression, case insensitive")
(AIREGEX . mkParameter <$> columnParser)
, P.fieldOptional $$(G.litName "_nregex")
(Just "does the column NOT match the given POSIX regular expression, case sensitive")
(ANREGEX . mkParameter <$> columnParser)
, P.fieldOptional $$(G.litName "_niregex")
(Just "does the column NOT match the given POSIX regular expression, case insensitive")
(ANIREGEX . mkParameter <$> columnParser)
]
-- Ops for JSONB type
, guard (isScalarColumnWhere (== PGJSONB) columnType) *>

View File

@ -241,6 +241,15 @@ data OpExpG (b :: BackendType) a
| ASIMILAR !a -- similar, regex
| ANSIMILAR !a-- not similar, regex
-- Now that in the RQL code we've started to take a "trees that grow"
-- approach (see PR #6003), we may eventually want to move these
-- recently added constructors, which correspond to newly supported
-- Postgres operators, to the backend-specific extensions of this type.
| AREGEX !a -- match POSIX case sensitive, regex
| AIREGEX !a -- match POSIX case insensitive, regex
| ANREGEX !a -- dont match POSIX case sensitive, regex
| ANIREGEX !a -- dont match POSIX case insensitive, regex
| AContains !a
| AContainedIn !a
| AHasKey !a
@ -311,6 +320,11 @@ opExpToJPair f = \case
ASIMILAR a -> ("_similar", f a)
ANSIMILAR a -> ("_nsimilar", f a)
AREGEX a -> ("_regex", f a)
AIREGEX a -> ("_iregex", f a)
ANREGEX a -> ("_nregex", f a)
ANIREGEX a -> ("_niregex", f a)
AContains a -> ("_contains", f a)
AContainedIn a -> ("_contained_in", f a)
AHasKey a -> ("_has_key", f a)

View File

@ -1,4 +1,4 @@
description: Select cities ending with ham
description: Select cities starting with new
url: /v1/graphql
status: 200
response:

View File

@ -0,0 +1,18 @@
description: Select cities starting with fram
url: /v1/graphql
status: 200
response:
data:
city:
- name: Framlingham
country: UK
query:
query: |
query {
city (
where: {name: {_iregex: "fram.*" }}
) {
name
country
}
}

View File

@ -1,4 +1,4 @@
description: Select cities ending with ham
description: Select cities not starting with new
url: /v1/graphql
status: 200
response:

View File

@ -0,0 +1,20 @@
description: Select cities not starting with new
url: /v1/graphql
status: 200
response:
data:
city:
- name: Durham
country: USA
- name: Framlingham
country: UK
query:
query: |
query {
city (
where: {name: {_niregex: "new.*" }}
) {
name
country
}
}

View File

@ -1,4 +1,4 @@
description: Select cities ending with ham
description: Select cities not ending with ham
url: /v1/graphql
status: 200
response:

View File

@ -1,4 +1,4 @@
description: Select cities ending with ham
description: Select cities not ending with ham
url: /v1/graphql
status: 200
response:

View File

@ -0,0 +1,24 @@
description: Select cities not starting with new (case-sensitively)
url: /v1/graphql
status: 200
response:
data:
city:
- name: Durham
country: USA
- name: New York
country: USA
- name: Framlingham
country: UK
- name: New Orleans
country: USA
query:
query: |
query {
city (
where: {name: {_nregex: "new.*" }}
) {
name
country
}
}

View File

@ -0,0 +1,18 @@
description: Select cities ending with gham
url: /v1/graphql
status: 200
response:
data:
city:
- name: Framlingham
country: UK
query:
query: |
query {
city (
where: {name: {_regex: ".*gham" }}
) {
name
country
}
}

View File

@ -1,4 +1,4 @@
description: Select cities ending with ham
description: Select cities ending with gham
url: /v1/graphql
status: 200
response:

View File

@ -0,0 +1,18 @@
- description: user can only select artists having name matching X-Hasura-iregex-artists
url: /v1/graphql
status: 200
headers:
X-Hasura-Role: user_iregex
X-Hasura-iregex-artists: "(.*ak)|(.*lla)"
response:
data:
Artist:
- name: Akon
- name: Camilla
query:
query: |
query {
Artist (order_by: {name: asc}) {
name
}
}

View File

@ -266,6 +266,17 @@ args:
name:
_nin: X-Hasura-Premium-Artists
#Create select permission on Track using _iregex operator
- type: create_select_permission
args:
table: Artist
role: user_iregex
permission:
columns: '*'
filter:
name:
_iregex: X-Hasura-IRegex-Artists
# Create search_track function
- type: run_sql
args:

View File

@ -1,4 +1,4 @@
description: Select cities ending with ham
description: Select cities starting with new
url: /v1/query
status: 200
response:

View File

@ -0,0 +1,16 @@
description: Select cities starting with fram
url: /v1/query
status: 200
response:
- name: Framlingham
country: UK
query:
type: select
args:
table: city
where:
name:
$iregex: 'fram.*'
columns:
- name
- country

View File

@ -1,4 +1,4 @@
description: Select cities ending with ham
description: Select cities not starting with new
url: /v1/query
status: 200
response:

View File

@ -0,0 +1,18 @@
description: Select cities not starting with new (case-sensitively)
url: /v1/query
status: 200
response:
- name: Durham
country: USA
- name: Framlingham
country: UK
query:
type: select
args:
table: city
where:
name:
$niregex: 'new.*'
columns:
- name
- country

View File

@ -1,4 +1,4 @@
description: Select cities ending with ham
description: Select cities not ending with ham
url: /v1/query
status: 200
response:

View File

@ -0,0 +1,22 @@
description: Select cities not starting with new (case-sensitively)
url: /v1/query
status: 200
response:
- name: Durham
country: USA
- name: New York
country: USA
- name: Framlingham
country: UK
- name: New Orleans
country: USA
query:
type: select
args:
table: city
where:
name:
$nregex: 'new.*'
columns:
- name
- country

View File

@ -0,0 +1,16 @@
description: Select cities ending with gham
url: /v1/query
status: 200
response:
- name: Framlingham
country: UK
query:
type: select
args:
table: city
where:
name:
$regex: '.*gham'
columns:
- name
- country

View File

@ -1,4 +1,4 @@
description: Select cities ending with ham
description: Select cities ending with gham
url: /v1/query
status: 200
response:

View File

@ -337,6 +337,9 @@ class TestGraphqlQueryPermissions:
def test_in_and_nin(self, hge_ctx, transport):
check_query_f(hge_ctx, self.dir() + '/in_and_nin.yaml', transport)
def test_iregex(self, hge_ctx, transport):
check_query_f(hge_ctx, self.dir() + '/iregex.yaml', transport)
def test_user_accessing_books_by_pk_should_fail(self, hge_ctx, transport):
check_query_f(hge_ctx, self.dir() + '/user_should_not_be_able_to_access_books_by_pk.yaml')
@ -370,6 +373,18 @@ class TestGraphQLQueryBoolExpSearch:
def test_city_where_not_similar(self, hge_ctx, transport):
check_query_f(hge_ctx, self.dir() + '/select_city_where_not_similar.yaml', transport)
def test_city_where_regex(self, hge_ctx, transport):
check_query_f(hge_ctx, self.dir() + '/select_city_where_regex.yaml', transport)
def test_city_where_nregex(self, hge_ctx, transport):
check_query_f(hge_ctx, self.dir() + '/select_city_where_nregex.yaml', transport)
def test_city_where_iregex(self, hge_ctx, transport):
check_query_f(hge_ctx, self.dir() + '/select_city_where_iregex.yaml', transport)
def test_city_where_niregex(self, hge_ctx, transport):
check_query_f(hge_ctx, self.dir() + '/select_city_where_niregex.yaml', transport)
@classmethod
def dir(cls):
return 'queries/graphql_query/boolexp/search'

View File

@ -168,6 +168,18 @@ class TestV1SelectBoolExpSearch:
def test_city_where_not_similar(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + '/select_city_where_not_similar.yaml')
def test_city_where_regex(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + '/select_city_where_regex.yaml')
def test_city_where_not_regex(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + '/select_city_where_nregex.yaml')
def test_city_where_iregex(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + '/select_city_where_iregex.yaml')
def test_city_where_not_iregex(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + '/select_city_where_niregex.yaml')
@classmethod
def dir(cls):
return 'queries/v1/select/boolexp/search'