API reference

Frostlake — Module
Frostlake

A driver for Frostlake, speaking the engine's HTTP protocol against a running DatabaseHttpServer.

using Frostlake

conn = Connection("frostlake://localhost:18082/MY_DB?schema=PUBLIC")
execute(conn, "CREATE TABLE people (id INTEGER, name VARCHAR)")
execute(conn, "INSERT INTO people VALUES (?, ?)", [1, "Ada"])
result = execute(conn, "SELECT name FROM people WHERE id = ?", [1])
scalar(result)  # "Ada"
close(conn)

Nothing outside the standard library is needed: the JSON reader, the DSN parser and the SQL scanner are all in the package, and the transport is Downloads.

source

Connecting

Frostlake.Connection — Type
Connection

A connection to a Frostlake HTTP server, and the engine session behind it.

Statements are serialized: a statement holds the connection until it has an answer, so session state — USE, session variables, an open transaction — carries from one statement to the next, which is exactly what a half-interleaved second statement would break.

The engine can lose the session: it expires after 30 idle minutes, and a restart ends them all. A statement that finds its session gone is sent once more on a fresh session with the DSN's scope, unless the lost session held an open transaction or context of the caller's own — then it throws SessionLostError instead.

Build one with Connection(dsn) and release it with close, which also releases the engine session.

source
Frostlake.connect — Function
Connection(dsn; kwargs...) -> Connection
Connection(f, dsn; kwargs...)

Opens a connection to a Frostlake HTTP server. Frostlake.connect is the same function under the name other drivers use; it is not exported, because connect is Sockets' name too.

conn = Connection("frostlake://localhost:18082/MY_DB?schema=PUBLIC")

The server is contacted before this returns: its health endpoint is called, and the database, schema, role and warehouse the DSN names are selected. A name that does not exist is therefore reported here, rather than surfacing later on whichever query happened to run first.

Given a function, the connection is passed to it and closed afterwards:

Connection("frostlake://localhost:18082") do conn
    execute(conn, "SELECT 1")
end

Every keyword may also be given in the DSN query string, where an explicit keyword outranks it. Timeouts are seconds, or a fixed-length Dates.Period such as Minute(10); 0 removes the bound.

KeywordMeaning
timeouthow long one statement may take
connect_timeouthow long to wait for the socket
idle_limitagainst an engine before 0.1.0, how long a connection may idle before its scope is re-applied
cacerta CA bundle for HTTPS, instead of the system's
verify_certificatefalse to accept any HTTPS certificate
source
Frostlake.ping — Function
ping(conn)

Checks that a Frostlake engine is answering, via GET /api/health.

A 200 on its own only says something is listening — anything can serve that. The health payload is what says it is an engine, so a body that is not one is reported rather than passed off as healthy.

source
Frostlake.apply_dsn_scope — Function
apply_dsn_scope(conn)

Selects the database, schema, role and warehouse the DSN names. connect calls this, so it is only worth calling again after the session has been moved somewhere else deliberately.

source
Frostlake.base_url — Function

The base URL of the server, without a trailing slash.

source

The server this connection speaks to, as scheme://host:port.

source

Statements

Frostlake.execute — Function
execute(conn, sql; multi_statement_count=nothing) -> Result
execute(conn, sql, parameters; multi_statement_count=nothing) -> Result

Runs one statement and returns its first result set.

parameters fill the statement's placeholders: a vector or tuple for positional ? markers, a dictionary or named tuple for :name markers. The count has to match exactly whenever arguments are supplied; with none at all the markers pass through to the server, where Snowflake Scripting binds them (OPEN c USING (...), EXECUTE IMMEDIATE :v).

execute(conn, "INSERT INTO people VALUES (?, ?)", [1, "Ada"])
execute(conn, "SELECT :a + :b AS total", (a=2, b=40))

multi_statement_count says how many statements this one request carries; see execute_all.

A string holding several ;-separated statements answers with the first one's result — use execute_all for the rest.

source
Frostlake.execute_all — Function
execute_all(conn, sql; multi_statement_count=nothing) -> Vector{Result}
execute_all(conn, sql, parameters; multi_statement_count=nothing) -> Vector{Result}

Runs a statement string and returns every result set it produced, in order. A single statement gives a one-element vector.

The engine refuses a request holding more statements than it was told to expect, so a pack says how many it holds:

execute_all(conn, "SELECT 1; SELECT 2"; multi_statement_count=2)

The count travels with this one request. It outranks the session's MULTI_STATEMENT_COUNT without changing it, so there is nothing to put back afterwards, and 0 allows any number. Left out, no count is sent at all and the session's value decides.

source

Transactions

Frostlake.transaction — Function
transaction(f, conn)

Runs f(conn) inside BEGIN ... COMMIT, rolling back if it throws and re-raising the original error either way.

transaction(conn) do c
    execute(c, "INSERT INTO acc VALUES (1)")
end

The connection is not held for the duration: a transaction lives on the session, so anything else run on this same connection meanwhile joins the transaction. Give a transaction its own connection if that is not what you want.

source

Results

Frostlake.Result — Type
Result

One result set: what a single statement answered with.

FieldWhat it holds
columnsa ColumnInfo per column, in order
valuesevery cell, positionally aligned with columns — the lossless view
updatecountrows affected by DML, or -1 when the statement returned data
countersthe raw number of rows ... counters behind updatecount

A Result iterates and indexes over its rows, so for row in result and result[1][2] work directly, and rows gives the same rows keyed by column name.

A DML statement answers with a status grid rather than data — one row of number of rows inserted-style counters — and that grid is reported here as it arrived. updatecount is the friendly reading of it.

source
Frostlake.ColumnInfo — Type
ColumnInfo

What the engine reported about one column of a result set: its name as the engine cased it, the declared datatype (NUMBER, VARCHAR, TIMESTAMP_NTZ, ...), whether it is nullable, the precision and scale of the fixed-point numerics, and the length of the text and binary ones.

length counts characters for a VARCHAR and bytes for a BINARY, and is what the account's own driver reports as such a column's precision and its display size: VARCHAR(9) is 9. A table column declared without a width carries 16777216 characters or 8388608 bytes; a cast without one, such as 'abc'::VARCHAR, reports 134217728 characters or 67108864 bytes, and a string literal its own length. Only those two families carry it; every other type leaves it nothing.

nullable, precision, scale and length are nothing when the server did not say — an engine that predates a field reports nothing rather than false or 0.

source
Frostlake.rows — Function
rows(result) -> Vector{Dict{String,Any}}

Each row keyed by column name, built on first use and kept.

A dictionary cannot represent two columns called the same thing — a self-join reports ID twice and the later one wins — so result.values stays the lossless view.

source
Frostlake.scalar — Function
scalar(result) -> Any

The first cell of the first row, or nothing when there is none — what a single-value query (SELECT COUNT(*), SELECT CURRENT_VERSION()) is after.

source
Frostlake.columnindex — Function
columnindex(result, name) -> Int

The position of a column, matched exactly first and case-insensitively after, or 0 when the result has no such column.

source

Values

Frostlake.ZonedTimestamp — Type
ZonedTimestamp

A TIMESTAMP_TZ / TIMESTAMP_LTZ value: a wall clock together with the offset it was written at.

Julia's standard library has no zoned type — a DateTime is a wall clock with nowhere to keep an offset — and folding the offset in would lose it, so the two halves are kept side by side. utc(z) gives the instant, which is what to compare two of these by; z.datetime gives the local reading, which is what the engine prints.

z = ZonedTimestamp(DateTime(2024, 1, 15, 10, 30), Dates.Second(3600))
utc(z)  # 2024-01-15T09:30:00
source
Frostlake.utc — Function
utc(z::ZonedTimestamp) -> DateTime

The instant z names, as a UTC wall clock. Two ZonedTimestamps written at different offsets are the same moment when their utc agree.

source

Errors

Frostlake.FrostlakeError — Type
FrostlakeError

Supertype of every failure this driver raises, so a single

try
    execute(conn, sql)
catch e
    e isa FrostlakeError || rethrow()
end

catches the lot. The subtypes say which kind it was.

source
Frostlake.QueryError — Type
QueryError <: FrostlakeError

The engine rejected a statement: it compiled badly, referenced something that does not exist, or failed while running. message is the engine's own wording, unmodified.

`statement` is sensitive

Binding is client-side, so statement holds the SQL after parameter substitution — a bound password or card number appears in it verbatim. message and the printed form of the error carry none of it, so log those freely and treat statement as sensitive.

source
Frostlake.ConnectionError — Type
ConnectionError <: FrostlakeError

The server could not be reached, the request died mid-flight, or what came back was not a Frostlake response at all.

A statement that failed this way has an unknown fate: the request may have arrived and run before the connection broke, so it must not be blindly retried — re-running an INSERT would duplicate it.

Fields: message, endpoint (the URL called, when the failure names one), status (the HTTP status, or nothing when the request never completed) and cause (the underlying transport error, when there was one).

source
Frostlake.SessionLostError — Type
SessionLostError <: FrostlakeError

The engine no longer holds the connection's session — it expired, was released, or the server restarted — and the statement did not run.

A lost session is replaced without a word when nothing went with it: the DSN's scope goes onto a fresh session and the statement is sent once more. This is thrown instead when the lost session held something a fresh one cannot reproduce — an open transaction, or context set up with USE, SET, ALTER SESSION or a temporary object — because re-running the statement would put it somewhere its author did not intend.

The connection stays usable: the next statement starts a fresh session on the DSN's scope. Whatever the unit of work had done in the lost session is gone, so it is the unit of work that has to start over.

statement is the SQL that did not run, after parameter substitution — as sensitive as QueryError.statement.

source
Frostlake.UsageError — Type
UsageError <: FrostlakeError

The driver was asked for something impossible and never sent a request: a malformed DSN, a closed connection, a bind value with no SQL equivalent, an argument count that does not match the placeholders.

source