NusaDB

Documentation / Clients and protocol

Clients and protocol

NusaDB speaks its own wire protocol. This page covers what that means for integration, which drivers exist and how to use them, and how to connect securely.

Its own protocol, deliberately

NusaDB does not implement another database's wire format. A client built for a different engine sends a handshake the server does not recognise, gets no reply, and eventually times out. From the client's side that looks like a dead socket rather than a refusal. This is by design.

Keep two things separate when planning an integration:

  • The wire format is NusaDB's own. Tools for other databases cannot connect, and no configuration changes that.
  • The SQL dialect is broadly familiar. Queries written for other engines usually run unchanged once they arrive over a NusaDB connection.

So porting an application means changing how it connects, not rewriting its SQL.

Official clients

Every driver speaks the protocol directly with no native dependencies, binds parameters as $1, $2, ..., and returns typed values (an INT column arrives as the host language's integer, NUMERIC as a decimal, TIMESTAMPTZ as a datetime, JSON parsed).

ClientInstallNotes
nusadb-cliin the container image and every releaseinteractive shell and batch runner
Pythonpip install nusadbDB-API 2.0, with a SQLAlchemy dialect
Node.jsnpm install nusadbPromise-based, TypeScript typings
Gogo get github.com/nusadb/godatabase/sql driver, registered as nusadb
Rustcargo add nusadbsync and async (Tokio), connection pool, tls feature
Rubygem install nusadbnative client
PHPcomposer require nusadb/nusadbPDO-style API in pure PHP
Javacom.nusadb:nusadb-jdbc:0.1.0 (Maven Central)JDBC, pure Java; Hibernate dialects: nusadb-hibernate5, nusadb-hibernate6
.NET (ADO.NET)in the source treenot yet published to NuGet
Two names changed recently.

The shell is nusadb-cli and the bootstrap superuser is nusadb-root. Container images published before that change carry the older nusa-cli and nusa-root, so on an older image use those instead.

Python:

python
import nusadb

conn = nusadb.connect(host="127.0.0.1", port=5678, user="app", password="change-me",
                      database="nusadb")
cur = conn.cursor()
cur.execute("INSERT INTO customers (email, country) VALUES ($1, $2) RETURNING id",
            ["dewi@example.com", "ID"])
print(cur.fetchone())                       # (4,)
cur.execute("SELECT email FROM customers WHERE country = $1", ["ID"])
for (email,) in cur:
    print(email)
conn.close()

Node.js:

js
const { connect } = require('nusadb');

const conn = await connect({ host: '127.0.0.1', port: 5678, user: 'app',
                             password: 'change-me', database: 'nusadb' });
const res = await conn.query('SELECT id, email FROM customers WHERE country = $1', ['ID']);
console.log(res.rows);          // [[1, 'ana@example.com'], [2, 'budi@example.com']]
console.log(res.columnTypes);   // ['INT', 'TEXT']
await conn.close();

Go:

go
import (
    "database/sql"
    _ "github.com/nusadb/go"
)

db, err := sql.Open("nusadb", "nusadb://app:change-me@127.0.0.1:5678/nusadb")
var email string
err = db.QueryRow("SELECT email FROM customers WHERE id = $1", 1).Scan(&email)

Java:

java
// pom.xml: <dependency><groupId>com.nusadb</groupId>
//          <artifactId>nusadb-jdbc</artifactId><version>0.1.0</version></dependency>
import java.sql.*;

String url = "jdbc:nusadb://app:change-me@127.0.0.1:5678/nusadb";
try (Connection conn = DriverManager.getConnection(url);
     PreparedStatement ps = conn.prepareStatement("SELECT email FROM customers WHERE country = $1")) {
    ps.setString(1, "ID");
    try (ResultSet rs = ps.executeQuery()) {
        while (rs.next()) System.out.println(rs.getString("email"));
    }
}

Rust:

rust
use nusadb::Connection;

let mut conn = Connection::connect("nusadb://app:change-me@127.0.0.1:5678/nusadb")?;
let rows = conn.query_params("SELECT id, email FROM customers WHERE country = $1", &[&"ID"])?;

A prepared statement is what every driver does for you: the statement is parsed once and run with values for $1..$n, and the server caches the plan. The SQL spellings PREPARE / EXECUTE / DEALLOCATE also work on any connection, which is how nusadb-cli binds parameters.

Errors and retries

Every failed statement carries a five-character SQLSTATE, and the drivers expose it. Branch on the code, not on the message. Class 40 (40001, 40P01) means run the whole transaction again; 25P02 means the transaction is aborted and must be rolled back first; 42501 is a missing grant. The retry loop every write path needs is on the transactions page, and the full table of codes is in the SQL reference.

Connection settings

SettingDefaultNotes
Host and port127.0.0.1:5678one connection targets one database
Databasenusadbcross-database queries are refused; CREATE DATABASE makes another
Usernusadb-rootthe bootstrap superuser
Passwordfrom NUSADB_PASSWORD (shell) or the driver's optionrequired once the server lists any --auth-user
TLSoffon the server with a certificate and key, on the same port

TLS and authentication

TLS runs on the same port: the server offers it when a certificate and key are configured, and a plaintext client is then refused. Because there is no system trust store in play, the client is told what to trust.

shell
nusadb-cli --host db.internal:5678 --user app --tls --tls-ca /etc/nusadb/ca.crt
nusadb-cli --host 10.0.0.5:5678 --tls --tls-ca ca.crt --tls-domain db.internal
A self-signed certificate is not automatically its own trust anchor.

A certificate generated with the usual one-liner is marked as a certificate authority, and presenting it as a server certificate is rejected. Either create a separate CA and sign a server certificate with it, or generate a leaf certificate that is not marked as a CA.

Hostname verification is applied, and a mismatch reports which names the certificate covers. For mutual TLS, the server takes a client CA and then requires every client to present a certificate signed by it.

Authentication is SCRAM-SHA-256. Passwords are never sent, in either direction: the exchange proves the client knows the password without transmitting it, and it also proves the server knows the stored verifier. A wrong password and an unknown user return the same authentication failure, so the error cannot be used to discover which usernames exist. Who may connect is the server's --auth-user list; what they may do is decided by SQL roles and grants, described under configuration.

Notifications

LISTEN channel subscribes a connection; NOTIFY channel, 'payload' from any connection in the same database reaches every listener, at commit when issued inside a transaction. A listener receives notifications between its own statements, which is how the drivers surface them.

Bulk transfer

COPY moves rows in a single streaming exchange rather than a round trip per row, in both directions. The shell forms read from the command's standard input and write to its standard output, which makes them composable with ordinary pipelines. See getting started.

Connections and pooling

The server handles connections on a thread pool rather than one process per connection, so a moderate number of idle connections is cheap. The default cap is 25 concurrent connections and excess connections queue rather than fail; --reject-excess-connections turns the queue into an immediate 53300 so a pool can back off. Raise --max-connections on a larger host. An external pooler is optional rather than a prerequisite.

Writing your own client

The protocol is documented in full in the wire protocol reference: framing, the start-up and SCRAM exchange, simple and extended queries, COPY, cancellation, and the text form of every type.