Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for conditional querying #138

Open
rohansharmaa opened this issue Mar 23, 2021 · 22 comments
Open

Support for conditional querying #138

rohansharmaa opened this issue Mar 23, 2021 · 22 comments
Assignees

Comments

@rohansharmaa
Copy link

Sorry, I'm new to this.
Is there a way to generate conditional queries like -

query reviews {
      Reviews(where: { id:  {_eq : <some number> } } ) {
          id
          review_text
      }
}

I'm only asking about the "where: { id : { _eq : <some_number> } } "part, how do I add such conditions in my Operation ?

@barbieri
Copy link
Member

These are called arguments in GraphQL world (knowing the name makes it easier to search).

Take a look at examples at https://sgqlc.readthedocs.io/en/latest/sgqlc.operation.html#selecting-to-generate-queries, in particular look for arguments and you'll find this one, among others:

>>> op.repository(id='repo1').__fields__(
...    issues={'title_contains': 'bug'}, # adds field and children
... )

This should also work:

>>> op.repository(id='repo1').issues(title_contains='bug')

Then in your case, the following should work:

op.reviews(where={'id': {'_eq': Variable('some_number') }}).__fields__('id', 'review_text')

Two advices:

  • You can always use sgqlc-codegen operation to generate the Python code from the GraphQL DSL... (and if it's broken, let me know!)
  • Try to use Variable('some_number') as written above, this way you keep the same query and just change the variable, often helps the backend implementations, some can cache the parsed query

@barbieri barbieri self-assigned this Mar 23, 2021
@rohansharmaa
Copy link
Author

rohansharmaa commented Mar 23, 2021

This is what I tried,

class Query(Type):
    reviews = Field(reviews ,args={'where':dict})

op = Operation(Query)
rating = op.reviews(where={'id':{'_eq': 12}}).__fields__('id','review_text')

But it gave me a KeyError: <class 'dict'> followed by a TypeError: Not BaseType or mapped: <class 'dict'>
Is dict not supported for arguments, or am I doing it wrong?
I will also try the sgqlc-codegen operation as you suggested, but I'm just curious as to why the above method does not work.

@barbieri
Copy link
Member

there isn't a mapping to a generic dict, you must create Input types for your where argument. Then you declare which keys do you expect, their types (including wrappers/modifiers such as list, non-null).

How is your server defining this? You need to use the same types and signatures in the client side, otherwise the server may reject it, in particular if you're using variables, any modifier mismatch will cause errors.

If you didn't write the server yet, I recommend you to go explicit and do not try to map a generic JSON object (you can't because all the object keys must be fixed, you'd have to create an array of key-value objects instead). Example schema:

input WhereIntRange {
  min: Int!
  max: Int!
}

input WhereInt {
  eq: Int
  lt: Int
  gt: Int
  le: Int
 ge: Int
 range: WhereIntRange
}

input ReviewsWhere {
  id: WhereInt
}

type Query {
   reviews(where: ReviewsWhere): [Review!]!
}

@philon123
Copy link

philon123 commented Apr 4, 2021

@rohansharmaa I assume you are trying to do the same that I am - using Hasura as a backend. Just use the code generator to generate the "Query" part and then your Operation that you posted above should work fine.

So far I've had no trouble integrating sgqlc with Hasura. The code generator works like a charm and I'm successfully running queries. Regarding the syntax, @barbieri thanks for noting that the code generator can transform raw graphql queries to python code! It allows to use the Hasura documentation and GraphiQL to make a query and then see how it should look in gsqlc.

@philon123
Copy link

philon123 commented Apr 4, 2021

Let me follow up on the original question. I found that it's possible to write Hasuras dict-like filter queries using the gsqlc syntax. See here two equivalent filter queries:

op.user(where={"height_cm": {"_gte": 200}})  # dict-syntax
op.user(where=schema.user_bool_exp(height_cm=schema.Int_comparison_exp(_gte=200)))  # gsql-syntax

I am wondering why the codegen doesn't generate the gsql-syntax but instead the dict-syntax? I am wondering which is better to use since the gsql-syntax is more type safe but the dict-syntax is easier to read/write.

@barbieri
Copy link
Member

is this still an issue? Also, I highly recommend these kind of parameters to be set via Variables, so you give it a name that is used across the operation.

The actual value of the variable you send as a plain Python "JSON-like" object, the server will handle all the needed checks to avoid errors.

@rohansharmaa
Copy link
Author

I was on a massive time crunch, so as of now I've manually defined all the queries as a string and using those. Once I find the time to experiment, I'll try out these suggestions and get back to you.
Haven't had time to try out the codegen either., but since @philon123 has tried it out and it works fine for him, it should work for me too.

@philon123
Copy link

philon123 commented Apr 25, 2021

The actual value of the variable you send as a plain Python "JSON-like" object, the server will handle all the needed checks to avoid errors.

I love the generated types of sgqlc, because it will not allow me to write a broken query. Basically - if it compiles, it should work. That's great and I tried to apply the thought to Hasuras complex arguments. Do you think it's better not to validate the json argument before sending to the server?

@hungertaker
Copy link

hungertaker commented Apr 28, 2021

[PT-BR]
@barbieri, pela localização acho que você é BR. Cara, parabéns pelo trabalho tá incrível. Veja se você pode me ajudar. A biblioteca funciona lisinha com várias operações do Hasura, limit, where, offset e diversas outras, mas não consigo de maneira nenhuma, implementar o order_by. Gerei o codegen direto da query para reproduzir de forma mais resumida. Agradeço imensamente pelo feedback.

[EN]
@barbieri, by the location I think you are BR. Dude, congratulations on the job. It is incredible! See if you can help me. The library works smoothly with several Hasura operations, limit, where, offset and several others, but I am not in any way able to implement order_by. I generated the codegen directly from the query to reproduce in a more summarized way. Thank you so much for the feedback.

import sgqlc.types
import sgqlc.operation
import hasura_schema

_schema = hasura_schema
_schema_root = _schema.hasura_schema

__all__ = ('Operations',)


def query_my_query():
    _op = sgqlc.operation.Operation(_schema_root.query_type, name='MyQuery', variables=dict(order_by=sgqlc.types.Arg(sgqlc.types.list_of(sgqlc.types.non_null(_schema.binance_assets_order_by)), default={'id': 'asc'})))
    _op_binance_assets = _op.binance_assets(offset=10, order_by=sgqlc.types.Variable('order_by'))
    _op_binance_assets.fk_info_assets_id()
    _op_binance_assets.fiat()
    return _op


class Query:
    my_query = query_my_query()


class Operations:
    query = Query

""" AssertionError: 'id' (str) is not a JSON Object """
print(query_my_query())
Traceback (most recent call last):
  File "/home/hungertaker/Projects/pysura/graphql/introspection/teste.py", line 19, in <module>
    class Query:
  File "/home/hungertaker/Projects/pysura/graphql/introspection/teste.py", line 20, in Query
    my_query = query_my_query()
  File "/home/hungertaker/Projects/pysura/graphql/introspection/teste.py", line 12, in query_my_query
    _op = sgqlc.operation.Operation(_schema_root.query_type, name='MyQuery', variables=dict(order_by=sgqlc.types.Arg(sgqlc.types.list_of(sgqlc.types.non_null(_schema.binance_assets_order_by)), default={'id': 'asc'})))
  File "/home/hungertaker/Dev/ENVS/pysura/lib/python3.8/site-packages/sgqlc/types/__init__.py", line 2169, in __init__
    typ(default)
  File "/home/hungertaker/Dev/ENVS/pysura/lib/python3.8/site-packages/sgqlc/types/__init__.py", line 973, in __new__
    return [realize_type(v, selection_list) for v in json_data]
  File "/home/hungertaker/Dev/ENVS/pysura/lib/python3.8/site-packages/sgqlc/types/__init__.py", line 973, in <listcomp>
    return [realize_type(v, selection_list) for v in json_data]
  File "/home/hungertaker/Dev/ENVS/pysura/lib/python3.8/site-packages/sgqlc/types/__init__.py", line 967, in realize_type
    return t(v, selection_list)
  File "/home/hungertaker/Dev/ENVS/pysura/lib/python3.8/site-packages/sgqlc/types/__init__.py", line 949, in __new__
    return realize_type(json_data, selection_list)
  File "/home/hungertaker/Dev/ENVS/pysura/lib/python3.8/site-packages/sgqlc/types/__init__.py", line 944, in realize_type
    return t(v, selection_list)
  File "/home/hungertaker/Dev/ENVS/pysura/lib/python3.8/site-packages/sgqlc/types/__init__.py", line 2566, in __init__
    super().__init__(_json_obj, _selection_list)
  File "/home/hungertaker/Dev/ENVS/pysura/lib/python3.8/site-packages/sgqlc/types/__init__.py", line 1711, in __init__
    assert json_data is None or isinstance(json_data, dict), \
AssertionError: 'id' (str) is not a JSON Object

Process finished with exit code 1

@barbieri
Copy link
Member

barbieri commented May 3, 2021

Hi @hungertaker , yes I'm also 🇧🇷 :-) But let's keep it in english so others may read it as well.

I need the description of binance_assets_order_by, given the traceback it should be an object (class ContainerType). Take GitHub's https://docs.github.com/en/graphql/reference/input-objects#repositoryorder it's a field (enum) and direction (also an enum)

@barbieri
Copy link
Member

barbieri commented May 3, 2021

in your example, it looks wrong. If that was generated, also send me the operation so I can see what's wrong in my codegen, but:

order_by=sgqlc.types.Arg(
  sgqlc.types.list_of(sgqlc.types.non_null(_schema.binance_assets_order_by)), # list! 
  default={'id': 'asc'} # object, not a list...
)

If the variable is declared as $orderBy=[binance_assets_order_by!], the default should be [{'id': 'asc'}] or something like that.

@hungertaker
Copy link

Worked perfectly. Thank you very much. The abstraction you made from graphql to python, in the case of Hasura, which is a mirror of Postgres, allows to consult the entire endpoint, with a few classes. Soon I share with the community here some examples and ideas.

@hungertaker
Copy link

The query, codegen and schema.binance_assets_order_by:

query:

query MyQuery($order_by: [binance_assets_order_by!] = {id: asc}) {
  binance_assets(offset: 10, order_by: $order_by) {
    fk_info_assets_id
    fiat
  }
}

codegen

import sgqlc.types
import sgqlc.operation
import hasura_schema
import json

_schema = hasura_schema
_schema_root = _schema.hasura_schema

__all__ = ('Operations',)


def query_my_query():

    _op = sgqlc.operation.Operation(_schema_root.query_type, name='MyQuery', variables=dict(
        order_by=sgqlc.types.Arg(sgqlc.types.list_of(
            sgqlc.types.non_null(_schema.binance_assets_order_by)),
            default={'id': 'asc'}))) # Correct default=[{'id': 'asc'}]

    _op_binance_assets = _op.binance_assets(offset=10, order_by=sgqlc.types.Variable('order_by'))
    _op_binance_assets.fk_info_assets_id()
    _op_binance_assets.fiat()
    return _op


class Query:
    my_query = query_my_query()


class Operations:
    query = Query

schema.binance_assets_order_by

class binance_assets_order_by(sgqlc.types.Input):
    __schema__ = hasura_schema
    __field_names__ = ('fiat', 'fk_info_assets_id', 'id', 'last_update', 'name', 'pairs_by_quote_symbol_id_aggregate', 'pairs_aggregate', 'symbol')
    fiat = sgqlc.types.Field(order_by, graphql_name='fiat')
    fk_info_assets_id = sgqlc.types.Field(order_by, graphql_name='fk_info_assets_id')
    id = sgqlc.types.Field(order_by, graphql_name='id')
    last_update = sgqlc.types.Field(order_by, graphql_name='last_update')
    name = sgqlc.types.Field(order_by, graphql_name='name')
    pairs_by_quote_symbol_id_aggregate = sgqlc.types.Field('binance_pairs_aggregate_order_by', graphql_name='pairsByQuoteSymbolId_aggregate')
    pairs_aggregate = sgqlc.types.Field('binance_pairs_aggregate_order_by', graphql_name='pairs_aggregate')
    symbol = sgqlc.types.Field(order_by, graphql_name='symbol')

@barbieri
Copy link
Member

barbieri commented May 3, 2021

yes, your query is wrong, I wonder it was passing the checks, but if you try to paste that in GraphiQL/Explorer it should fail. Could you check with the latest master? Now it's doing much more checks and I think it would point out the error.

@hungertaker
Copy link

Exactly. The query is wrong. I had tested it on Hasura's Graphi and as it passed I thought everything was ok. I generated codegen again with the [...] ones and everything worked out. I'm going to test it on the new master and I'm already giving the feeedback.

@barbieri
Copy link
Member

barbieri commented Jun 2, 2021

@hungertaker could you confirm the current master reports the incorrect query?

@barbieri
Copy link
Member

https://pypi.org/project/sgqlc/14.0/ is released with that fix, so you can use the package.

@CalebEverett
Copy link

I was trying to query a list of objects based on the property of a child object. Is it possible to query selection lists with this syntax?

op.repositories(owner='Alice').__fields__(
     issues={'title_contains': 'bug'}, # adds field and children
)

@barbieri
Copy link
Member

it should be, isn't it working? could you paste the error or at least print the operation and paste the results?

Just notice this will only query issues, ok?

@CalebEverett
Copy link

CalebEverett commented Jul 27, 2021

HI, thank you. It is saying that the key doesn't exist. I put it into a colab here. I was using the introspection and code generation (really nice - thank you) and finding the generated filter object with that key for the object I was querying, but maybe the schema isn't implemented to perform that operation?

https://colab.research.google.com/drive/1p1UjF4qPLWNPK05s1dMRA34OMEu9ouN-#scrollTo=kGECcPjsQVVk

op = Operation(schema.Query)
positions = op.positions(first=2, where={"pool": "0x8ad599c3a0ff1de082011efddc58f1908eb6e6d8"}).__fields__(
    tick_upper={"tick_idx_lte": 200000}
)
endpoint(op)
KeyError                                  Traceback (most recent call last)
<ipython-input-9-6102748212fb> in <module>()
      3     tick_upper={"tick_idx_lte": 200000}
      4 )
----> 5 endpoint(op)

7 frames
/usr/local/lib/python3.7/dist-packages/sgqlc/types/__init__.py in __to_graphql_input__(self, values, indent, indent_string)
   2391             args = []
   2392             for k, v in values.items():
-> 2393                 p = self[k]
   2394                 args.append(p.__to_graphql_input__(v))
   2395             s.extend((', '.join(args), ')'))

KeyError: 'tick_idx_lte'

I also tried this formulation:

op = Operation(schema.Query)
op.positions(
    first=2,
    where={"pool": "0x8ad599c3a0ff1de082011efddc58f1908eb6e6d8", "tick_upper": {"tick_idx_lte": 200000}}
)
endpoint(op)

and got back this error:


op = Operation(schema.Query)
op.positions(
    first=2,
    where={"pool": "0x8ad599c3a0ff1de082011efddc58f1908eb6e6d8", "tick_upper": {"tick_idx_lte": 200000}}
)
endpoint(op)
GraphQL query failed with 1 errors
{'errors': [{'locations': [{'column': 93, 'line': 2}],
   'message': 'Unexpected `"tick_idx_lte"[StringValue]`\nExpected `Name`, `:` or `}`'}]}

@barbieri
Copy link
Member

Well, I just did the introspection here and checked it, you can search for Position_filter, both in the json or the generated Python file. The tick_upper is a simple string, other similar fields are:

class Position_filter(sgqlc.types.Input):
    # ...
    tick_upper = sgqlc.types.Field(String, graphql_name='tickUpper')
    tick_upper_not = sgqlc.types.Field(String, graphql_name='tickUpper_not')
    tick_upper_gt = sgqlc.types.Field(String, graphql_name='tickUpper_gt')
    tick_upper_lt = sgqlc.types.Field(String, graphql_name='tickUpper_lt')
    tick_upper_gte = sgqlc.types.Field(String, graphql_name='tickUpper_gte')
    tick_upper_lte = sgqlc.types.Field(String, graphql_name='tickUpper_lte')
    tick_upper_in = sgqlc.types.Field(sgqlc.types.list_of(sgqlc.types.non_null(String)), graphql_name='tickUpper_in')
    tick_upper_not_in = sgqlc.types.Field(sgqlc.types.list_of(sgqlc.types.non_null(String)), graphql_name='tickUpper_not_in')
    tick_upper_contains = sgqlc.types.Field(String, graphql_name='tickUpper_contains')
    tick_upper_not_contains = sgqlc.types.Field(String, graphql_name='tickUpper_not_contains')
    tick_upper_starts_with = sgqlc.types.Field(String, graphql_name='tickUpper_starts_with')
    tick_upper_not_starts_with = sgqlc.types.Field(String, graphql_name='tickUpper_not_starts_with')
    tick_upper_ends_with = sgqlc.types.Field(String, graphql_name='tickUpper_ends_with')
    tick_upper_not_ends_with = sgqlc.types.Field(String, graphql_name='tickUpper_not_ends_with')

You can confirm in JSON:

            {
              "defaultValue": null,
              "description": null,
              "name": "tickUpper",
              "type": {
                "kind": "SCALAR",
                "name": "String",
                "ofType": null
              }
            },
            {
              "defaultValue": null,
              "description": null,
              "name": "tickUpper_not",
              "type": {
                "kind": "SCALAR",
                "name": "String",
                "ofType": null
              }
            },

The only occurrence of tickIdx_lte (tick_idx_lte) is in Tick_filter:

            {
              "defaultValue": null,
              "description": null,
              "name": "tickIdx_lte",
              "type": {
                "kind": "SCALAR",
                "name": "BigInt",
                "ofType": null
              }
            },

And Tick_filter is used in Pool.ticks, Query.ticks, Subscription.ticks... none in Positions.

@CalebEverett
Copy link

CalebEverett commented Jul 29, 2021

Thanks for taking a look and that helps for looking at the schema to see what is possible in the future. Looks like the filter I need is not implemented.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants