Skip to content

CUBRID PL/JavaSP — Java Stored Procedures, JDBC Bridge, and the PL/CSQL-Sibling External PL Engine

Contents:

This document covers JavaSP — the Java stored-procedure sub-system. Its sibling document cubrid-pl-plcsql.md covers PL/CSQL, CUBRID’s Oracle-dialect procedural language. The two form the PL family (pl-family tag) because they share almost everything except the final dispatch into user code.

LayerShared artefact
Catalog_db_stored_procedure, _db_stored_procedure_args, _db_stored_procedure_code rows in the same three system classes (see sp_constants.hpp for attribute names and sp_catalog.hpp for the C++ structs that write them)
Transportpl_connection.cpp / pl_comm.c — Unix domain socket (UDS) or TCP from cub_server to cub_pl; the same PL_CONNECTION_POOL and connection_view types are used regardless of language
Executorpl_executor.cpp / pl_session.cpp — the C++ executor class sends every invocation to cub_pl over the shared connection pool; the session class tracks cursors, execution stacks, and per-session parameters for both language types
JVM hostingServer.java, ListenerThread.java, ExecuteThread.java — a single cub_pl process runs one JVM that hosts both JavaSP JAR dispatch and the PL/CSQL compiler+runtime
ConcernArtefact
Reflective dispatch on user JARsTargetMethod.java — resolves ClassName.methodName(argTypes) by reflection; StoredProcedure.invoke() calls Method.invoke() on the loaded class
Classloader hierarchyclassloader/ package — ClassLoaderManager, ContextClassLoader, SessionClassLoader, ServerClassLoader; user JARs are loaded from $CUBRID_DATABASES/<db>/java/ (dynamic) or java_static/ (static) paths
Security sandboxSpSecurityManager.java — a custom SecurityManager that allows almost everything but blocks System.exit() (unless the server itself is shutting down) and blocks native library loading (System.loadLibrary) from user classloaders
Legacy JVM in-process pathpl_sr_jvm.cpppl_start_jvm_server() / pl_server_port() provided the old in-process JNI embedding before the external cub_pl process model; still compiled but superseded

When reading either PL document, understand that the catalog and the wire protocol are explained here once and cross-referenced there.

A stored procedure is a named, parameterized unit of computation stored inside the database engine and invoked by SQL or a client call. From a database systems perspective, three design choices dominate the implementation space:

  1. Language runtime placement. The procedure body can run inside the database server process (in-process), in a satellite process that the server forks (external process), or on a separate application server. In-process gives the lowest call overhead and direct access to internal data structures, but a buggy stored procedure can crash the entire server. External processes give isolation at the cost of IPC latency. CUBRID started with an in-process JNI approach and migrated to a forked external process (cub_pl) for isolation.

  2. Language. SQL/PSM (SQL standard procedural extension), PL/SQL-family languages (Oracle-dialect, Sybase-dialect), Java (IBM DB2 Java routines, CUBRID JavaSP), and JavaScript/Python (modern NewSQL engines). Java was historically appealing because it is strongly typed, already used for JDBC-based application logic, and produces portable bytecode. The trade-off is JVM startup and GC pauses; CUBRID mitigates this by keeping the JVM alive as a long-running sidecar.

  3. JDBC back-channel. When a Java stored procedure issues a SQL query it needs a database connection. If the query runs through the normal client-server path it would introduce network round-trips and require the user to pass credentials. The common pattern (IBM DB2 uses this; CUBRID follows the same design) is a server-side JDBC driver that bypasses the network and sends callback requests back to the originating server worker thread over the same IPC channel that delivered the invocation.

cub_server (C/C++ process)
│ PRM_ID_STORED_PROCEDURE = true
├── pl_server_init() ← boot_sr.c calls this during server boot
│ └── server_manager::start()
│ └── create_child_process("cub_pl", db_name) ← fork+exec
├── server_monitor_task (daemon thread, 1-sec loop)
│ ├── is_terminated_process(pid) ← reap and restart if cub_pl crashes
│ ├── do_check_connection() → do_ping_connection() ← SP_CODE_UTIL_PING
│ └── do_bootstrap_request() ← SP_CODE_UTIL_BOOTSTRAP (sends sysprms)
└── connection_pool (10 connections, UDS or TCP)
└── connection_view (RAII handle: claim/retire)
cub_pl (Java process — pl_engine/ Gradle artifact)
├── Server.main()
│ ├── SpSecurityManager installed
│ ├── ClassLoaderManager dirs created
│ └── initializeSocket() → ServerSocket (UDS via junixsocket or TCP)
├── ListenerThread (accept loop)
│ └── for each accepted socket → new ExecuteThread(socket).start()
└── ExecuteThread (one per active connection)
├── RequestCode.UTIL_PING → respond with server name
├── RequestCode.UTIL_BOOTSTRAP → apply sysprm settings
├── RequestCode.INVOKE_SP → processStoredProcedure()
│ ├── PrepareArgs.readArgs()
│ ├── makeStoredProcedure() → StoredProcedure
│ └── StoredProcedure.invoke()
│ ├── [JavaSP] TargetMethod.getMethod() → Method.invoke()
│ └── [PL/CSQL] PlcsqlCompilerMain / compiled class dispatch
├── RequestCode.COMPILE → processCompile()
└── RequestCode.DESTROY → ContextManager.destroyContext()

cub_pl writes its PID and port (or -1 for UDS mode) to $CUBRID/var/pl_<db_name>.info after binding its socket. The C++ side reads this file via pl_read_info() (in pl_file.c) to learn the port number. The monitor task polls this file on every reconnect attempt. In UDS mode the socket path is a well-known file path so no info file port field is needed.

Every message on the socket is a length-prefixed byte buffer. The first field is a Header that carries the session ID and a request code. The C++ side uses SP_CODE values (defined in pl_comm.h) for system-level commands (PING, BOOTSTRAP, DESTROY) and METHOD_REQUEST values (in sp_constants.hpp) for SP invocations. The Java side’s RequestCode constants mirror the C++ enums exactly.

// pl_comm.h — SP_CODE
SP_CODE_INVOKE = 0x01 // invoke a SP
SP_CODE_RESULT = 0x02 // result from SP to server (callback)
SP_CODE_ERROR = 0x04 // error from SP to server
SP_CODE_INTERNAL_JDBC = 0x08 // back-channel JDBC request
SP_CODE_DESTROY = 0x10 // session teardown
SP_CODE_COMPILE = 0x80 // PL/CSQL compile request
SP_CODE_UTIL_BOOTSTRAP = 0xDD // bootstrap sysprm
SP_CODE_UTIL_PING = 0xDE // liveness probe
SP_CODE_UTIL_STATUS = 0xEE // status query

The invoke_java packable struct (in pl_executor.cpp) serialises the invocation payload: transaction ID, signature string (ClassName.methodName), auth context, language tag (SP_LANG_JAVA or SP_LANG_PLCSQL), argument count, per-argument mode and DB type, return type, and transaction-control flag.

// invoke_java::invoke_java — pl_executor.cpp
signature.assign (sig->ext.sp.target_class_name)
.append (".").append (sig->ext.sp.target_method_name);
lang = sig->type; // PL_TYPE_JAVA_SP or PL_TYPE_PLCSQL
transaction_control = (lang == SP_LANG_PLCSQL) ? true : tc;

The PL/CSQL language always gets transaction_control = true; JavaSP inherits it from the SP’s definition.

Three system classes hold stored procedure metadata:

_db_stored_procedure (SP_CLASS_NAME)
unique_name, sp_name, sp_type, return_type, arg_count, args,
lang, pkg_name, is_system_generated, directive,
target_class, target_method, ← JavaSP-only meaningful fields
owner, sql_data_access, comment, created_time, updated_time
_db_stored_procedure_args (SP_ARG_CLASS_NAME)
sp_of, index_of, arg_name, data_type, mode,
default_value, is_optional, comment
_db_stored_procedure_code (SP_CODE_CLASS_NAME)
name, created_time, owner, is_static, is_system_generated,
stype (source type: PLCSQL=0 / JAVA=1),
scode (source code text),
otype (object code type: JAVA_CLASS / JAVA_JAR),
ocode (compiled object code, base64)

sp_info::lang is SP_LANG_JAVA (1) for JavaSP and SP_LANG_PLCSQL (0) for PL/CSQL — this single field routes the invocation to the appropriate handler inside ExecuteThread.

The C++ functions sp_add_stored_procedure(), sp_add_stored_procedure_argument(), and sp_add_stored_procedure_code() in sp_catalog.cpp write these rows at CREATE PROCEDURE / CREATE FUNCTION time. jsp_cl.cpp handles the client-side DDL processing (parse tree to catalog row), guarded by #if !defined(SERVER_MODE) since DDL runs client-side.

cubpl::session (in pl_session.hpp) is the per-CUBRID-session state object:

  • m_stack_map / m_exec_stack — execution stack tracking (one execution_stack per recursive SP call; each gets its own worker thread in the cub_pl thread pool).
  • m_cursor_map — server-side cursors opened by JDBC back-channel queries.
  • m_session_connections — a deque of connection_view objects borrowed from the global pool for the duration of a call.
  • m_session_params — per-session parameter shadow (DBMS_OUTPUT flag, etc.); synced to cub_pl via METHOD_CALLBACK_SET_PL_SESSION_PARAM.

cubpl::executor (in pl_executor.hpp) drives one invocation:

  1. fetch_args_peek() — reads argument DB_VALUEs from the XASL value descriptor or a direct CALL statement arg list.
  2. request_invoke_command() — packs an invoke_java and sends it over the claimed connection.
  3. response_invoke_command() — runs a loop reading responses; each response is either a final result (SP_CODE_RESULT), an error (SP_CODE_ERROR), or a callback request (SP_CODE_INTERNAL_JDBC). Callback requests are dispatched to callback_prepare(), callback_execute(), callback_fetch(), etc., which call into the server’s query execution machinery and write results back to cub_pl over the same socket.

JavaSP-specific: reflective dispatch and classloaders

Section titled “JavaSP-specific: reflective dispatch and classloaders”

TargetMethod resolves the target at invocation time. It is constructed from a Signature that carries the class name, method name, and a comma-separated argument-type descriptor string. The constructor calls classesFor() which maps each type name to a Class<?> using a static argClassMap. The map covers all primitive types, their boxed equivalents, java.math.BigDecimal, java.sql.Date/Time/Timestamp, cubrid.sql.CUBRIDOID, and one- and two-dimensional arrays of all of the above. Once the Class<?>[] is resolved, getMethod() calls Class.getMethod(methodName, argsTypes) by reflection.

The classloader hierarchy is:

JVM bootstrap classloader
└── ServerClassLoader (server JARs: cubrid-jdbc, pl_server.jar)
└── ContextClassLoader (per-database user JARs, dynamic path)
└── SessionClassLoader (per-session isolation, if needed)

ClassLoaderManager roots dynamic JARs at $CUBRID_DATABASES/<db>/java/ and static JARs at $CUBRID_DATABASES/<db>/java_static/. It tracks last-modified timestamps so a re-loadjava causes the classloader to pick up the new JAR without restarting cub_pl.

SpSecurityManager is installed as System.setSecurityManager() during Server construction. Its key restrictions:

  • checkExit() — throws SecurityException unless the Server instance is already in shutdown state. This prevents user stored procedures from calling System.exit() and killing the JVM.
  • checkLink() — inspects the classloader chain; if any frame belongs to a ContextClassLoader or SessionClassLoader, native library loading is blocked with a clear message pointing to loadjava -jni.
  • All other check*() methods are permissive no-ops (blank overrides).

Inside cub_pl, CUBRIDServerSideDriver / CUBRIDServerSideConnection are registered as the JDBC driver. When user Java code runs a SQL query, the JDBC call routes to CUBRIDServerSidePreparedStatement.execute() which serialises a METHOD_CALLBACK_QUERY_PREPARE or METHOD_CALLBACK_QUERY_EXECUTE request and sends it back to cub_server over the same socket that delivered the original SP_CODE_INVOKE. The C++ executor::response_callback_command() loop recognises these callback codes and dispatches them to the appropriate server subsystem (query prepare, execute, cursor fetch, LOB ops, etc.), then writes the result back to cub_pl.

This creates a synchronous callback loop on a single TCP/UDS connection:

cub_server cub_pl
──── SP_CODE_INVOKE ──────►
◄─── METHOD_CALLBACK_QUERY_PREPARE ───
──── (prepared stmt handle) ─────────►
◄─── METHOD_CALLBACK_QUERY_EXECUTE ───
──── (cursor, rows) ──────────────────►
◄─── METHOD_CALLBACK_FETCH ────────────
──── (row data) ──────────────────────►
◄─── SP_CODE_RESULT ───────────────────

Recursive SP calls (a Java SP that calls another SP) each acquire a new execution_stack entry and are handled by a separate ExecuteThread, but they re-use the same session.

The full opcode taxonomy, the server-side dispatcher (cubpl::executor::response_callback_command and its twelve handlers), the recursion guard (METHOD_MAX_RECURSION_DEPTH = 15), and the shared packed wire structures live in the third PL-family sibling, cubrid-pl-server-bridge.md. That doc also covers the older server→CAS callback path (Path A) that shares the same opcode set — the two paths are physically distinct but use the same METHOD_CALLBACK_* enumeration.

server_monitor_task implements a small state machine:

STOPPED ──fork cub_pl──► READY_TO_INITIALIZE
ping poll (up to 10×, 1s each)
bootstrap_request (send sysprms)
┌──────────────┴──────────────────┐
RUNNING FAILED_TO_INITIALIZE
│ │
(monitor daemon re-checks every 1s) (after >10 failures)
process exits? → STOPPED → re-fork

In SERVER_MODE the monitor daemon runs as a cubthread::daemon (1-second looper). In SA_MODE (standalone/csql) do_monitor() is called synchronously and retries up to 10 times.

SymbolFileRole
pl_server_initpl_sr.cppEntry point called by boot_sr.c at server boot; creates server_manager, forks cub_pl
pl_server_destroypl_sr.cppCalled at server shutdown; deletes server_manager
pl_server_wait_for_readypl_sr.cppCalled after init to block until cub_pl is accepting connections
get_connection_poolpl_sr.cppReturns the global PL_CONNECTION_POOL; used by executor to claim connections
pl_server_port_from_infopl_sr.cppReads $CUBRID/var/pl_<db>.info via pl_read_info()
server_managerpl_sr.cppOwns pool + monitor task; start/stop lifecycle
server_monitor_taskpl_sr.cppDaemon task: forks cub_pl, pings, bootstraps, detects crashes
bootstrap_requestpl_sr.cppPackable: sends system-parameter snapshot (SP_CODE_UTIL_BOOTSTRAP)
connection_poolpl_connection.hppFixed-size pool of connection objects; epoch-based invalidation on restart
connectionpl_connection.hppWraps a SOCKET; send_buffer_args, receive_buffer, auto-reconnect
pl_connect_serverpl_comm.cLow-level socket connect (UDS or TCP)
pl_writen / pl_readnpl_comm.cReliable write/read helpers (handle EINTR)
SP_CODE enumpl_comm.hWire protocol codes shared with Java RequestCode
executorpl_executor.hppOne-invocation driver: fetch args → send invoke → handle callbacks → return result
invoke_javapl_executor.cppPackable invocation payload (signature, lang, args, result type)
sessionpl_session.hppPer-CUBRID-session: stacks, cursors, connections, params, interrupt
sys_parampl_session.hppPackable system parameter (DB prm or PL-specific prm)
pl_signaturepl_signature.hppResolved SP descriptor: type, name, auth, arg modes/types, ext (target class/method or code OID)
sp_infosp_catalog.hppC++ representation of _db_stored_procedure row
sp_arg_infosp_catalog.hppC++ representation of _db_stored_procedure_args row
sp_code_infosp_catalog.hppC++ representation of _db_stored_procedure_code row
sp_add_stored_proceduresp_catalog.cppWrites a new SP row at CREATE time
PL_SERVER_INFOpl_file.h{pid, port} struct read/written via pl_read_info / pl_write_info
jsp_create_stored_procedurejsp_cl.cppClient-side CREATE PROCEDURE/FUNCTION handler
jsp_make_pl_signaturejsp_cl.cppBuilds pl_signature from a PT_NODE
SymbolFileRole
ServerServer.javaEntry point; installs SpSecurityManager, sets up socket, starts ListenerThread
ListenerThreadListenerThread.javaServerSocket.accept() loop; creates ExecuteThread per connection; exponential backoff on error
ExecuteThreadExecuteThread.javaPer-connection thread; dispatches on Header.code; processStoredProcedure() / processCompile()
processStoredProcedureExecuteThread.javaReads args, builds StoredProcedure, calls invoke(), sends result
TargetMethodTargetMethod.javaResolves Class.getMethod(name, argTypes) by reflection from a Signature
TargetMethod.argClassMapTargetMethod.javaStatic map: type-name string → Class<?> (primitives, boxed, SQL types, arrays)
TargetMethod.getMethodTargetMethod.javaReturns Method for invocation; throws NoSuchMethodException with full signature in message
SpSecurityManagerSpSecurityManager.javaCustom SecurityManager: blocks exit unless shutting down; blocks native lib load from user classloaders
SpSecurityManager.checkLinkSpSecurityManager.javaInspects getClassContext() chain; rejects loadLibrary from ContextClassLoader/SessionClassLoader
ClassLoaderManagerclassloader/ClassLoaderManager.javaRoot/static/dynamic path management; last-modified tracking for hot JAR reload
ClassLoaderManager.getDynamicPathclassloader/ClassLoaderManager.javaReturns $db_path/java/ (user-uploaded JARs via loadjava)
ClassLoaderManager.getStaticPathclassloader/ClassLoaderManager.javaReturns $db_path/java_static/ (server-wide JARs)
SymbolFileApprox. line
pl_server_initsrc/sp/pl_sr.cpp696
server_manager::startsrc/sp/pl_sr.cpp262
server_monitor_task::do_monitorsrc/sp/pl_sr.cpp356
server_monitor_task::do_bootstrap_requestsrc/sp/pl_sr.cpp625
pl_server_port_from_infosrc/sp/pl_sr.cpp763
SP_CODE enumsrc/sp/pl_comm.h44
connection_pool classsrc/sp/pl_connection.hpp64
executor::executesrc/sp/pl_executor.cpp(in executor class body)
invoke_java::invoke_javasrc/sp/pl_executor.cpp45
session classsrc/sp/pl_session.hpp106
pl_signature structsrc/sp/pl_signature.hpp86
SP_LANG_JAVA, SP_LANG_PLCSQLsrc/sp/sp_constants.hpp147
SP_CLASS_NAME macrosrc/sp/sp_constants.hpp22
sp_info structsrc/sp/sp_catalog.hpp115
PL_SERVER_INFO structsrc/sp/pl_file.h35
Server constructorpl_engine/.../Server.java70
ListenerThread.runpl_engine/.../ListenerThread.java63
ExecuteThread.runpl_engine/.../ExecuteThread.java129
ExecuteThread.processStoredProcedurepl_engine/.../ExecuteThread.java319
TargetMethod.getMethodpl_engine/.../TargetMethod.java243
SpSecurityManager.checkLinkpl_engine/.../SpSecurityManager.java72
ClassLoaderManager.getDynamicPathpl_engine/.../classloader/ClassLoaderManager.java58
  • pl_sr_jvm.cpp is a legacy remnant. pl_start_jvm_server() and pl_server_port() represent the old in-process JNI path where the JVM was embedded into cub_server directly. These symbols still compile but the active code path uses the server_manager fork/exec model in pl_sr.cpp. The JNI path may be dead code or kept for specific build configurations.

  • SpSecurityManager is deprecated in Java 17+. SecurityManager was deprecated for removal in Java 17 (JEP 411). If CUBRID moves to a newer JVM, this sandbox mechanism will need to be replaced (e.g., with a restricted class-loading policy or a separate process per SP execution). The current code targets JDK 1.8+.

  • connection_pool::CONNECTION_POOL_SIZE = 10 is hard-coded. There is no system parameter governing the pool size. High concurrency with many simultaneous SP calls will queue at the pool. The is_system_pool flag distinguishes the monitor’s own system connection pool from the main pool.

  • TargetMethod.argClassMap covers only a fixed set of types. Any user JAR method that takes a class type not in the map (e.g., a custom DTO class) throws ClassNotFoundException at dispatch time. The comment in classFor() acknowledges this gap with a TODO pointing to a future ClassAccess.getClass() implementation.

  • The info file race window. Between fork() returning in the monitor task and cub_pl writing its port to the info file there is a window where pl_server_port_from_info() returns PL_PORT_DISABLED. The monitor addresses this with a polling loop (do_check_connection(), up to 10×, 1s sleep each), but a very slow JVM start could exceed this budget.

  1. Graceful upgrade of SpSecurityManager to Java 17+. No replacement mechanism is visible in the current source. What is the plan when JDK 17+ becomes the target baseline?

  2. Hot JAR reload granularity. ClassLoaderManager.isModified() tracks last-modified timestamps at the JAR file level. Does an in-flight SP invocation that already loaded a class see the old version until the next call? Is there a handoff mechanism?

  3. pl_sr_jvm.cpp fate. Is the in-process JNI path still compiled into any product configuration, or is it entirely dead and scheduled for removal?

  4. Recursive SP depth limit. METHOD_MAX_RECURSION_DEPTH is defined as 15 in sp_constants.hpp. Is this enforced by the C++ side, the Java side, or both? The Java ExecuteThread does not appear to check this directly.

  5. Transaction rollback on SP error. When a JavaSP throws an uncaught exception, ExecuteThread catches it, logs it, and sends sendError(). How does the C++ executor::response_invoke_command() translate this into a CUBRID transaction rollback? The callback loop must set an error condition on the thread that triggers rollback, but the exact path is not traced here.

  • src/sp/ — C/C++ PL server bridge (all files listed in references: above)
  • pl_engine/pl_server/src/main/java/com/cubrid/jsp/ — Java PL engine
  • pl_engine/pl_server/src/main/java/com/cubrid/jsp/classloader/ — JAR classloader hierarchy
  • pl_engine/AGENTS.md — Gradle build structure overview
  • references/cubrid/CLAUDE.md — CUBRID engine structure and build notes