Skip to content

Commit

Permalink
Adding E2E SQL example
Browse files Browse the repository at this point in the history
  • Loading branch information
jmeulemans committed Jan 29, 2019
1 parent 3e629ba commit a356e3c
Showing 1 changed file with 124 additions and 0 deletions.
124 changes: 124 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ It's modeled after Python's `json.tool`, reading from stdin and writing to stdou
* [The GraphQL schema](#the-graphql-schema)
* [Execution model](#execution-model)
* [SQL Support](#sql-support)
* [Configuring SQLAlchemy](#configuring-sqlalchemy)
* [End-To-End SQL Example](#end-to-end-sql-example)
* [Configuring SQL Database to Match Schema](#configuring-sql-database-to-match-schema)
* [Miscellaneous](#miscellaneous)
* [Expanding `@optional` vertex fields](#expanding-optional-vertex-fields)
* [Optional `type_equivalence_hints` compilation parameter](#optional-type_equivalence_hints-parameter)
Expand Down Expand Up @@ -1154,6 +1157,127 @@ supported for various relational database flavors:
| SQLite | Root-only queries (no edges), [@output](#output), [@filter](#filter) | [\__typename](#__typename) output metafield, [intersects](#intersects) filter, [has_edge_degree](#has_edge_degree) filter and [name_or_alias](#name_or_alias) filter unsupported|


### Configuring SQLAlchemy
Support for relational databases is accomplished by compiling to SQLAlchemy core as an intermediate
language, and then relying on SQLAlchemy's compilation of the dialect specific SQL string to query
the target database.

For the SQL backend, GraphQL types are assumed to have a SQL table of the same name, and with the
same properties. For example, a schema type
```
type Animal {
name: String
}
```
is expected to correspond to a SQLAlchemy table object of the same name (case-insensitive) like

```python
from sqlalchemy import MetaData, Table, Column, String
# table for GraphQL type Animal
metadata = MetaData()
animal_table = Table(
'animal', # name of table matches type name from schema
metadata,
Column('name', String(length=12)), # Animal.name field has corresponding 'name' column
)
```


### End-To-End SQL Example
An end-to-end example including relevant GraphQL schema and SQLAlchemy engine preparation follows.

Note this is intended to show the setup steps for the SQL backend of the Graphql compiler, and does
not represent best practices for configuring and running SQLAlchemy in a production system. Where
possible links to relevant SQLAlchemy documentation are included.

```python
from graphql import parse
from graphql.utils.build_ast_schema import build_ast_schema
from sqlalchemy import MetaData, Table, Column, String, create_engine
from graphql_compiler.compiler.ir_lowering_sql.metadata import SqlMetadata
from graphql_compiler import compile_graphql_to_sql, insert_arguments_into_query

# Step 1: Configure a GraphQL schema (note that this can also be done programmatically)
schema_text = '''
schema {
query: RootSchemaQuery
}
# IMPORTANT NOTE: all compiler directives are expected here, but not shown to keep the example brief
directive @filter(op_name: String!, value: [String!]!) on FIELD | INLINE_FRAGMENT
directive @output(out_name: String!) on FIELD
type Animal {
name: String
}
'''
schema = build_ast_schema(parse(schema_text))

# Step 2: For all GraphQL types, bind the corresponding SQLAlchemy Table to a SQLAlchemy metadata
# instance, using the expected naming detailed above.
# See https://docs.sqlalchemy.org/en/latest/core/metadata.html for more details on this step.
metadata = MetaData()
animal_table = Table(
'animal', # name of table matches type name from schema
metadata,
# Animal.name schema field has corresponding 'name' column in animal table
Column('name', String(length=12)),
)

# Step 3: Prepare a SQLAlchemy engine to query the target relational database.
# See https://docs.sqlalchemy.org/en/latest/core/engines.html for more detail on this step.
engine = create_engine('<connection string>')

# Step 4: Wrap the SQLAlchemy metadata and dialect as a SqlMetadata GraphQL compiler object
sql_metadata = SqlMetadata(engine.dialect, metadata)

# Step 5: Prepare and compile a GraphQL query against the schema
graphql_query = '''
{
Animal {
name @output(out_name: "animal_name")
@filter(op_name: "in_collection", value:["$names"])
}
}
'''
parameters = {
'names': ['animal name 1', 'animal name 2'],
}

compilation_result = compile_graphql_to_sql(schema, graphql_query, sql_metadata)
compilation_result = insert_arguments_into_query(compilation_result, parameters)


# Step 6: Execute compiled query against a SQLAlchemy engine/connection.
# See https://docs.sqlalchemy.org/en/latest/core/connections.html for more details.
query = compilation_result.query
query_results = [dict(result_proxy) for result_proxy in engine.execute(query)]
```

### Configuring SQL Database to Match Schema
For simplicity, the SQL backend expects an exact match between SQLAlchemy Tables and GraphQL types,
and between SQLAlchemy Columns and GraphQL fields. What if the table name or column name in the
database doesn't conform to these rules? Eventually the plan is to make this aspect of the
SQL backend more configurable. In the near-term, a possible way to address this is by using
SQL view.

For example, suppose there is a table in the database called `animal_table` and it has a column
called `animal_name`. If the desired schema has type
```
type Animal {
name: String
}
```
Then this could be exposed via a view like:
```sql
CREATE VIEW animal AS
SELECT
animal_name AS name
FROM animal_table
```
At this point, the `animal` view can be used in the SQLAlchemy Table for the purposes of compiling.

## Miscellaneous

### Expanding [`@optional`](#optional) vertex fields
Expand Down

0 comments on commit a356e3c

Please sign in to comment.