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.
Turning it on
Section titled “Turning it on”curral serve ... --flight-listen :31337Or CURRAL_FLIGHT_LISTEN=:31337. Without it nothing listens. To try it with
the quickstart configuration, publish the port and add
the flag:
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 :31337Every 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-certand--tls-keyalso cover the Flight port. Without them curral warns that credentials travel unencrypted.
The JDBC driver
Section titled “The JDBC driver”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.
DataGrip
Section titled “DataGrip”-
Add the driver. Database Explorer → ⚙ Drivers → +. Under Driver Files, add the jar with + → Custom JARs… (the file, not its folder).
-
Pick the class. Choose
org.apache.arrow.driver.jdbc.ArrowFlightJdbcDriverfrom the Class list. A class left atjava.sql.Driverfails withjava.sql.Driver.<init>(). -
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
tokendriver property. -
Set the Java options. In Advanced → VM options, add both:
--add-opens=java.base/java.nio=ALL-UNNAMED --sun-misc-unsafe-memory-access=allowDataGrip runs each driver in its own JVM, and its bundled runtime is Java 25, so both are needed.
DBeaver
Section titled “DBeaver”-
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. -
Create the connection. A new connection with this driver: host, port (
31337), and the curral user and password. When the port has no TLS, setuseEncryption=falsein the connection’s Driver properties; an API key goes there too, astoken. -
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=allowDrop the second line if DBeaver runs on a Java older than 24. Restart DBeaver afterwards.
What the IDE sees
Section titled “What the IDE sees”- 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 asschemaevents. 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 thedatabase=URL property and, for other clients, adatabaseheader. Otherwise the catalog file’sdefaultapplies. 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.
Logging in
Section titled “Logging in”- 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
tokenproperty). See Users and API keys and OIDC.
Session tokens only work on the Flight port; over HTTP they get 401.
Prepared statements
Section titled “Prepared statements”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.
Errors
Section titled “Errors”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 (Python, Go, R)
Section titled “ADBC (Python, Go, R)”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.
Troubleshooting
Section titled “Troubleshooting”| 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 |