Skip to content

Exporting Query Results

PuppyGraph supports exporting query results to cloud storage services.

A query names its destination by referring to an export location that an administrator has registered. Each location holds a storage type, a base path, and the credentials needed to reach it, and a query supplies the location name plus a directory beneath that path. A catalog already registered in the graph schema can serve as a destination as well.

Supported storage types and file formats

Storage Types

  • Amazon S3
  • Azure Data Lake Storage (Gen2)
  • Google Cloud Storage
  • MinIO
  • HDFS

File Formats

  • CSV files
  • Parquet files
  • Parquet Iceberg tables, through a schema catalog only
  • Parquet Hive tables, through a schema catalog only

Syntax

EXPORT TO '<location>:<directory>'
[PROPERTIES {
  key: value,
  ...
}]
[cypher query]

From Graph traversal queries:

g.with("exportTo", "<location>:<directory>")
.with("key", value)
...
[gremlin steps]
From Graph algorithm programs:
graph.program([program_def])
.submitAndSave([
  "exportTo": "<location>:<directory>",
  "key": value,
  ...
])

A CSV export with the default transfer settings needs no other options, so the PROPERTIES block is optional in OpenCypher.

Parameters

Key Description Default Value
exportTo Export target: <location>:<directory> for a registered export location, or a path URI or <database>.<table> name when catalog is set -
fileType Target file format (csv, parquet, or table) csv
catalog Catalog name defined in the schema, to export with that catalog's own storage configuration -
transferSize Number of rows per transfer batch 10000
timeout Export timeout in seconds 43200

Registering an export location

Registering, changing, and deleting export locations requires the CATALOG:write permission, which only the Admin role holds. Listing them requires CATALOG:read, which every role holds, so any query author can discover the available names, and credentials are masked in the listing. Every user who can run a query can export to every registered location. See Role-Based Access Control.

Manage locations from Exports in the left sidebar of the Web UI, or through any of the interfaces below. A change is pushed to every running query engine immediately and resent periodically, so a restarted engine picks the registered locations up again on its own.

Location fields

  • name: how queries refer to the location. Letters, digits, _, and -, starting with a letter or _.
  • type: one of MINIO, S3, GCS, AZURE_DLS2, or HDFS.
  • path: the base URI that every export to this location is written under. Its scheme must match the type: s3:// or s3a:// for MINIO and S3, gs:// for GCS, abfs:// or abfss:// for AZURE_DLS2, and hdfs:// for HDFS. A trailing / is added if missing.
  • The remaining fields are the storage settings for that type, listed in Location settings per storage type.

REST API

# register
curl -u <user>:<password> -H 'content-type: application/json' \
  -X POST http://localhost:8081/ui-api/exportLocation -d '{
    "name": "minio_test",
    "type": "MINIO",
    "path": "s3://test/",
    "endpoint": "http://minio:9000",
    "identifier": "admin",
    "credential": "password"
  }'

# list, with credentials masked as ******
curl -u <user>:<password> http://localhost:8081/ui-api/exportLocation

# change: send the whole location again. A masked credential keeps the stored value.
curl -u <user>:<password> -H 'content-type: application/json' \
  -X PUT http://localhost:8081/ui-api/exportLocation -d '{ ... }'

# delete
curl -u <user>:<password> -X DELETE 'http://localhost:8081/ui-api/exportLocation?name=minio_test'

From a query session

CALL puppy.schema.addExportLocation($location) YIELD success, error_message
CALL puppy.schema.updateExportLocation($location) YIELD success, error_message
CALL puppy.schema.listExportLocations() YIELD name
CALL puppy.schema.removeExportLocation('minio_test') YIELD success, error_message
$location is the location JSON, bound as a query parameter.

graph.getTopology2().addExportLocation('{"name": "minio_test", "type": "MINIO", "path": "s3://test/", "endpoint": "http://minio:9000", "identifier": "admin", "credential": "password"}')
graph.getTopology2().updateExportLocation('{"name": "minio_test", "type": "MINIO", "path": "s3://test/", "endpoint": "http://minio:9000", "identifier": "admin", "credential": "password"}')
graph.getTopology2().listExportLocations()
graph.getTopology2().removeExportLocation('{"name": "minio_test"}')

Location settings per storage type

Amazon S3

Before exporting to Amazon S3, ensure that:

  • The registered credentials have been granted write permissions to the target S3 bucket
  • The IAM policy includes proper authorization for s3:PutObject actions
{
  "name": "s3_exports",
  "type": "S3",
  "path": "s3://your_bucket/exports/",
  "region": "us-east-1",
  "identifier": "<aws_access_key_id>",
  "credential": "<aws_secret_access_key>"
}
  • region: AWS region where the target S3 bucket is located
  • identifier: AWS access key ID for S3 authentication
  • credential: Corresponding secret access key for AWS authentication

MinIO

Before registering the location, ensure the credentials have write access to the target bucket.

{
  "name": "minio_test",
  "type": "MINIO",
  "path": "s3://test/",
  "endpoint": "http://minio:9000",
  "identifier": "<user>",
  "credential": "<password>"
}
  • endpoint: Endpoint URL of the MinIO service. TLS is used when it starts with https://
  • identifier: Authentication username for MinIO
  • credential: Authentication password for MinIO

Google Cloud Storage

Before registering the location, ensure the credentials have write access to the target bucket.

To authenticate with a JSON key file, mount the key file during container creation and set the GOOGLE_APPLICATION_CREDENTIALS environment variable to the mounted path inside the container. See Authentication for PuppyGraph to access Google Cloud resources. The location then needs no credential fields:

{ "name": "gcs_exports", "type": "GCS", "path": "gs://your_bucket/exports/" }

To use the service account attached to the Compute Engine VM instance PuppyGraph runs on, set useComputeEngineService. The instance must be associated with a service account whose access scope covers storage operations. See service accounts.

{ "name": "gcs_exports", "type": "GCS", "path": "gs://your_bucket/exports/", "useComputeEngineService": true }

To register a service account key directly:

{
  "name": "gcs_exports",
  "type": "GCS",
  "path": "gs://your_bucket/exports/",
  "serviceAccountEmail": "<email>",
  "identifier": "<key_id>",
  "credential": "<secret_key>"
}
  • serviceAccountEmail: Service account email address associated with your Google Cloud project
  • identifier: Private key ID of the service account
  • credential: Private key of the service account
  • impersonationServiceAccount: Optional service account to impersonate

Azure Data Lake Storage Gen2

Before registering the location, ensure the credentials have write access to the target storage location. The path has the form abfss://<container-name>@<storage-account-name>.dfs.core.windows.net/<path>/.

Using a storage account access key:

{
  "name": "adls_exports",
  "type": "AZURE_DLS2",
  "path": "abfss://<container-name>@<storage-account-name>.dfs.core.windows.net/exports/",
  "identifier": "<storage-account-name>",
  "credential": "<storage-account-access-key>"
}

Using a managed identity:

{
  "name": "adls_exports",
  "type": "AZURE_DLS2",
  "path": "abfss://<container-name>@<storage-account-name>.dfs.core.windows.net/exports/",
  "useManagedIdentity": true,
  "tenantId": "<tenant-id-of-identity>",
  "clientId": "<client-id-of-identity>"
}
  • identifier: Name of your Azure Storage Account
  • credential: Access key for the storage account
  • tenantId: Tenant ID of the managed identity
  • clientId: Client ID (application ID) of the managed identity

A workload identity adds useWorkloadIdentity and tokenFile. A service principal with a client secret uses clientId, clientSecret, and endpoint, the OAuth2 token URL.

HDFS

Before registering the location, ensure you have write access to the target path.

{ "name": "hdfs_exports", "type": "HDFS", "path": "hdfs://hadoop_node:9000/user/exports/" }

Add identifier and credential for simple authentication.

Exporting to a registered location

In a query, the EXPORT TO target in OpenCypher and the exportTo option in Gremlin take the form <location>:<directory>. The name before the first colon identifies the registered location, and everything after it is a directory under that location's path. With the minio_test location above, minio_test:reports/2026 writes to s3://test/reports/2026/.

EXPORT TO 'minio_test:reports/2026'
MATCH (p)-[:created]->(s:software)
RETURN s.name as name, sum(p.age) as totalAge
g.with("exportTo", "minio_test:reports/2026")
  .V().as("s")
  .in("created").as("p")
  .group().by(select("s")).by(values('age').sum())
  .unfold()
  .project('name', 'totalAge')
    .by(select(keys).values('name'))
    .by(select(values))

The directory accepts letters and digits in any script, along with ., _, -, =, :, and /, so a timestamped directory such as minio_test:dt=2026-09-05T00:00:00Z/ works. Use a single colon after the location name, not ://, and keep the directory relative so that it stays under the location's path. An empty directory, as in minio_test:, exports directly under that path.

For Parquet, add PROPERTIES { fileType: 'parquet' } after the target in OpenCypher, or .with("fileType", "parquet") in Gremlin.

Export using schema catalog configuration

When the export destination storage matches the schema's catalog configuration, you can reuse existing storage settings. Before saving data to this location, ensure you possess appropriate write access privileges. With catalog set, the target is interpreted as a catalog destination rather than a location reference.

Export to cloud storage

EXPORT TO '<target_path>'
PROPERTIES {
  catalog: '<catalog name>'
}
MATCH (p)-[:created]->(s:software) 
RETURN s.name as name
g.with("exportTo", "<target_path>")
  .with("catalog": "<catalog name>")
  .V()
  .out("created")
  .project('name')
    .by(values('name'))
  • <target_path>: Storage path URI matching catalog scheme (e.g., s3://my_bucket/subfolder)
  • <catalog name>: Name of the catalog defined in the graph schema

Bounding catalog export paths

A catalog export uses the catalog's credentials while the query chooses the path, and for hdfs://, abfss://, and gs:// URIs the path includes the host. An administrator can bound this with the runtime property feature.engine.exportCatalogAllowedPathPrefixes, a comma-separated list of URI prefixes the export path must start with. Prefixes are compared exactly and case-sensitively, after . and .. segments are refused. The default * allows any path, and every query engine logs it at WARN on startup and whenever it takes effect, so an unconfigured deployment is visible in its logs.

curl -u <user>:<password> -X PUT http://localhost:8081/ui-api/features \
  -H 'Content-Type: application/json' \
  -d '{"properties": {"feature.engine.exportCatalogAllowedPathPrefixes": "s3://warehouse/exports/,gs://analytics/exports/"}}'

Exporting as a table is not affected, because its target is a <database>.<table> name in the catalog rather than a path.

Export as Iceberg/Hive table

It is also possible to store query results as an Iceberg or Hive table. This feature is currently experimental.

To store results as an Iceberg table:

  • Configure the catalog type in the graph schema as Iceberg.
  • Ensure the Iceberg catalog service aligns with the schema configuration.
  • Confirm you have CREATE TABLE privileges on the target Iceberg database (schema).
  • Check that you have write permissions for the designated storage path.

To store results as a Hive table:

  • Configure the catalog type in the graph schema as Hive (See Connecting to Hive).
  • If you are using a kerberized Hive cluster, ensure you have configured PuppyGraph with the Kerberos settings (See Querying Kerberized Hive Data as a Graph).
  • Ensure the Hive catalog service aligns with the schema configuration.
  • Confirm you have CREATE TABLE privileges on the target Hive database.
  • Check that you have write permissions for the designated storage path.

Query results will be stored as Parquet files in the new table. A table export requires catalog; it cannot be written to an export location.

EXPORT TO '<target_database>.<target_table>'
PROPERTIES {
  catalog: '<catalog name>',
  fileType: 'table'
}
MATCH (p)-[:created]->(s:software) 
RETURN s.name as name
g.with("exportTo", "<target_database>.<target_table>")
  .with("catalog", "<catalog name>")
  .with("fileType", "table")
  .V()
  .out("created")
  .project('name')
    .by(values('name'))
  • <target_database>.<target_table>: The database and table to save to, database name must be provided. e.g. mydatabase.result_table
  • <catalog name>: Name of the catalog defined in the graph schema. It must be of Iceberg or Hive type.
  • fileType parameter: Must be set to table