github.com/hexacluster/gocubrid is the first pure Go driver for
CUBRID: zero cgo, zero dependencies beyond the
Go standard library. It implements the CUBRID CAS broker wire protocol
natively and ships two layers:
- the root package (
cubridsql): a thindatabase/sqldriver registered under the name"cubrid"; - the
cubridsubpackage: a native client exposing the full protocol surface: schema introspection, streaming LOBs, single round-trip batches, savepoints, stored-procedure OUT parameters, and isolation-level control.
The official CUBRID/cubrid-go driver is a cgo wrapper around the CCI C
client library; it needs the C library installed, ties your build to it,
and disables cross-compilation. This driver speaks the wire protocol
itself: CGO_ENABLED=0 builds, static binaries, and
context.Context-aware cancellation on every network operation.
go get github.com/hexacluster/gocubridRequires Go 1.24+. No C toolchain, no CCI library, no other modules.
package main
import (
"database/sql"
"fmt"
"log"
_ "github.com/hexacluster/gocubrid" // registers driver "cubrid"
)
func main() {
db, err := sql.Open("cubrid", "cubrid://dba:password@localhost:33000/demodb")
if err != nil {
log.Fatal(err)
}
defer db.Close()
rows, err := db.Query("SELECT code, name FROM athlete WHERE gender = ? LIMIT 5", "M")
if err != nil {
log.Fatal(err)
}
defer rows.Close()
for rows.Next() {
var code int64
var name string
if err := rows.Scan(&code, &name); err != nil {
log.Fatal(err)
}
fmt.Println(code, name)
}
if err := rows.Err(); err != nil {
log.Fatal(err)
}
}To skip DSN re-parsing per connection (or to build the configuration programmatically), use the connector:
import (
cubridsql "github.com/hexacluster/gocubrid"
"github.com/hexacluster/gocubrid/cubrid"
)
cfg, err := cubrid.ParseDSN("cubrid://dba:password@localhost:33000/demodb")
// ... or fill in a &cubrid.Config{...} by hand
db := sql.OpenDB(cubridsql.NewConnector(cfg))Server and broker errors pass through the adapter unwrapped: match them
with errors.As:
var cerr *cubrid.Error
if errors.As(err, &cerr) {
fmt.Println(cerr.Code, cerr.Message)
}NUMERIC columns scan into string by default (the exact server
rendering, no float rounding); cubrid.Numeric is the opt-in exact
decimal target, and binds back as a parameter:
var price cubrid.Numeric
err := db.QueryRow("SELECT price FROM item WHERE id = ?", 1).Scan(&price)Runnable, compile-checked versions of these flows live in
example_test.go.
cubrid://user:password@host:port/database?param=value&...
The user defaults to public, the port to 33000 (the broker port:
CUBRID clients connect to a broker, not the database server). Reserved
characters in the password must be percent-encoded. Parameters:
| parameter | default | meaning |
|---|---|---|
fetch_size |
100 |
rows pulled per FETCH round-trip |
connect_timeout |
none | dial + handshake budget per connection attempt, as a Go duration (5s, 500ms) |
altHosts |
none | comma-separated standby brokers (host or host:port, port defaults to 33000) tried in order after the primary; see HA failover |
ssl |
false |
upgrade the connection to TLS right after the plaintext greeting; the broker must run SSL=ON |
ssl_verify |
false |
verify the broker certificate against the system roots; see Limitations for why it defaults off |
Unknown parameters are rejected, not ignored.
cubrid://dba:pw@broker1:33000/demodb?altHosts=broker2:33000,broker3&connect_timeout=3s
Connect tries the primary first, then each alternative in order. Every
attempt is bounded by connect_timeout (5 s per attempt when unset, so a
dead primary cannot stall the sweep); if all hosts fail, the error
reports every attempted address.
The cubrid subpackage is the full-power client: use it when
database/sql's common denominator is too narrow. One *cubrid.Conn is
one socket and is not goroutine-safe; pooling is what the root
package and database/sql are for.
cfg, _ := cubrid.ParseDSN(dsn)
conn, err := cubrid.Connect(ctx, cfg)
defer conn.Close()Schema introspection: typed helpers over GET_SCHEMA_INFO:
Tables, Columns, PrimaryKey, ImportedKeys, ExportedKeys, and
Indexes (which merges the primary key's backing index, something no
broker version reports in its constraint rows). SchemaInfo exposes the
raw rows for everything else.
idxs, err := conn.Indexes(ctx, "athlete")
// []IndexInfo{ {Name: "pk_athlete_code", Unique: true, IsPrimary: true, Columns: ["code"]}, ... }LOB streaming: BLOB/CLOB locators with chunked (<=128 KiB) reads and
appends; ReadAt follows the io.ReaderAt convention. Locators are
transaction-scoped, so do LOB work with autocommit off. Methods take a
context.Context instead of implementing io.Reader/io.Writer
because every call is one or more broker round-trips.
blob, err := conn.NewBlob(ctx)
_, err = blob.Append(ctx, payload) // append-only writes
_, err = conn.Exec(ctx, "INSERT INTO t VALUES (?, ?)", id, blob)
// reading: SELECT returns *cubrid.Lob -> ReadAt / ReadAll / StringBatch execution: one round-trip for many statements; per-row errors
are isolated in BatchResult.Err and the server keeps executing past a
failed row.
stmt, _ := conn.Prepare(ctx, "INSERT INTO t VALUES (?, ?)")
results, err := stmt.ExecBatch(ctx, [][]any{{1, "a"}, {2, "b"}})
results, err = conn.ExecBatchSQL(ctx, []string{"CREATE TABLE ...", "INSERT ..."})Savepoints: conn.Savepoint(ctx, "sp1") /
conn.RollbackToSavepoint(ctx, "sp1") inside an explicit transaction.
Stored procedures: PrepareCall + RegisterOutParam +
OutValues for CALL sp(?) / ? = CALL fn(?) with scalar OUT/INOUT
parameters:
st, _ := conn.PrepareCall(ctx, "CALL inc_and_get(?)")
st.RegisterOutParam(1)
_, err = st.Exec(ctx, 41)
out := st.OutValues() // [42]Isolation and locking: SetIsolationLevel / IsolationLevel
(READ COMMITTED, REPEATABLE READ, SERIALIZABLE) and SetLockTimeout.
Through database/sql, pass the level in sql.TxOptions to BeginTx.
Collections: SET/MULTISET/SEQUENCE columns decode to []any, and a
[]any argument binds as a collection parameter.
The driver advertises broker protocol V12 and gates features at runtime on what the broker negotiates down to: no per-version code forks. Every release below is exercised live by the integration suite:
| CUBRID | broker protocol | notes |
|---|---|---|
| 9.3.9 | V6 | ENUM works; no TZ types, no JSON |
| 10.1.8 | V9 | + timezone types (TIMESTAMPTZ, DATETIMETZ, ...) |
| 10.2.18 | V10 | + JSON |
| 11.0.16 | V10 | same surface as 10.2 |
| 11.4.5 | V12 | primary target; SSL and Java SP verified here |
Capability gates: timezone types need >=10.0, JSON needs >=10.2; SSL brokers and Java stored procedures are verified on 11.x.
Result.LastInsertIdis unsupported: the protocol path used here does not return generated keys. RunSELECT LAST_INSERT_ID()on the same connection instead.- Named parameters are unsupported: CUBRID binds by
?ordinal only;sql.Namedarguments are rejected with a clear error. - Read-only transactions are rejected: CUBRID has no read-only
transaction mode;
BeginTxwithReadOnly: trueerrors. - LOB columns through database/sql materialize eagerly as
[]byteup to 1 MiB. Larger LOBs return an error directing you to the native API, which streams them in chunks instead of buffering. - RESULTSET-typed OUT parameters are unsupported (scalar OUT/INOUT work); they fail with a clear error.
ssl=trueencrypts but does not authenticate by default. This matches the CUBRID client ecosystem: the JDBC driver ships an accept-all trust manager and brokers serve self-signed certificates out of the box. Setssl_verify=trueonce your broker presents a CA-issued certificate.
The driver is verified in rings, from hermetic to hostile:
- Unit + golden frames (
make test): codec and protocol tests against byte-exact fake brokers, plus golden wire frames captured from all five live broker versions (testdata/frames/) replayed offline. No network needed; this is what CI runs. - Live integration matrix (
make integration/make matrix): the full suite against real brokers for every supported version, DSNs viaCUBRID_TEST_DSN_<ver>environment variables. - Soak ring (
make soak): race-enabled pool storm (64 goroutines), 100k-row streaming fetch, broker-restart recovery, and query-timeout storms. - Protocol fuzzing (
make fuzz): native Go fuzzing of every wire parser, corpora seeded from the golden frames; parsers must error cleanly, never panic or overallocate.
- Suman Michael michael@hexacluster.ai
Contributions are welcome. Please read CONTRIBUTING.md and agree to the Contributor License Agreement before submitting a pull request.
BSD-3-Clause; see LICENSE. Wire-protocol behavior was implemented with reference to the BSD-licensed CUBRID JDBC driver: facts only, no code copied; see NOTICE.
Patent rights are reserved by HexaCluster Corp.; see PATENTS.