Skip to content

Troubleshooting

JVMError: could not find Java

jdbc4py looks for Java via JAVA_HOME first, then PATH. If neither works:

# Check if Java is on PATH
java -version

# Set JAVA_HOME if needed
export JAVA_HOME=$(/usr/libexec/java_home)        # macOS
export JAVA_HOME=/usr/lib/jvm/java-17-openjdk     # Linux

If Java is installed but JAVA_HOME isn't set, jdbc4py will try to find it via PATH. If that fails, set JAVA_HOME explicitly and make sure it points to a Java 11+ runtime.

JVMError: Java version X is below the minimum

jdbc4py requires Java 11 or newer. Check what's running:

java -version

If you have multiple Java versions, set JAVA_HOME to point to the right one.

DriverNotFoundError

jdbc4py couldn't find a JAR for your engine. Check:

  1. Is the JAR in drivers/<engine>/?
  2. Does the JAR filename match the expected glob? (e.g. postgresql-42.7.10.jar matches postgresql-*.jar)
  3. If using JDBC4PY_DRIVER_PATHS, is the path an absolute path that exists?

Enable debug logging to see where jdbc4py is looking:

import logging
logging.basicConfig(level=logging.DEBUG)

JVMError: classpath is immutable — cannot add JARs to a running JVM

You called connect() with a JAR that isn't already on the JVM classpath, but the JVM was already started by a previous connect() call.

Fix: start the JVM explicitly with all your JARs before the first connection:

from jdbc4py.jvm import JVMManager

JVMManager.ensure_started(jars=[
    "drivers/postgres/postgresql-42.7.10.jar",
    "drivers/mysql/mysql-connector-j-9.1.0.jar",
])

Connection refused / OperationalError

The database isn't reachable. Check:

  • Is the database running?
  • Is the host and port correct?
  • Is there a firewall blocking the connection?
  • For Docker, is the container healthy and the port mapped correctly?

DatabaseError: Authentication failed

Wrong username or password. Double-check credentials. For Kerberos or IAM auth, check that the JDBC connection properties are set correctly via properties=.

TypeError during parameter binding

jdbc4py doesn't know how to bind the value you passed. Common cases:

  • numpy integer/float types — cast to Python int or float first
  • Custom objects — convert to a supported type before passing as a parameter
# Instead of:
cur.execute("INSERT INTO t VALUES (?)", [np.int64(42)])

# Do:
cur.execute("INSERT INTO t VALUES (?)", [int(np.int64(42))])

fetchall() returns empty results or wrong types

Check cursor.description after execute() — it shows the column names and types as reported by the JDBC driver. If the types look wrong, it's usually a driver-specific type name that jdbc4py's converter doesn't know about. Open an issue with the driver name and type name.

Arrow / read_sql returns wrong types

The Arrow type inference for NUMBER columns without precision (common in Oracle) may default to float64. If you need integers, cast in SQL:

SELECT CAST(my_number_col AS INTEGER) FROM my_table

CLOB columns are truncated

The Java CLOB cap defaults to 64M characters. If your CLOBs are larger, increase the cap:

import jpype
jpype.startJVM("-Djdbc4py.clob.max_chars=268435456")  # 256M chars

import jdbc4py
conn = jdbc4py.connect(...)

This must be set before the JVM starts (before the first connect() call).

Memory usage growing over time

If memory usage keeps growing during long-running read_sql calls:

  • Use iter_arrow_batches() with a bounded batch_size instead of read_sql() — this lets Python garbage-collect each batch after processing
  • If using a connection pool, check that connections are being returned (conn.close() or with pool.connect() as conn:)

Pool checkout timeout

OperationalError: ConnectionPool timed out after Xs waiting for a free connection means all max_connections connections are checked out and none was returned within checkout_timeout seconds.

Options: - Increase max_connections if the database can handle more connections - Increase checkout_timeout if bursts are expected - Check for connection leaks — connections not being returned to the pool

Queries not getting cancelled

cursor.cancel() calls Statement.cancel() on the JDBC driver. Whether the driver actually interrupts the query depends on driver support. Most production drivers (PostgreSQL, SQL Server) implement it properly. Some drivers (older versions of ClickHouse JDBC) may not.

If cancel has no effect, check the driver documentation and version.

Each Python thread that makes JDBC calls must be attached to the JVM. Recent JPype (1.5+) handles this automatically. If you're seeing RuntimeError: thread is not attached to JVM or similar, upgrade JPype or attach manually:

import jpype
from jdbc4py.jvm import JVMManager

JVMManager._attach_thread(jpype)
try:
    # ... JDBC calls ...
finally:
    JVMManager._detach_thread(jpype)

Enabling debug logging

import logging
logging.basicConfig(level=logging.DEBUG, format="%(name)s %(levelname)s: %(message)s")

The loggers you care about are:

  • jdbc4py.connection — connection open/close
  • jdbc4py.cursor — query execution, timeouts
  • jdbc4py.pool — checkout/return, eviction
  • jdbc4py.drivers — JAR discovery, JDBC4PY_DRIVER_PATHS validation
  • jdbc4py.jvm — JVM startup, thread attachment