frostlake

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 19, 2026 License: Apache-2.0 Imports: 16 Imported by: 0

README

frostlake-go

A pure-Go database/sql driver for Frostlake, speaking the engine's HTTP protocol against a running DatabaseHttpServer. No JVM, no cgo — stdlib only.

Engine version

Requires a Frostlake engine 0.0.7 or newer. Ask a running server which one it is with SELECT CURRENT_VERSION() — every release answers it, so the check works against any engine.

The driver versions independently of the engine: it speaks the HTTP protocol, not the jar, so this is a floor rather than a lockstep pin.

Usage

import (
    "database/sql"
    _ "github.com/Frostlake-DB/frostlake-go"
)

db, err := sql.Open("frostlake", "frostlake://localhost:18082/MY_DB?schema=PUBLIC")

Or skip the registry and parse the DSN once:

c, err := frostlake.NewConnector("frostlake://localhost:18082/MY_DB?schema=PUBLIC")
db := sql.OpenDB(c)
DSN
frostlake://host:port[/DATABASE][?param=value&…]
Parameter Meaning Default
schema schema to USE on every new session
role role to USE on every new session
warehouse warehouse to USE on every new session
timeout per-request timeout, as a Go duration; 0 disables it 5m
loc IANA zone the engine's zoneless timestamps belong to UTC
tls true to speak HTTPS — an https:// DSN does the same false

The role, warehouse, database and schema are applied as USE statements on the session before its first statement. An unknown parameter is an error rather than a silent no-op, and so is a username or password — the engine's HTTP API has no authentication to hand them to.

Semantics

  • Parameters are inlined client-side (the protocol has no server-side binding), with the same rules as Frostlake's JDBC driver: strings escape backslashes and quotes, []byte binds as a hex BINARY literal, time.Time as a TIMESTAMP_NTZ literal. A ? inside a string literal, quoted identifier, $$…$$ body or comment is never a placeholder. The argument count has to match the placeholder count — a placeholder left without an argument is an error, never a silently bound NULL.

  • Timestamps bind in the DSN's loc, UTC unless you say otherwise. TIMESTAMP_NTZ stores a wall clock, so the zone a time.Time carries has to be pinned somewhere; rendering every value in one zone is what keeps two time.Times naming the same instant from storing as two different timestamps, and it matches the zone they are read back in.

  • Named parameters: a statement may use positional ? or named :name placeholders — one style per statement, mixing them is an error. Named arguments are sql.Named values and bind by name, so their order does not matter. A :: cast, a := assignment and a :1 positional reference are never parameters.

  • Types: DATE, TIME and every TIMESTAMP variant scan as time.Time; BINARY as []byte; integral NUMBER columns as int64, fractional and floating as float64; BOOLEAN as bool; VARIANT/OBJECT/ARRAY as their JSON text in a string. An integer too wide for int64NUMBER(38,0) holds them — arrives as its exact digits in a string rather than a rounded float64. TIMESTAMP_LTZ/_TZ keep the offset the engine sends; the zoneless types are read in the DSN's loc.

  • Column metadata: sql.ColumnType reports the database type name, nullability, decimal precision and scale for numeric columns, and the Go type each column scans as.

  • Several statements in one request: A; B answers with one result set each. Rows.NextResultSet walks them, and RowsAffected adds up the DML counts.

  • Errors are typed. A failure the engine reported is a *frostlake.Error carrying the message, the statement as sent, and the HTTP status. A failure that never became an answer — refused connection, expired context, a proxy replying instead — is a *frostlake.TransportError, which unwraps to its cause.

    var ferr *frostlake.Error
    if errors.As(err, &ferr) {
        log.Printf("engine refused: %s", ferr.Message)
    }
    

    Error.Statement holds the rendered SQL. Because binding is client-side, that means every parameter inlined — a bound password or card number appears in it verbatim. The message from Error() carries none of it, so log Message freely and treat Statement as sensitive.

  • Transactions: Begin/Commit/Rollback ride the session's autocommit flag plus BEGIN/COMMIT/ROLLBACK statements, matching the JDBC transport. The engine offers read committed, so BeginTx accepts the default and sql.LevelReadCommitted and refuses any other level, along with ReadOnly — an option it cannot honour is refused rather than ignored. Commit rides the transaction's context; Rollback deliberately does not, so a transaction whose context was cancelled can still be ended rather than left open.

  • A broken connection is retired, never retried. When the transport fails — the host refuses, the socket dies, the answer is not a Frostlake response — the connection is marked unusable so the pool discards it, and the error reaches you with its diagnosis intact. The statement is not re-run on another connection: after a transport failure its fate is unknown, and re-running it would duplicate an INSERT. A cancelled context is not a broken connection and keeps the connection alive.

  • RowsAffected is derived from the engine's one-cell DML result (number of rows inserted / updated / deleted). LastInsertId returns an error — the engine has no generated row ids, and a zero would read as a real one.

  • Sessions and the pool: one HTTP session per driver.Conn, so database/sql's pool maps pooled connections to engine sessions 1:1. A statement that moves the session's scope — USE, the SET family, ALTER SESSION, and CREATE/DROP of a DATABASE or SCHEMA — is therefore scoped to one connection, not to the pool. Every statement in a request is examined, so a USE riding behind a leading SELECT counts too. On its way back to the pool that connection is put back on the DSN's scope; if the DSN named none, it is retired instead, so the next query cannot inherit a scope it never asked for. A connection that has sat idle long enough that its engine session may have been reclaimed has its scope re-established rather than assumed.

    Name the scope in the DSN, or hold a single sql.Conn, rather than issuing USE against a sql.DB. Two routes still move the scope without the driver seeing it — EXECUTE IMMEDIATE of a USE, and a procedure that switches scope when CALLed — so prefer the DSN when it matters.

Known limitations

  • Server sessions are not released on close. The HTTP API has no endpoint for ending a session, so a closed connection's session lingers until the engine's own 30-minute idle sweep reclaims it. Connection churn therefore accrues server-side sessions.
  • A session idle past that sweep silently resumes at the server's default scope, because the engine re-creates an expired session under the same id — nothing in the answer tells a client its session was reclaimed. The driver covers this by re-establishing the DSN's scope on a connection that has been idle for more than five minutes, but anything else the session held (session variables, an ALTER SESSION setting) is gone. Keep SetConnMaxIdleTime below the engine's session timeout if connections may sit unused for long stretches.
  • Timestamps lose sub-millisecond precision in transit. The engine holds full nanoseconds, but the HTTP layer serialises milliseconds, so a time.Time round trip is millisecond-precise however fine the value bound.

Tests

The unit tests need nothing installed — no engine, no JVM:

go test ./...

The integration tests additionally boot a real server from an engine classpath, and skip themselves when FROSTLAKE_CLASSPATH is unset:

JAVA_HOME=~/.jdks/liberica-17.0.18 \
FROSTLAKE_CLASSPATH="<engine classes>:<dependency classpath>" \
go test ./...

License

Apache-2.0 — see LICENSE.

Documentation

Overview

Package frostlake provides a database/sql driver for Frostlake, speaking the engine's HTTP protocol (POST /api/execute against a running DatabaseHttpServer).

DSN format:

frostlake://host:port[/DATABASE][?param=value&…]

Example:

db, err := sql.Open("frostlake", "frostlake://localhost:18082/MY_DB?schema=PUBLIC")

Parameters:

schema     schema to USE on every new session
role       role to USE on every new session
warehouse  warehouse to USE on every new session
timeout    per-request timeout as a Go duration (default 5m; 0 disables it)
loc        IANA zone the engine's zoneless timestamps belong to (default UTC)
tls        true to speak HTTPS; an https:// DSN does the same

Statement parameters are inlined client-side (there is no server-side binding), exactly like Frostlake's JDBC driver: strings are quoted with backslash and quote escaping, []byte binds as a hex BINARY literal, time.Time as a TIMESTAMP_NTZ literal in loc. Both positional (?) and named (:name) placeholders are supported, one style per statement.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func NewConnector

func NewConnector(dsn string) (driver.Connector, error)

NewConnector parses a DSN up front and returns a connector for sql.OpenDB, which skips both the driver registry and re-parsing the DSN per connection.

c, err := frostlake.NewConnector("frostlake://localhost:18082/MY_DB?schema=PUBLIC")
db := sql.OpenDB(c)

Types

type Driver

type Driver struct{}

Driver implements database/sql/driver.Driver for Frostlake.

func (*Driver) Open

func (d *Driver) Open(dsn string) (driver.Conn, error)

Open opens a new connection to a Frostlake HTTP server.

func (*Driver) OpenConnector

func (d *Driver) OpenConnector(dsn string) (driver.Connector, error)

OpenConnector implements driver.DriverContext.

type Error

type Error struct {
	// Message is the engine's own wording, unmodified.
	Message string
	// Statement is the SQL as it was sent, after client-side parameter substitution.
	Statement string
	// Status is the HTTP status the answer arrived with. Statement failures are reported
	// with 200; 500 means the engine threw while handling the request.
	Status int
}

Error is the failure the engine itself reported: the statement compiled badly, referenced something that does not exist, or failed while running. Callers that need to tell a bad statement from a bad network reach for this with errors.As.

var ferr *frostlake.Error
if errors.As(err, &ferr) {
    log.Printf("engine refused %q: %s", ferr.Statement, ferr.Message)
}

func (*Error) Error

func (e *Error) Error() string

type TransportError

type TransportError struct {
	// Endpoint is the URL that was called.
	Endpoint string
	// Status is the HTTP status, or 0 when the request never completed.
	Status int
	// Body is a bounded excerpt of whatever came back, empty when nothing did.
	Body string
	// Err is the underlying error, if the failure came from the HTTP client.
	Err error
}

TransportError is a failure that never became an answer from the engine: the host refused the connection, the context expired, a proxy replied instead, or the body was not a Frostlake response at all. Err carries the underlying cause where there is one.

func (*TransportError) Error

func (e *TransportError) Error() string

func (*TransportError) Unwrap

func (e *TransportError) Unwrap() error

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL