CUBRID PL/JavaSP — Java Stored Procedures, JDBC Bridge, and the PL/CSQL-Sibling External PL Engine
Contents:
- Relationship to the PL Family — What Is Shared and What Is JavaSP-Specific
- Theoretical Background
- CUBRID’s Approach
- Source Walkthrough
- Cross-check Notes
- Open Questions
- Sources
Relationship to the PL Family
Section titled “Relationship to the PL Family”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.
Shared infrastructure
Section titled “Shared infrastructure”| Layer | Shared 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) |
| Transport | pl_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 |
| Executor | pl_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 hosting | Server.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 |
JavaSP-specific
Section titled “JavaSP-specific”| Concern | Artefact |
|---|---|
| Reflective dispatch on user JARs | TargetMethod.java — resolves ClassName.methodName(argTypes) by reflection; StoredProcedure.invoke() calls Method.invoke() on the loaded class |
| Classloader hierarchy | classloader/ package — ClassLoaderManager, ContextClassLoader, SessionClassLoader, ServerClassLoader; user JARs are loaded from $CUBRID_DATABASES/<db>/java/ (dynamic) or java_static/ (static) paths |
| Security sandbox | SpSecurityManager.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 path | pl_sr_jvm.cpp — pl_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.
Theoretical Background
Section titled “Theoretical Background”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:
-
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. -
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.
-
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.
CUBRID’s Approach
Section titled “CUBRID’s Approach”Process topology
Section titled “Process topology”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()Discovery and rendezvous
Section titled “Discovery and rendezvous”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.
Wire protocol (SP_CODE / METHOD_REQUEST)
Section titled “Wire protocol (SP_CODE / METHOD_REQUEST)”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_CODESP_CODE_INVOKE = 0x01 // invoke a SPSP_CODE_RESULT = 0x02 // result from SP to server (callback)SP_CODE_ERROR = 0x04 // error from SP to serverSP_CODE_INTERNAL_JDBC = 0x08 // back-channel JDBC requestSP_CODE_DESTROY = 0x10 // session teardownSP_CODE_COMPILE = 0x80 // PL/CSQL compile requestSP_CODE_UTIL_BOOTSTRAP = 0xDD // bootstrap sysprmSP_CODE_UTIL_PING = 0xDE // liveness probeSP_CODE_UTIL_STATUS = 0xEE // status queryThe 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.cppsignature.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_PLCSQLtransaction_control = (lang == SP_LANG_PLCSQL) ? true : tc;The PL/CSQL language always gets transaction_control = true; JavaSP inherits
it from the SP’s definition.
Catalog rows
Section titled “Catalog rows”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.
C++ session and executor
Section titled “C++ session and executor”cubpl::session (in pl_session.hpp) is the per-CUBRID-session state object:
m_stack_map/m_exec_stack— execution stack tracking (oneexecution_stackper recursive SP call; each gets its own worker thread in thecub_plthread pool).m_cursor_map— server-side cursors opened by JDBC back-channel queries.m_session_connections— a deque ofconnection_viewobjects borrowed from the global pool for the duration of a call.m_session_params— per-session parameter shadow (DBMS_OUTPUTflag, etc.); synced tocub_plviaMETHOD_CALLBACK_SET_PL_SESSION_PARAM.
cubpl::executor (in pl_executor.hpp) drives one invocation:
fetch_args_peek()— reads argumentDB_VALUEs from the XASL value descriptor or a direct CALL statement arg list.request_invoke_command()— packs aninvoke_javaand sends it over the claimed connection.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 tocallback_prepare(),callback_execute(),callback_fetch(), etc., which call into the server’s query execution machinery and write results back tocub_plover 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()— throwsSecurityExceptionunless theServerinstance is already in shutdown state. This prevents user stored procedures from callingSystem.exit()and killing the JVM.checkLink()— inspects the classloader chain; if any frame belongs to aContextClassLoaderorSessionClassLoader, native library loading is blocked with a clear message pointing toloadjava -jni.- All other
check*()methods are permissive no-ops (blank overrides).
Server-side JDBC back-channel
Section titled “Server-side JDBC back-channel”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.
Startup FSM
Section titled “Startup FSM”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-forkIn 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.
Source Walkthrough
Section titled “Source Walkthrough”C++ side (src/sp/)
Section titled “C++ side (src/sp/)”| Symbol | File | Role |
|---|---|---|
pl_server_init | pl_sr.cpp | Entry point called by boot_sr.c at server boot; creates server_manager, forks cub_pl |
pl_server_destroy | pl_sr.cpp | Called at server shutdown; deletes server_manager |
pl_server_wait_for_ready | pl_sr.cpp | Called after init to block until cub_pl is accepting connections |
get_connection_pool | pl_sr.cpp | Returns the global PL_CONNECTION_POOL; used by executor to claim connections |
pl_server_port_from_info | pl_sr.cpp | Reads $CUBRID/var/pl_<db>.info via pl_read_info() |
server_manager | pl_sr.cpp | Owns pool + monitor task; start/stop lifecycle |
server_monitor_task | pl_sr.cpp | Daemon task: forks cub_pl, pings, bootstraps, detects crashes |
bootstrap_request | pl_sr.cpp | Packable: sends system-parameter snapshot (SP_CODE_UTIL_BOOTSTRAP) |
connection_pool | pl_connection.hpp | Fixed-size pool of connection objects; epoch-based invalidation on restart |
connection | pl_connection.hpp | Wraps a SOCKET; send_buffer_args, receive_buffer, auto-reconnect |
pl_connect_server | pl_comm.c | Low-level socket connect (UDS or TCP) |
pl_writen / pl_readn | pl_comm.c | Reliable write/read helpers (handle EINTR) |
SP_CODE enum | pl_comm.h | Wire protocol codes shared with Java RequestCode |
executor | pl_executor.hpp | One-invocation driver: fetch args → send invoke → handle callbacks → return result |
invoke_java | pl_executor.cpp | Packable invocation payload (signature, lang, args, result type) |
session | pl_session.hpp | Per-CUBRID-session: stacks, cursors, connections, params, interrupt |
sys_param | pl_session.hpp | Packable system parameter (DB prm or PL-specific prm) |
pl_signature | pl_signature.hpp | Resolved SP descriptor: type, name, auth, arg modes/types, ext (target class/method or code OID) |
sp_info | sp_catalog.hpp | C++ representation of _db_stored_procedure row |
sp_arg_info | sp_catalog.hpp | C++ representation of _db_stored_procedure_args row |
sp_code_info | sp_catalog.hpp | C++ representation of _db_stored_procedure_code row |
sp_add_stored_procedure | sp_catalog.cpp | Writes a new SP row at CREATE time |
PL_SERVER_INFO | pl_file.h | {pid, port} struct read/written via pl_read_info / pl_write_info |
jsp_create_stored_procedure | jsp_cl.cpp | Client-side CREATE PROCEDURE/FUNCTION handler |
jsp_make_pl_signature | jsp_cl.cpp | Builds pl_signature from a PT_NODE |
Java side (pl_engine/pl_server/)
Section titled “Java side (pl_engine/pl_server/)”| Symbol | File | Role |
|---|---|---|
Server | Server.java | Entry point; installs SpSecurityManager, sets up socket, starts ListenerThread |
ListenerThread | ListenerThread.java | ServerSocket.accept() loop; creates ExecuteThread per connection; exponential backoff on error |
ExecuteThread | ExecuteThread.java | Per-connection thread; dispatches on Header.code; processStoredProcedure() / processCompile() |
processStoredProcedure | ExecuteThread.java | Reads args, builds StoredProcedure, calls invoke(), sends result |
TargetMethod | TargetMethod.java | Resolves Class.getMethod(name, argTypes) by reflection from a Signature |
TargetMethod.argClassMap | TargetMethod.java | Static map: type-name string → Class<?> (primitives, boxed, SQL types, arrays) |
TargetMethod.getMethod | TargetMethod.java | Returns Method for invocation; throws NoSuchMethodException with full signature in message |
SpSecurityManager | SpSecurityManager.java | Custom SecurityManager: blocks exit unless shutting down; blocks native lib load from user classloaders |
SpSecurityManager.checkLink | SpSecurityManager.java | Inspects getClassContext() chain; rejects loadLibrary from ContextClassLoader/SessionClassLoader |
ClassLoaderManager | classloader/ClassLoaderManager.java | Root/static/dynamic path management; last-modified tracking for hot JAR reload |
ClassLoaderManager.getDynamicPath | classloader/ClassLoaderManager.java | Returns $db_path/java/ (user-uploaded JARs via loadjava) |
ClassLoaderManager.getStaticPath | classloader/ClassLoaderManager.java | Returns $db_path/java_static/ (server-wide JARs) |
Position hints (as of 2026-04-30)
Section titled “Position hints (as of 2026-04-30)”| Symbol | File | Approx. line |
|---|---|---|
pl_server_init | src/sp/pl_sr.cpp | 696 |
server_manager::start | src/sp/pl_sr.cpp | 262 |
server_monitor_task::do_monitor | src/sp/pl_sr.cpp | 356 |
server_monitor_task::do_bootstrap_request | src/sp/pl_sr.cpp | 625 |
pl_server_port_from_info | src/sp/pl_sr.cpp | 763 |
SP_CODE enum | src/sp/pl_comm.h | 44 |
connection_pool class | src/sp/pl_connection.hpp | 64 |
executor::execute | src/sp/pl_executor.cpp | (in executor class body) |
invoke_java::invoke_java | src/sp/pl_executor.cpp | 45 |
session class | src/sp/pl_session.hpp | 106 |
pl_signature struct | src/sp/pl_signature.hpp | 86 |
SP_LANG_JAVA, SP_LANG_PLCSQL | src/sp/sp_constants.hpp | 147 |
SP_CLASS_NAME macro | src/sp/sp_constants.hpp | 22 |
sp_info struct | src/sp/sp_catalog.hpp | 115 |
PL_SERVER_INFO struct | src/sp/pl_file.h | 35 |
Server constructor | pl_engine/.../Server.java | 70 |
ListenerThread.run | pl_engine/.../ListenerThread.java | 63 |
ExecuteThread.run | pl_engine/.../ExecuteThread.java | 129 |
ExecuteThread.processStoredProcedure | pl_engine/.../ExecuteThread.java | 319 |
TargetMethod.getMethod | pl_engine/.../TargetMethod.java | 243 |
SpSecurityManager.checkLink | pl_engine/.../SpSecurityManager.java | 72 |
ClassLoaderManager.getDynamicPath | pl_engine/.../classloader/ClassLoaderManager.java | 58 |
Cross-check Notes
Section titled “Cross-check Notes”-
pl_sr_jvm.cppis a legacy remnant.pl_start_jvm_server()andpl_server_port()represent the old in-process JNI path where the JVM was embedded intocub_serverdirectly. These symbols still compile but the active code path uses theserver_managerfork/exec model inpl_sr.cpp. The JNI path may be dead code or kept for specific build configurations. -
SpSecurityManageris deprecated in Java 17+.SecurityManagerwas 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 = 10is hard-coded. There is no system parameter governing the pool size. High concurrency with many simultaneous SP calls will queue at the pool. Theis_system_poolflag distinguishes the monitor’s own system connection pool from the main pool. -
TargetMethod.argClassMapcovers 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) throwsClassNotFoundExceptionat dispatch time. The comment inclassFor()acknowledges this gap with aTODOpointing to a futureClassAccess.getClass()implementation. -
The info file race window. Between
fork()returning in the monitor task andcub_plwriting its port to the info file there is a window wherepl_server_port_from_info()returnsPL_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.
Open Questions
Section titled “Open Questions”-
Graceful upgrade of
SpSecurityManagerto Java 17+. No replacement mechanism is visible in the current source. What is the plan when JDK 17+ becomes the target baseline? -
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? -
pl_sr_jvm.cppfate. Is the in-process JNI path still compiled into any product configuration, or is it entirely dead and scheduled for removal? -
Recursive SP depth limit.
METHOD_MAX_RECURSION_DEPTHis defined as15insp_constants.hpp. Is this enforced by the C++ side, the Java side, or both? The JavaExecuteThreaddoes not appear to check this directly. -
Transaction rollback on SP error. When a JavaSP throws an uncaught exception,
ExecuteThreadcatches it, logs it, and sendssendError(). 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.
Sources
Section titled “Sources”src/sp/— C/C++ PL server bridge (all files listed inreferences:above)pl_engine/pl_server/src/main/java/com/cubrid/jsp/— Java PL enginepl_engine/pl_server/src/main/java/com/cubrid/jsp/classloader/— JAR classloader hierarchypl_engine/AGENTS.md— Gradle build structure overviewreferences/cubrid/CLAUDE.md— CUBRID engine structure and build notes