Developing with the Stargate GraphQL API (CQL-first)
Stargate is a data gateway deployed between client applications and a database. The GraphQL API modifies and queries your table data using GraphQL types, queries, and mutations.
The CQL-first approach directly translates CQL tables into GraphQL types, mutations, and queries. The GraphQL schema is automatically generated from the keyspace, tables, and columns defined, but no customization is allowed. A standard set of mutations and queries are produced for searching and modifying the table data. If you are familiar with Cassandra, you might prefer this approach.
For more information about the GraphQL API, see the blog post on the GraphQL API. |
For every table in your keyspace, a series of GraphQL objects are generated, along with queries and mutations for searching and modifying the table data.
Prerequisites
If you are looking to just get started, DataStax Astra Database-as-a-Service can get you started with no install steps. |
-
Install cURL, a utility for running REST, Document, or GraphQL queries on the command line.
-
[Optional] If you prefer, you can use Postman as a client interface for exploring the APIs
-
You will also find links to downloadable collections and environments in Using Postman
-
-
[Optional] If you going to use the GraphQL API, you will want to use the GraphQL Playground to deploy schema and execute mutations and queries.
-
[Optional] For the REST and Document APIs, you can use the Swagger UI.
-
Install Docker for Desktop
-
Pull a Stargate Docker image
v2
For Stargate v2, you’ll need to pull an image for coordinator, plus an image for each API that you wish to run: restapi, graphql, and docsapi. The coordinator image contains a Apache Cassandra™ backend, the Cassandra Query Language (CQL), and the gRPC API.
The following are the commands for each of those images using the tag v2
:
docker pull stargateio/coordinator-4_0:v2
docker pull stargateio/restapi:v2
docker pull stargateio/docsapi:v2
docker pull stargateio/graphqlapi:v2
v1
This image contains the Cassandra Query Language (CQL), REST, Document, GraphQL APIs, and GraphQL Playground, along with an Apache Cassandra™ 4.0 backend.
docker pull stargateio/stargate-4_0:v1.0.57
v2
For Stargate v2, you’ll need to pull an image for coordinator, plus an image for each API that you wish to run: restapi, graphql, and docsapi. The coordinator image contains a Apache Cassandra™ backend, the Cassandra Query Language (CQL), and the gRPC API.
The following are the commands for each of those images using the tag v2
:
docker pull stargateio/coordinator-3_11:v2
docker pull stargateio/restapi:v2
docker pull stargateio/docsapi:v2
docker pull stargateio/graphqlapi:v2
v1
This image contains the Cassandra Query Language (CQL), REST, Document, GraphQL APIs, and GraphQL Playground, along with an Apache Cassandra™ 3.11 backend.
docker pull stargateio/stargate-3_11:v1.0.57
v2
For Stargate v2, you’ll need to pull an image for coordinator, plus an image for each API that you wish to run: restapi, graphql, and docsapi. The coordinator image contains a Apache Cassandra™ backend, the Cassandra Query Language (CQL), and the gRPC API.
The following are the commands for each of those images using the tag v2
:
docker pull stargateio/coordinator-68:v2
docker pull stargateio/restapi:v2
docker pull stargateio/docsapi:v2
docker pull stargateio/graphqlapi:v2
v1
This image contains the Cassandra Query Language (CQL), REST, Document, GraphQL APIs, and GraphQL Playground, along with a DataStax Enterprise™ 6.8 backend.
docker pull stargateio/stargate-dse-68:v1.0.57
-
Run the Stargate Docker image
v2
Use this docker-compose shell script to start the coordinator and APIs in developer mode.
The easiest way to do that is to navigate to the <install_location>/stargate/docker-compose
directory, and run the script.
You will want to run, for example:
./start_cass_4_0_dev_mode.sh
This command will start using the latest available coordinator and API images with the v2
tag.
You may also select a specific image tag using the -t <image_tag>
option. A list of the available tags for the coordinator can be found here.
v1
Start the Stargate container in developer mode. Developer mode removes the need to set up a separate Cassandra instance and is meant for development and testing only.
docker run --name stargate \
-p 8080:8080 \
-p 8081:8081 \
-p 8082:8082 \
-p 127.0.0.1:9042:9042 \
-d \
-e CLUSTER_NAME=stargate \
-e CLUSTER_VERSION=4.0 \
-e DEVELOPER_MODE=true \
stargateio/stargate-4_0:v1.0.57
v2
Use this docker-compose shell script to start the coordinator and APIs in developer mode.
The easiest way to do that is to navigate to the <install_location>/stargate/docker-compose
directory, and run the script.
You will want to run, for example:
./start_cass_3_11_dev_mode.sh
This command will start using the latest available coordinator and API images with the v2
tag.
You may also select a specific image tag using the -t <image_tag>
option. A list of the available tags for the coordinator can be found here.
v1
Start the Stargate container in developer mode. Developer mode removes the need to set up a separate Cassandra instance and is meant for development and testing only.
docker run --name stargate \
-p 8080:8080 \
-p 8081:8081 \
-p 8082:8082 \
-p 127.0.0.1:9042:9042 \
-d \
-e CLUSTER_NAME=stargate \
-e CLUSTER_VERSION=3.11 \
-e DEVELOPER_MODE=true \
stargateio/stargate-3_11:v1.0.57
v2
Use this docker-compose shell script to start the coordinator and APIs in developer mode.
The easiest way to do that is to navigate to the <install_location>/stargate/docker-compose
directory, and run the script.
You will want to run, for example:
./start_dse_68_dev_mode.sh
This command will start using the latest available coordinator and API images with the v2
tag.
You may also select a specific image tag using the -t <image_tag>
option. A list of the available tags for the coordinator can be found here.
v1
Start the Stargate container in developer mode. Developer mode removes the need to set up a separate DSE instance and is meant for development and testing only.
docker run --name stargate \
-p 8080:8080 \
-p 8081:8081 \
-p 8082:8082 \
-p 127.0.0.1:9042:9042 \
-d \
-e CLUSTER_NAME=stargate \
-e CLUSTER_VERSION=6.8 \
-e DEVELOPER_MODE=true \
stargateio/stargate-dse-68:v1.0.57
-
Generate an authorization token to access the interface by following the instructions in Table-based authentication/Authorization
API reference
If you prefer to learn using a QuickStart, try out the Stargate GraphQL CQL-first QuickStart.
About the GraphQL API endpoints
There are two Stargate GraphQL API endpoints, one for creating schema and the other for querying or mutating a keyspace. The URLS are:
The schema endpoint is used to create or alter CQL schema in GraphQL cql-first using direct schema manipulation. The querying and mutating endpoint is constructed using a particular keyspace name.
Each request must have a valid application token. Each request can also have an optional unique request id. The request id is recommended in a production environment and can be useful in troubleshooting issues.
Generating UUIDs Consider using a tool like this online UUID generator to quickly create a random UUID to pass with your requests if you are submitting the queries manually using a tool like cURL. |
Naming conventions for GraphQL
The GraphQL API uses specific naming conventions to preserve capitalization and
special characters. Note that if typical GraphQL naming conventions are used,
such as camelCase
, that the underlying Cassandra storage tables will use double
quoting to preserve the capitalization. If a naming conflict occurs, an error
results that the table already exists.
GraphQL table name | CQL table name | GraphQL mutation format |
---|---|---|
foo |
foo |
insertfoo |
Foo |
"Foo" |
insertFoo |
foo_bar |
foo_bar |
insertfoo_bar |
FooBar |
"FooBar" |
insertFooBar |
Hellox21_ |
"Hello!" |
insertHellox21_ |
Mapping Stargate tables to GraphQL fields and types in CQL-first
The Stargate GraphQL API generates fields and types for each table in your database.
For example, for an Stargate table named book
the following fields and types
are generated.
schema {
query: Query
mutation: Mutation
}
type Query {
book(value: bookInput, filter: bookFilterInput, orderBy: [bookOrder], options: QueryOptions): bookResult
bookFilter(filter: bookFilterInput!, orderBy: [bookOrder], options: QueryOptions): bookResult
}
type Mutation {
insertbook(value: bookInput!, ifNotExists: Boolean, options: UpdateOptions): bookMutationResult
updatebook(value: bookInput!, ifExists: Boolean, ifCondition: bookFilterInput, options: UpdateOptions): bookMutationResult
deletebook(value: bookInput!, ifExists: Boolean, ifCondition: bookFilterInput, options: UpdateOptions): bookMutationResult
}
Generated query types
The following query types are generated:
-
book()
: Query book values by equality. If no value argument is provided, then the first hundred (default pagesize) values are returned. -
bookFilter
: Query book values by filtering the result with additional operators. For examplegt
(greater than),lt
(less than),in
(in a list of values). Thebook()
equality style query is preferable if your queries don’t require non-equality operators.
Generated mutation types
The following mutations are generated:
Several mutations are created that you can use to insert, update, or delete books. Some important facts about these mutations are:
-
insertbook()
is an upsert operation if a book with the same information exist, unless theifNotExists
is set to true. -
updatebook()
is also an upsert operation, and will create a new book if it doesn’t exist, unlessifNotExists
is set to true. -
deletebook()
will delete a book. -
Using the
ifNotExists
orifCondition
options affects the performance of operations because of the compare-and-set execution path in Cassandra. Under the hood these operations are using a feature in Cassandra called lightweight transactions (LWTs).
As more tables are added to your keyspace, additional fields are added to the query and mutation types to handle queries and mutations for the new tables.
Using the GraphQL Playground
The easiest way to get started with GraphQL is to use the built-in GraphQL playground.
In Stargate, go to your browser and launch the url:
http://localhost:8080/playground
Add your application token to the HTTP HEADERS section at the lower lefthand corner of the GraphQL Playground window:
{"x-cassandra-token":"$AUTH_TOKEN"}
Once in the playground, you can create new schema and interact with the GraphQL APIs. The server paths are structured to provide access to creating and querying schema, as well as querying and modifying Cassandra data:
-
/graphql-schema
-
An API for exploring and creating schema, or Data Definition Language (DDL). For example, Stargate has queries to create, modify, or drop tables, such as
createTable
, ordropTable
.
-
-
/graphql/<keyspace>
-
An API for querying and modifying your tables using GraphQL fields. Generally, you will start the playground with
/graphql-schema
to create some schema.
-
Create or delete schema
In order to use the GraphQL API, you must create schema that defines the keyspace and
tables that will store the data. A keyspace is a container for which a
replication factor
defines the number of data replicas the database will store.
Tables consist of columns that have a defined data type. Multiple tables are contained
in a keyspace, but a table cannot be contained in multiple keyspaces.
Create a keyspace
Before you can start using the GraphQL API, you must first create a Cassandra keyspace and at least one table in your database. If you are connecting to a Cassandra database with existing schema, you can skip this step.
Inside the GraphQL playground, navigate to http://localhost:8080/graphql-schema and create a keyspace by executing the following mutation:
# create a keyspace called library
mutation createKsLibrary {
createKeyspace(name:"library", replicas: 1)
}
For each keyspace created in your Cassandra schema, a new path is created under
the graphql-path
root (default is: /graphql
). For example, the mutation just
executed creates a path /graphql/library
for the library
keyspace when
Cassandra creates the keyspace.
Add the auth token to the HTTP Headers box in the lower lefthand corner:
{
"X-Cassandra-Token":"bff43799-4682-4375-99e8-23c8a9d0f304"
}
Notice that the key for this JSON token is different than the value that the
generate token has. It is |
Now run the mutation to create the keyspace. You should see a return value of:
{
"data": {
"createKeyspace": true
}
}
Check keyspace existence
To check if a keyspace exists, execute a GraphQL query:
# Works in graphql-schema
# for either CQL-first or schema-first
query GetKeyspace {
keyspace(name: "library") {
name
dcs {
name
replicas
}
tables {
name
columns {
name
kind
type {
basic
info {
name
}
}
}
}
}
}
{
"data": {
"keyspace": {
"name": "library",
"dcs": [],
"tables": [
{
"name": "book",
"columns": [
{
"name": "title",
"kind": "PARTITION",
"type": {
"basic": "VARCHAR",
"info": null
}
},
{
"name": "author",
"kind": "CLUSTERING",
"type": {
"basic": "VARCHAR",
"info": null
}
},
{
"name": "format",
"kind": "REGULAR",
"type": {
"basic": "SET",
"info": {
"name": null
}
}
},
{
"name": "genre",
"kind": "REGULAR",
"type": {
"basic": "SET",
"info": {
"name": null
}
}
},
{
"name": "isbn",
"kind": "REGULAR",
"type": {
"basic": "VARCHAR",
"info": null
}
},
{
"name": "language",
"kind": "REGULAR",
"type": {
"basic": "VARCHAR",
"info": null
}
},
{
"name": "pub_year",
"kind": "REGULAR",
"type": {
"basic": "INT",
"info": null
}
}
]
},
{
"name": "reader",
"columns": [
{
"name": "name",
"kind": "PARTITION",
"type": {
"basic": "VARCHAR",
"info": null
}
},
{
"name": "user_id",
"kind": "CLUSTERING",
"type": {
"basic": "UUID",
"info": null
}
},
{
"name": "addresses",
"kind": "REGULAR",
"type": {
"basic": "LIST",
"info": {
"name": null
}
}
},
{
"name": "birthdate",
"kind": "REGULAR",
"type": {
"basic": "DATE",
"info": null
}
},
{
"name": "email",
"kind": "REGULAR",
"type": {
"basic": "SET",
"info": {
"name": null
}
}
},
{
"name": "reviews",
"kind": "REGULAR",
"type": {
"basic": "TUPLE",
"info": {
"name": null
}
}
}
]
}
}
}
}
Delete a keyspace
You can delete a keyspace. All tables and table data will be deleted along with the keyspace schema.
mutation dropKsLibrary {
dropKeyspace(name:"library", ifExists: true)
}
A note about what schema is
A full GraphQL table schema can include user-defined types (UDTs) and indexes. Queries and mutations are automatically generated based on the schema. The next sections describes the definition of these items.
Data types
A column’s CQL data type is inferred from the GraphQL table column type. GraphQL’s built-in scalar types are mapped:
GraphQL | CQL |
---|---|
ID |
uuid |
String |
varchar |
Int |
int |
Float |
double |
Boolean |
boolean |
In addition, Stargate provides a set of custom scalar types that map directly to the CQL types of the same name: Uuid, TimeUuid, Inet, Date, Duration, BigInt, Counter, Ascii, Decimal, Varint, Float32, Blob, SmallInt, TinyInt, Timestamp, Time, Set, List, Map, Tuple.
Lastly, user-defined types (UDTs) are supported.
Create a table
After the keyspace exists, you can create a table by executing mutations. For this example, two tables are created:
# create two tables (book, reader) in library with a single mutation
# DATA TYPES: TEXT, UUID, SET(TEXT), TUPLE(TEXT, INT, DATE), LIST(UDT)
mutation createTables {
book: createTable(
keyspaceName:"library",
tableName:"book",
partitionKeys: [ # The keys required to access your data
{ name: "title", type: {basic: TEXT} }
]
clusteringKeys: [
{ name: "author", type: {basic: TEXT} }
]
)
reader: createTable(
keyspaceName:"library",
tableName:"reader",
partitionKeys: [
{ name: "name", type: {basic: TEXT} }
]
clusteringKeys: [ # Secondary key used to access values within the partition
{ name: "user_id", type: {basic: UUID}, order: "ASC" }
]
values: [
{ name: "birthdate", type: {basic: DATE} }
{ name: "email", type: {basic: SET, info:{ subTypes: [ { basic: TEXT } ] } } }
{ name: "reviews", type: {basic: TUPLE, info: { subTypes: [ { basic: TEXT }, { basic: INT }, { basic: DATE } ] } } }
{ name: "addresses", type: { basic: LIST, info: { subTypes: [ { basic: UDT, info: { name: "address_type", frozen: true } } ] } } }
]
)
}
"data": {
"book": true,
"reader": true
}
}
It is worth noting that one mutation is used to create two tables. Information about partition keys and clustering keys can be found in the CQL reference.
The second table, reader
, also defines a column using a
user-defined type (UDT).
IF NOT EXISTS option
A table can be created with an option ifNotExists
that will only create the
table if it does not already exist:
# create two tables, magazine and article, IF THEY DON'T EXIST
# DATA TYPES: TEXT, INT, LIST(TEXT)
mutation createTableIfNotExists {
magazine: createTable(
keyspaceName:"library",
tableName:"magazine",
partitionKeys: [ # The keys required to access your data
{ name: "title", type: {basic: TEXT} }
]
clusteringKeys: [ # Secondary key used to access values within the partition
{ name: "pub_yr", type: {basic: INT}, order: "ASC" }
{ name: "pub_mon", type: {basic: INT} }
{ name: "mag_id", type: {basic: INT} }
],
ifNotExists: true,
values: [ # The values associated with the keys
{ name: "editor", type: {basic: TEXT} }
]
)
article: createTable(
keyspaceName:"library",
tableName:"article",
partitionKeys: [ # The keys required to access your data
{ name: "title", type: {basic: TEXT} }
]
clusteringKeys: [ # Secondary key used to access values within the partition
{ name: "mtitle", type: {basic: TEXT} }
],
ifNotExists: true,
values: [ # The values associated with the keys
{ name: "authors", type: {basic:LIST, info:{ subTypes: [ { basic: TEXT } ] } } }
]
)
}
{
"data": {
"magazine": true,
"article": true
}
}
One of these tables includes creating a column with the data type LIST
, an
ordered collection of text values.
One of these tables includes creating a column with the data type LIST
, an
ordered collection of text values.
Collection (set, list, map) columns
Including a collection in a table has a couple of extra parts:
# create a table with a MAP
# DATA TYPE: TEXT, INT, MAP(TEXT, DATE)
# Sample: btype=Editor, badge_id=1, earned = [Gold:010120, Silver:020221]
mutation createMapTable {
badge: createTable (
keyspaceName:"library",
tableName: "badge",
partitionKeys: [
{name: "btype", type: {basic:TEXT}}
]
clusteringKeys: [
{ name: "badge_id", type: { basic: INT} }
],
ifNotExists:true,
values: [
{name: "earned", type:{basic:LIST { basic:MAP, info:{ subTypes: [ { basic: TEXT }, {basic: DATE}]}}}}
]
)
}
{
"data": {
"badge": true
}
}
This example shows a map. A previous example shows a list. In the next example, a set will be defined.
Add columns to table schema
If you need to add more attributes to something you are storing in a table, you can add one or more columns:
# alter a table and add columns
# DATA TYPES: TEXT, INT, SET(TEXT)
mutation alterTableAddCols {
alterTableAdd(
keyspaceName:"library",
tableName:"book",
toAdd:[
{ name: "isbn", type: { basic: TEXT } }
{ name: "language", type: {basic: TEXT} }
{ name: "pub_year", type: {basic: INT} }
{ name: "genre", type: {basic:SET, info:{ subTypes: [ { basic: TEXT } ] } } }
{ name: "format", type: {basic:SET, info:{ subTypes: [ { basic: TEXT } ] } } }
]
)
}
{
"data": {
"alterTableAdd": true
}
}
Check table and column existence
To check if a table or particular table columns exist, execute a GraphQL query:
query GetTables {
keyspace(name: "library") {
name
tables {
name
columns {
name
kind
type {
basic
info {
name
}
}
}
}
}
}
{
"data": {
"keyspace": {
"name": "library",
"tables": [
{
"name": "reader",
"columns": [
{
"name": "name",
"kind": "PARTITION",
"type": {
"basic": "VARCHAR",
"info": null
}
},
]
},
{
"name": "book",
"columns": [
{
"name": "title",
"kind": "PARTITION",
"type": {
"basic": "VARCHAR",
"info": null
}
},
{
"name": "author",
"kind": "REGULAR",
"type": {
"basic": "VARCHAR",
"info": null
}
},
{
"name": "isbn",
"kind": "REGULAR",
"type": {
"basic": "VARCHAR",
"info": null
}
}
]
}
]
}
}
}
Because these queries are named, the GraphQL playground will allow you to select
which query to run. The first query will return information about the keyspace
library
and the tables within it. The second query will return just information
about the tables in that keyspace.
Delete columns from table schema
If you find an attribute is no longer required in a table, you can remove a column. All column data will be deleted along with the column schema.
Delete a table
You can delete a table. All data will be deleted along with the table schema.
# drop a table
mutation dropTableBook {
dropTable(keyspaceName:"library",
tableName:"article")
}
{
"data": {
"dropTable": true
}
}
IF EXISTS option
You can delete a table after checking that it exists with the ifExists
option.
All data will be deleted along with the table schema.
Create an index
Cassandra supports indexing any regular, non-primary key columns in a table. Any column designated as a partition key or clustering column cannot be indexed, unless DataStax Enterprise is the defined database.
If you wish to create a table query that uses anything other than the partition key to define which row or rows are to be retrieved, a column index must be created on each column in order to read the data.
Currently, those indexes can be created with CQL or GraphQL.
Use the application token you generated to create schema in your keyspace using the GraphQL playground.
You can create an index using a mutation in /graphql-schema
.
In the following example, three indexes are created for the tables book
and reader
.
The table columns for these indexes are created are author
, birthdate
, and email
.
An index name can be defined, such as author_idx
in this example.
An additional option, indexType
can be defined to use SAI indexes if desired.
mutation createIndexes {
book: createIndex(
keyspaceName:"library",
tableName:"book",
columnName:"author",
indexName:"author_idx"
)
reader: createIndex(
keyspaceName:"library",
tableName:"reader",
columnName:"birthdate",
indexName:"reader_bdate_idx"
)
reader2: createIndex(
keyspaceName:"library",
tableName:"reader",
columnName:"email",
indexName:"reader_email_idx"
)
}
curl --location --request POST http://localhost:8080/graphql-schema \
--header "X-Cassandra-Token: $AUTH_TOKEN" \
--header 'Content-Type: application/json' \
--data-raw '{"query":"mutation createIndexes {\n book: createIndex(\n keyspaceName:\"library\",\n tableName:\"book\",\n columnName:\"author\",\n indexName:\"author_idx\"\n )\n reader: createIndex(\n keyspaceName:\"library\",\n tableName:\"reader\",\n columnName:\"birthdate\",\n indexName:\"reader_bdate_idx\"\n )\n reader2: createIndex(\n keyspaceName:\"library\",\n tableName:\"reader\",\n columnName:\"email\",\n indexName:\"reader_email_idx\"\n )\n}","variables":{}}'
Result TBD
Here is an additional example, which creates indexes that could be used in the REST API examples:
curl --location --request POST 'http://localhost:8082/api/graphql-schema' \
--header "X-Cassandra-Token: $AUTH_TOKEN" \
--header 'Content-Type: application/json' \
--data-raw '{"query":"mutation createIndexes {\n user1: createIndex(\n keyspaceName:\"'$KEYSPACE_NAME'\",\n tableName:\"'$TABLE_NAME'\",\n columnName:\"favorite_books\",\n indexName:\"fav_books_idx\",\n indexKind: VALUES\n )\n user2:createIndex(\n keyspaceName:\"'$KEYSPACE_NAME'\",\n tableName:\"'$TABLE_NAME'\",\n columnName:\"top_three_tv_shows\",\n indexName:\"tv_idx\",\n indexKind: VALUES\n )\n user3:createIndex(\n keyspaceName:\"'$KEYSPACE_NAME'\",\n tableName:\"'$TABLE_NAME'\",\n columnName:\"evaluations\",\n indexName:\"evalv_idx\",\n indexKind: VALUES\n )\n user4:createIndex(\n keyspaceName:\"'$KEYSPACE_NAME'\",\n tableName:\"'$TABLE_NAME'\",\n columnName:\"evaluations\",\n indexName:\"evalk_idx\",\n indexKind: KEYS\n )\n user5:createIndex(\n keyspaceName:\"'$KEYSPACE_NAME'\",\n tableName:\"'$TABLE_NAME'\",\n columnName:\"evaluations\",\n indexName:\"evale_idx\",\n indexKind: ENTRIES\n )\n users6: createIndex(\n keyspaceName:\"'$KEYSPACE_NAME'\",\n tableName:\"'$TABLE_NAME'\",\n columnName:\"current_country\",\n indexName:\"country_idx\"\n )\n}","variables":{}}'
CREATE INDEX books_idx ON users_keyspace.users (VALUES(favorite_books));
CREATE INDEX tv_idx ON users_keyspace.users (VALUES (top_three_tv_shows));
CREATE INDEX evalk_idx ON users_keyspace.users (KEYS (evaluations));
CREATE INDEX evalv_idx ON users_keyspace.users (VALUES (evaluations));
CREATE INDEX evale_idx ON users_keyspace.users (ENTRIES (evaluations));
CREATE INDEX country_idx ON users_keyspace.users (VALUES (current_country));
Result TBD
The CQL commands for creating these indexes is included here for reference.
The cqlsh
tool can be used to create the indexes if desired.
Delete an index
If you find an index is no longer required on a table column, or you need to change the index, you can delete it. All index data will be deleted along with the index schema.
mutation dropIndexBdate {
reader: dropIndex(
keyspaceName:"library",
indexName:"reader_bdate_idx"
)
}
curl --location --request POST http://localhost:8080/graphql-schema \
--header "X-Cassandra-Token: $AUTH_TOKEN" \
--header 'Content-Type: application/json' \
--data-raw '{"query":"mutation dropIndexBdate {\n\n reader: dropIndex(\n keyspaceName:\"library\",\n indexName:\"reader_bdate_idx\"\n )\n}","variables":{}}'
{
"data": {
"reader": true
}
}
Create a user-defined type (UDT)
Optional user-defined types (UDTs) can be created and used in table definitions.
This example creates a UDT called address_type
that includes a street, city,
state, and zipcode.
If you plan to use UDTs as a data type for columns in your table, create the UDT
first.
# create a user-defined type (UDT)
mutation createAddressUDT {
createType(
keyspaceName: "library"
typeName: "address_type"
fields: [
{ name: "street", type: { basic: TEXT } }
{ name: "city", type: { basic: TEXT } }
{ name: "state", type: { basic: TEXT } }
{ name: "zip", type: { basic: TEXT } }
]
)
}
{
"data": {
"createType": true
}
}
Delete a user-defined type (UDT)
You can delete a UDT. All tables that use the UDT must first be deleted.
# drop a UDT
mutation dropType {
dropType(keyspaceName:"library", typeName:"address_type", ifExists:true)
}
Interact with data stored in tables
API generation
Once schema is created, the GraphQL API generates mutations and queries can be used. In the GraphQL playground, expand the tabs on the righthand side labelled "DOCS" or "SCHEMA", to discover the items available and the syntax to use.
For each table in the Cassandra schema that we just created, several GraphQL
fields are created for
handling queries and mutations. For example, the GraphQL API generated for the
books
table is:
schema {
query: Query
mutation: Mutation
}
type Query {
book(value: bookInput, filter: bookFilterInput, orderBy: [bookOrder], options: QueryOptions): bookResult
bookFilter(filter: bookFilterInput!, orderBy: [bookOrder], options: QueryOptions): bookResult
}
type Mutation {
insertbook(value: bookInput!, ifNotExists: Boolean, options: UpdateOptions): bookMutationResult
updatebook(value: bookInput!, ifExists: Boolean, ifCondition: bookFilterInput, options: UpdateOptions): bookMutationResult
deletebook(value: bookInput!, ifExists: Boolean, ifCondition: bookFilterInput, options: UpdateOptions): bookMutationResult
}
The query books()
can query book values by equality. If no value argument is
provided, then the first hundred (default pagesize) values are returned.
Several mutations are created that you can use to insert, update, or delete books. Some important facts about these mutations are:
-
insertBooks()
is an upsert operation if a book with the same information exist, unless theifNotExists
is set to true. -
updateBooks()
is also an upsert operation, and will create a new book if it doesn’t exist, unlessifNotExists
is set to true. -
Using the
ifNotExists
orifCondition
options affects the performance of operations because of the compare-and-set execution path in Cassandra. Under the hood these operations are using a feature in Cassandra called lightweight transactions (LWTs).
As more tables are added to a keyspace, additional GraphQL fields will add query and mutation types that can be used to interact with the table data.
Insert data
Any of the created APIs can be used to interact with the GraphQL data, to write or read data.
First, let’s navigate to your new keyspace library
inside the playground.
Change the location to
http://localhost:8080/graphql/library
and add a couple of books to the book
table:
# insert 2 books in one mutation
mutation insert2Books {
moby: insertbook(value: {title:"Moby Dick", author:"Herman Melville"}) {
value {
title
}
}
catch22: insertbook(value: {title:"Catch-22", author:"Joseph Heller"}) {
value {
title
}
}
}
{
"data": {
"moby": {
"value": {
"title": "Moby Dick"
}
},
"catch22": {
"value": {
"title": "Catch-22"
}
}
}
}
Note that the keyword value
is used twice in the mutation.
The first use defines the value that the record is set to, for instance, the title
to Moby Dick and the author to Herman Melville.
The second use defines the values that will be displayed after the success
of the mutation, so that proper insertion can be verified.
This same method is valid for updates and read queries.
Insertion options
Three insertion options are configurable during data insertion or updating:
An example insertion that sets the consistency level and TTL:
# insert a book and set the option for consistency level
mutation insertBookWithOption {
nativeson: insertbook(value: {title:"Native Son", author:"Richard Wright"}, options: {consistency: LOCAL_QUORUM, ttl:86400}) {
value {
title
}
}
}
{
"data": {
"moby": {
"value": {
"title": "Moby Dick"
}
}
}
}
The serial consistency can also be set with serialConsistency
in the options,
if needed.
Insert collections (set, list, map)
Inserting a collection is simple. An example of inserting a list:
# insert an article USING A LIST (authors)
mutation insertArticle {
magarticle: insertarticle(value: {title:"How to use GraphQL", authors: ["First author", "Second author"], mtitle:"Database Magazine"}) {
value {
title
mtitle
authors
}
}
}
{
"data": {
"magarticle": {
"value": {
"title": "How to use GraphQL",
"mtitle": "Database Magazine",
"authors": [
"First author",
"Second author"
]
}
}
}
}
A map is slightly more complex:
mutation insertOneBadge {
gold: insertBadges(value: { btype:"Gold", earned: "2020-11-20", category: ["Editor", "Writer"] } ) {
value {
btype
earned
category
}
}
}
{
"data": {
"gold": {
"value": {
"badge_type": "Gold",
"badge_id": 100,
"earned": [
{
"key": "Writer",
"value": "2020-11-20"
}
]
}
}
}
}
Insert a tuple
Inserting a tuple involves inserting an object; note the use of item0
, item`1
,
and so on, to insert the parts of the tuple
# insert a reader record that uses a TUPLE
mutation insertJaneWithTuple{
jane: insertreader(
value: {
user_id: "b5b5666b-2a37-4d0b-a5eb-053e54fc242b"
name: "Jane Doe"
birthdate: "2000-01-01"
email: ["janedoe@gmail.com", "janedoe@yahoo.com"]
reviews: { item0: "Moby Dick", item1: 5, item2: "2020-12-01" }
}
) {
value {
user_id
name
birthdate
reviews {
item0
item1
item2
}
}
}
}
{
"data": {
"jane": {
"value": {
"user_id": "b5b5666b-2a37-4d0b-a5eb-053e54fc242b",
"name": "Jane Doe",
"birthdate": "2000-01-01",
"reviews": {
"item0": "Moby Dick",
"item1": 5,
"item2": "2020-12-01"
}
}
}
}
}
Insert a user-defined type (UDT)
Inserting a UDT requires taking careful note of the brackets used:
# insert a reader record that uses a UDT
mutation insertReaderWithUDT{
ag: insertreader(
value: {
user_id: "e0ed81c3-0826-473e-be05-7de4b4592f64"
name: "Allen Ginsberg"
birthdate: "1926-06-03"
addresses: [{ street: "Haight St", city: "San Francisco", zip: "94016" }]
}
) {
value {
user_id
name
birthdate
addresses {
street
city
zip
}
}
}
}
{
"data": {
"ag": {
"value": {
"user_id": "e0ed81c3-0826-473e-be05-7de4b4592f64",
"name": "Allen Ginsberg",
"birthdate": "1926-06-03",
"addresses": [
{
"street": "Haight St",
"city": "San Francisco",
"zip": "94016"
}
]
}
}
}
}
Retrieve data
Let’s check that the data was inserted.
Now let’s search for a particular record using a WHERE
clause. The primary
key of the table can be used in the WHERE
clause, but non-primary key columns
cannot be used unless indexed.
The following query, looking at the location
http://localhost:8080/graphql/library
will get both the title
and the author
for the specified book WHERE
title:"Moby Dick"
:
# get one book using the primary key title with a value
query oneBook {
book (value: {title:"Moby Dick"}) {
values {
title
author
}
}
}
{
"data": {
"books": {
"values": [
{
"title": "Moby Dick",
"author": "Herman Melville"
}
]
}
}
}
To find multiple books, an addition to the WHERE
clause is required, to denote that
the list of titles desired is IN
a group:
# get 3 books using the primary keys with an "in" filter clause of the primary key title
query ThreeBooks {
book(filter: { title: { in: ["Native Son", "Moby Dick", "Catch-22"] } } ) {
values {
title
author
}
}
}
{
"data": {
"book": {
"values": [
{
"title": "Catch-22",
"author": "Joseph Heller"
},
{
"title": "Moby Dick",
"author": "Herman Melville"
}
]
}
}
}
To display the contents of a UDT, notice the inclusion of addresses
in the values displayed for this read query:
# query the author to see the UDT
query getReaderWithUDT{
reader(value: { name:"Allen Ginsberg" user_id: "e0ed81c3-0826-473e-be05-7de4b4592f64" }) {
values {
name
birthdate
addresses {
street
city
zip
}
}
}
}
{
"data": {
"reader": {
"values": [
{
"name": "Allen Ginsberg",
"birthdate": "1926-06-03",
"addresses": [
{
"street": "Haight St",
"city": "San Francisco",
"zip": "94016"
}
]
}
]
}
}
}
To display the contents of a map collection, notice the inclusion of earned
in the values displayed for this read query:
# query a badge record that has a MAP (earned) with only the partition key
query oneGoldBadge {
badge(value: { badge_type: "Gold" } ) {
values {
badge_type
badge_id
earned {
key
value
}
}
}
}
{
"data": {
"badge": {
"values": [
{
"badge_type": "Gold",
"badge_id": 100,
"earned": [
{
"key": "Writer",
"value": "2020-11-20"
}
]
}
]
}
}
}
Filter options for reading
It’s possible to customize the condition of each parameter with WHERE
with the following arguments:
-
column: the GraphQL column name to which the condition applies
-
predicate: the conditional predicate to use
The filters available are:
Predicate |
columns that can have condition applied |
eq (equal) |
partition key, clustering column, regular indexed column |
notEq (not equal) |
partition key, clustering column, regular indexed column; allowed in conditional updates, but not selects |
in (within) |
partition key, clustering column, regular indexed column |
gt (greater than) |
clustering column |
gte (greater than or equal to) |
clustering column |
lt (less than) |
clustering column |
lte (less than or equal to) |
clustering column |
contains |
regular indexed column that is a , set or list, and has an index target of VALUES |
containsKey |
map contains the specified key |
containsEntry |
map contains the specified key:value pair |
Note that these can only be used with primary key columns, just like in Cassandra, unless indexing is created.
The next examples will query the same table, badge
, using a variety of filters to illustrate
the versatility of such filters.
The first example finds the record that has the partition key badge_type
equal to Gold
, and
the badge_id
equal to 100
:
# query a badge record that has a MAP (earned) with the partition key and the clustering key
query oneGold100Badge {
badge(filter: { badge_type: {eq:"Gold"} badge_id: {eq:100}} ) {
values {
badge_type
badge_id
earned {
key
value
}
}
}
}
{
"data": {
"badge": {
"values": [
{
"badge_type": "Gold",
"badge_id": 100,
"earned": [
{
"key": "Writer",
"value": "2020-11-20"
}
]
}
]
}
}
}
Now if we use a different operator gt
with the same query, notice that the query will fail,
because no badge_id
greater than a value of 100 is found:
# query a badge record that has a MAP (earned) with the partition key and the clustering key
# filter badge_id: {gt:100 will fail}
query oneGold100BadgeFail {
badge(filter: { badge_type: {eq:"Gold"} badge_id: {gt:100}} ) {
values {
badge_type
badge_id
earned {
key
value
}
}
}
}
{
"data": {
"badge": {
"values": []
}
}
}
In order to use filters for any columns that are not part of the primary key, currently you need to use CQL to create a secondary index using the CQL shell. The next three examples show the CQL creation of an index in order to query a column that is a map collection.
In this example, an index is created on the keys of the map earned
, so the containsKey
filter can be used to query in GraphQL.
# query a badge record that has a MAP (earned) with the partition key, clustering key, and a MAP key
# Requires: CREATE INDEX badge_idx ON library.badge(KEYS(earned));
query oneWriterBadge {
badge(filter: { badge_type: {eq:"Gold"} badge_id: {eq:100} earned: { containsKey: "Writer"} } ) {
values {
badge_type
badge_id
earned {
key
value
}
}
}
}
{
"data": {
"badge": {
"values": [
{
"badge_type": "Gold",
"badge_id": 100,
"earned": [
{
"key": "Writer",
"value": "2020-11-20"
}
]
}
]
}
}
}
Because the index now exists, it is also possible to just filter based on the map key itself:
# query a badge record that has a MAP (earned) with only a MAP key
# CREATE INDEX badge_idx ON library.badge(KEYS(earned));
query oneWriterKeyBadge {
badge(filter: { earned: { containsKey: "Writer"} } ) {
values {
badge_type
badge_id
earned {
key
value
}
}
}
}
{
"data": {
"badge": {
"values": [
{
"badge_type": "Gold",
"badge_id": 100,
"earned": [
{
"key": "Writer",
"value": "2020-11-20"
}
]
}
]
}
}
}
In this next example, an index is created on the values of the map earned
, so the contains
filter can be used to query in GraphQL.
# query a badge record that has a MAP (earned) with only a MAP value
# Requires: CREATE INDEX badge2_idx ON library.badge(VALUES(earned));
query oneWriterValueBadge {
badge(filter: { earned: { contains: "2020-11-20"} } ) {
values {
badge_type
badge_id
earned {
key
value
}
}
}
}
{
"data": {
"badge": {
"values": [
{
"badge_type": "Gold",
"badge_id": 100,
"earned": [
{
"key": "Writer",
"value": "2020-11-20"
}
]
}
]
}
}
}
To make a complete set of filters, an index is created on the entries of the map earned
, so the containsEntry
filter can be used to query in GraphQL.
# query a badge record that has a MAP (earned) with only a MAP entry
# Requires: CREATE INDEX badge3_idx ON library.badge(ENTRIES(earned));
query oneWriterEntryBadge {
badge(filter: { earned: { containsEntry: {key:"Writer", value:"2020-11-20"}} } ) {
values {
badge_type
badge_id
earned {
key
value
}
}
}
}
{
"data": {
"badge": {
"values": [
{
"badge_type": "Gold",
"badge_id": 100,
"earned": [
{
"key": "Writer",
"value": "2020-11-20"
}
]
}
]
}
}
}
Update data
Using the column that we added earlier, the data for a book is updated with the
ISBN
value:
mutation updateOneBook {
moby: updatebook(value: {title:"Moby Dick", author:"Herman Melville", isbn: "9780140861723"}, ifExists: true ) {
value {
title
author
isbn
}
}
}
{
"data": {
"moby": {
"value": {
"title": "Moby Dick",
"author": "Herman Melville",
"isbn": "9780140861723"
}
}
}
}
Updates are upserts. If the row doesn’t exist, it will be created. If it does exist, it will be updated with the new row data. |
It is also possible to update other types of data, such as a set:
# update one book, adding a SET (genre)
mutation updateOneBookAgain {
moby: updatebook(value: {title:"Moby Dick", author:"Herman Melville", genre: ["Drama", "Classic lit"]}, ifExists: true ) {
value {
title
author
genre
}
}
}
{
"data": {
"moby": {
"value": {
"title": "Moby Dick",
"author": "Herman Melville",
"genre": [
"Drama",
"Classic lit"
]
}
}
}
}
Delete data
After adding the book "Pride and Prejudice" with an insertBooks()
, you can delete
the book using deleteBooks()
to illustrate deleting data:
mutation deleteOneBook {
PaP: deletebook(value: {title:"Pride and Prejudice", author: "Jane Austen"}, ifExists: true ) {
value {
title
}
}
}
{
"data": {
"PaP": {
"value": {
"title": "Pride and Prejudice"
}
}
}
}
Note the use of ifExists
to validate that the book exists before deleting it.
Deletion options
Similar to the option ifExists
, you can delete a book using consistency
,
serialConsistency
, or ttl
, similar to insertions: