Skip to content

Database IDEs

DuckDB has no network protocol, so a database IDE cannot open a curral server the way it opens Postgres. With --flight-listen, curral serves Arrow Flight SQL on a second port: DataGrip and DBeaver connect through the Arrow Flight SQL JDBC driver, and ADBC clients (Python, Go, R) connect directly. They browse only what the user may query and run SQL under the same rules as /v1/query.

Terminal window
curral serve ... --flight-listen :31337

Or CURRAL_FLIGHT_LISTEN=:31337. Without it nothing listens. To try it with the quickstart configuration, publish the port and add the flag:

Terminal window
docker run --rm -p 127.0.0.1:8080:8080 -p 127.0.0.1:31337:31337 \
-v "$PWD/examples:/etc/curral:ro" -e CURRAL_DATA=/var/lib/curral \
lucasapassos/curral serve \
--catalog /etc/curral/catalog.yaml \
--users /etc/curral/users.yaml \
--policy /etc/curral/policy.rego --policy /etc/curral/roles.json \
--flight-listen :31337

Every call goes through the same pipeline as the HTTP API: users, API keys and JWTs, lockouts, the policy, row filters and masks, limits, the audit log and the metrics. Nothing is configured twice.

  • Build: the port serves Arrow, so it needs a build with -tags duckdb_arrow, as are the release binaries and the Docker image. Without the tag, startup fails.
  • TLS: --tls-cert and --tls-key also cover the Flight port. Without them curral warns that credentials travel unencrypted.

DataGrip and DBeaver both use Apache Arrow’s Flight SQL JDBC driver. Download flight-sql-jdbc-driver-<version>.jar from Maven Central and add it to the IDE as a custom driver:

Setting Value
Driver class org.apache.arrow.driver.jdbc.ArrowFlightJdbcDriver
URL jdbc:arrow-flight-sql://curral.example.com:31337
Without TLS add ?useEncryption=false
Database for unqualified names add &database=logs (optional)
Password login the curral user and password
API key or JWT driver property token = curral_... or the JWT
Java options --add-opens=java.base/java.nio=ALL-UNNAMED, plus --sun-misc-unsafe-memory-access=allow on Java 24 and later

Locally, without TLS: jdbc:arrow-flight-sql://localhost:31337?useEncryption=false.

Why the Java options: the driver keeps results in off-heap Arrow buffers, allocated through the netty it bundles. That needs the address fields of java.nio buffers, closed to other modules since Java 16 (--add-opens), and sun.misc.Unsafe memory access, which Java 24 restricts (--sun-misc-unsafe-memory-access=allow). Without them the driver fails with ExceptionInInitializerError. Older JVMs refuse to start with the second option, so add it only on Java 24 and later. Any Flight SQL server needs these options with this driver; curral does not.

  1. Add the driver. Database Explorer → ⚙ Drivers → +. Under Driver Files, add the jar with + → Custom JARs… (the file, not its folder).

  2. Pick the class. Choose org.apache.arrow.driver.jdbc.ArrowFlightJdbcDriver from the Class list. A class left at java.sql.Driver fails with java.sql.Driver.<init>().

  3. Create the data source. A new data source with this driver: put the URL and the curral user and password. For an API key, leave user and password empty and add the token driver property.

  4. Set the Java options. In Advanced → VM options, add both:

    --add-opens=java.base/java.nio=ALL-UNNAMED --sun-misc-unsafe-memory-access=allow

    DataGrip runs each driver in its own JVM, and its bundled runtime is Java 25, so both are needed.

  1. Add the driver. Database → Driver Manager → New: add the jar under Libraries, the class above under Class Name, and jdbc:arrow-flight-sql://{host}:{port} as URL Template.

  2. Create the connection. A new connection with this driver: host, port (31337), and the curral user and password. When the port has no TLS, set useEncryption=false in the connection’s Driver properties; an API key goes there too, as token.

  3. Set the Java options. DBeaver loads drivers in its own JVM, so they go in dbeaver.ini, one per line after -vmargs:

    dbeaver.ini
    -vmargs
    --add-opens=java.base/java.nio=ALL-UNNAMED
    --sun-misc-unsafe-memory-access=allow

    Drop the second line if DBeaver runs on a Java older than 24. Restart DBeaver afterwards.

  • The database tree lists the attached databases as catalogs. Schemas, tables and columns come from the same listing as GET /v1/schema: only what the user could query, audited as schema events. Primary and foreign keys and type information come back empty.
  • Masks and row filters apply to every result, including the IDE’s data preview: a masked column arrives masked, and rows outside the user’s filter never arrive.
  • The SQL dialect in the IDE is generic: write DuckDB SQL. Catalog views (information_schema, duckdb_tables()…) stay blocked; the tree uses the Flight SQL listing instead.
  • Picking a database in the IDE (JDBC’s setCatalog) sets it for unqualified names. So do the database= URL property and, for other clients, a database header. Otherwise the catalog file’s default applies. The choice grants nothing: every statement is still authorized for the tables it really reads.
  • No transactions: each statement runs in its own transaction, as over HTTP; the IDE sees autocommit.
  • Row limit: a result cut at the row limit ends cleanly, with the trailer x-curral-error: row limit reached.
  • User and password: the client’s handshake trades them for a session token used on the calls that follow. It lasts 12 hours and is signed with a key that lives only in memory, so a restart, a password change or the user’s removal ends it. Roles come from the users file in effect, so a reload applies at once. The audit log records these calls as basic.
  • API keys and JWTs go as a bearer token on every call (JDBC: the token property). See Users and API keys and OIDC.

Session tokens only work on the Flight port; over HTTP they get 401.

IDEs send every statement as a prepared statement. Preparing inspects and authorizes it without running it, so a refused statement fails right away, and returns its parameter and result types. It writes a prepare audit event, kept out of the query metrics. Running it is authorized again and writes the usual query event.

Each statement sent through JDBC is therefore inspected twice. With lake catalogs, set cache_ttl so the second inspection does not pay the catalog round trip. DECIMAL parameters are sent as text.

A refused or failed statement comes back as a gRPC status, with the same message the HTTP API would send:

HTTP API Flight SQL Typical cause
400 INVALID_ARGUMENT invalid SQL, bad parameters
401 UNAUTHENTICATED wrong credentials, expired session token
403 PERMISSION_DENIED the policy, the engine or a protection refused
429 RESOURCE_EXHAUSTED lockout, or the user over their quota
503 UNAVAILABLE queue full, audit log unavailable
504 DEADLINE_EXCEEDED query timeout

A request to open a transaction gets UNIMPLEMENTED.

ADBC talks Flight SQL without JDBC. In Python, with pip install adbc-driver-flightsql pyarrow:

import adbc_driver_flightsql.dbapi as flightsql
conn = flightsql.connect("grpc://localhost:31337",
db_kwargs={"username": "analyst", "password": "analyst-pw"})
cur = conn.cursor()
cur.execute("SELECT * FROM orders WHERE id = ?", (3,))
table = cur.fetch_arrow_table() # pyarrow.Table
  • Use grpc+tls:// when the port has TLS.
  • For the database of unqualified names, set the option adbc.flight.sql.rpc.call_header.database.
  • Parameters use ? placeholders, as JDBC does; the HTTP API uses $1.
Symptom Cause
ExceptionInInitializerError when connecting the Java options are missing
Unrecognized option: --sun-misc-unsafe-memory-access=allow the JVM is older than Java 24; drop that option
java.sql.Driver.<init>() DataGrip’s Class was left at java.sql.Driver
TLS or handshake errors against a local server the port has no TLS; add useEncryption=false
Connection refused curral started without --flight-listen, or the port is not published
flight sql needs a build with -tags duckdb_arrow at startup a source build without the tag
The tree misses a table that exists the policy does not let this user query it
transactions are not supported the client turned autocommit off; turn it back on