Documentation
¶
Overview ¶
Package sqldriver implements a database/sql driver for the FoundationDB Record Layer relational (SQL) layer.
Importing this package registers the "fdbsql" driver. Register a domain, then open a connection:
import (
"database/sql"
"fdb.dev/pkg/relational/sqldriver"
)
sqldriver.RegisterDomainIfNotExists("FRL")
db, err := sql.Open("fdbsql", "fdbsql:///FRL/MYDB?cluster_file=/etc/foundationdb/fdb.cluster")
A database path is /DOMAIN/DATABASE, and its domain must be registered first, once per process, exactly as a Java program registers it with RelationalKeyspaceProvider.instance().registerDomainIfNotExists. The driver registers none itself (Java's engine neither); Java's server, CLI and yaml-test runner use FRL. /__SYS, the system catalog, needs no domain.
DSN shape mirrors Java's JDBC URI (minus the jdbc: prefix):
fdbsql:///PATH — embedded, default cluster file fdbsql:///PATH?cluster_file=/path — embedded, explicit cluster file fdbsql://HOST:PORT/PATH — remote (gRPC) — NOT YET IMPLEMENTED
This is the public entry point. Internally it wraps pkg/relational/core which implements the SQL engine over FDB.
The port follows Java's fdb-relational-* modules 1:1 wherever reasonable; database/sql compatibility is the single intentional deviation — the Go-idiomatic driver surface is at the edge, the Java surface (pkg/relational/api.Connection etc.) is preserved underneath.
Index ¶
Constants ¶
const ( DryRunParam = "dry_run" IsolationLevelSnapshotParam = "isolation_level_snapshot" )
DryRunParam and IsolationLevelSnapshotParam set api.OptDryRun and api.OptIsolationLevelSnapshot on every connection the DSN opens, parsed like the other booleans: a connection whose DML previews and stores nothing, and one whose SELECTs read at snapshot isolation (and which refuses everything else), as Java's connection options DRY_RUN and ISOLATION_LEVEL_SNAPSHOT.
const DriverName = "fdbsql"
DriverName is the database/sql driver name.
const PlannerStatisticsParam = "planner_statistics"
PlannerStatisticsParam is the DSN query parameter that sets api.OptPlannerStatistics (RFC-236) on every connection the DSN opens. Spelled as the lower-cased option name, like its sibling above, so the DSN parameter and the option stay obviously the same knob.
It belongs here — decided before the first statement — because it is part of the plan-cache key: a connection that changes its mind mid-session would otherwise share cache entries with one that did not.
const RestrictDDLToSessionDatabaseParam = "restrict_ddl_to_session_database"
RestrictDDLToSessionDatabaseParam is the DSN query parameter that sets api.OptRestrictDDLToSessionDatabase on every connection the DSN opens. It is spelled as the lower-cased option name so the DSN parameter and the option stay obviously the same knob.
const Scheme = "fdbsql"
Scheme is the DSN scheme accepted by this driver. It matches Java's JDBC scheme (minus the "jdbc:" prefix, which Go's database/sql does not use).
const TransactionTagsParam = "transaction_tags"
TransactionTagsParam is the DSN query parameter that sets api.OptTransactionTags on every connection the DSN opens. Multiple tags are comma-separated: `?transaction_tags=tenant-a,bulk`.
Variables ¶
This section is empty.
Functions ¶
func EnableStoreTimer ¶
func EnableStoreTimer(clusterFile string) *recordlayer.StoreTimer
EnableStoreTimer arms record-layer instrumentation for the given cluster_file key and returns the StoreTimer that collects it. Idempotent: repeat calls for the same key return the same timer, so an operator never has to reason about who called first.
This is the inversion the SQL path needs. The driver opens the record-layer database itself and caches it privately (fdbDBCache), so a tenant service that speaks only database/sql has no *recordlayer.FDBDatabase to call SetTimer on — and record-layer counters were, in consequence, unobservable from SQL. Arming by key instead of by handle works before the lazy Connect that opens the database:
timer := sqldriver.EnableStoreTimer("/etc/foundationdb/fdb.cluster")
http.Handle("/metrics/recordlayer", rlmetrics.Handler(timer))
db, _ := sql.Open("fdbsql", "fdbsql:///T/1?cluster_file=/etc/foundationdb/fdb.cluster")
SCOPE — and this is the part that decides what the numbers mean. One timer per cluster-file key means one timer per process, aggregating EVERY tenant, connection and transaction that runs against that cluster. It is deliberately not per-tenant: tenant count in a SaaS deployment is unbounded and operator-driven, so a tenant label would make the cardinality of every record-layer metric grow with the customer list — the classic way to take down a Prometheus. Per-tenant attribution is already available at a layer that can afford it, because it is sampled and log-shaped rather than a live time series: PlanGenerationLogger and ExecutionStatsLogger are installed per connection borrow (ResetSession removes them) and close over the tenant ID (see docs/mt-saas.md §4).
A caller who genuinely wants finer scope has it without a label: build the *recordlayer.FDBDatabase, call SetTimer on it, and RegisterBackend it under a key of its own. That keeps the cardinality decision explicit and in the caller's hands.
func RegisterBackend ¶
func RegisterBackend(key string, db *recordlayer.FDBDatabase) (unregister func())
RegisterBackend associates an already-built FDBDatabase with a cluster_file key, so a DSN of the form "fdbsql:///FRL/DB?cluster_file=<key>" drives the full SQL engine (parser → Cascades → executor → record layer) over that backend instead of opening a real cluster. It returns a func that unregisters the key.
This is the seam for deterministic-simulation harnesses that back the SQL stack with SimFDB (RFC-199 DST): register a SimFDB-backed FDBDatabase, open a `fdbsql` DSN against its key, and the whole relational layer runs in-process with no Docker. It only exposes the cache population that connect() already performs internally — production behavior is unchanged.
func RegisterDomainIfNotExists ¶
func RegisterDomainIfNotExists(name string)
RegisterDomainIfNotExists makes /name/DATABASE a valid database path for every connection of the process: Java's RelationalKeyspaceProvider.instance().registerDomainIfNotExists. Idempotent and additive; a path under an unregistered domain is INVALID_PATH where Java resolves it (CREATE DATABASE, a schema's store, connecting to a schema).
Types ¶
type Connector ¶
type Connector struct {
// contains filtered or unexported fields
}
Connector holds a parsed DSN and produces connections on demand. The FDB database, keyspace, and factory are initialised lazily by the first Connect that succeeds in doing so; a failed initialization is retried by the next Connect. The catalog Bootstrap (Initialize) is deferred further — it runs inside the first DDL transaction, not at Connect time.
func (*Connector) Connect ¶
Connect opens a connection. Honors ctx.Done() for cancellation, also while it waits for the connector's initialization, which ctx does not cut short (initFlight).
func (*Connector) DSN ¶
DSN returns a COPY of the parsed DSN. Exposed for diagnostics.
The copy is the point: the Connector's DSN is an immutable snapshot taken at OpenConnector, and handing out the internal pointer would make it mutable again through this accessor — the only route by which anything outside this package can reach it. Mutating the returned value affects nothing; to change a connection's configuration, open a new Connector with a new DSN string.
func (*Connector) StoredQueryWarmUp ¶
func (c *Connector) StoredQueryWarmUp() embedded.StoredQueryWarmUpCounts
StoredQueryWarmUp returns what the connector's start planned into its plan cache: Java's OFFLINE_STORED_QUERIES_* counts. Zero before the first connection.
type DSN ¶
type DSN struct {
// Mode selects embedded vs. remote.
Mode Mode
// Path is the database path (corresponds to Java's
// RelationalConnection.getPath()). Always starts with "/".
Path string
// Schema is the initial schema name set on the connection.
// Corresponds to the ?schema= query option.
Schema string
// Host is the gRPC host:port for remote mode. Empty for embedded.
Host string
// Options are raw query-string options. Empty values are kept as "".
Options map[string]string
}
DSN is a parsed connection string.
Accepted forms:
fdbsql:///PATH — embedded, default cluster file fdbsql:///PATH?cluster_file=/path — embedded, explicit cluster file fdbsql:///PATH?max_rows=100&dry_run=true — connection options (acceptedDSNParams) fdbsql://HOST:PORT/PATH — remote (gRPC), NOT YET IMPLEMENTED
The path component is the database path (Java's RelationalConnection.getPath()) and is mandatory.
func ParseDSN ¶
ParseDSN parses a DSN string into a DSN.
Returns a relational Error with code InvalidPath if the DSN is malformed or uses an unsupported scheme. Matches Java's behavior (JDBCRelationalDriver.acceptsURL + connect). A malformed query string is InvalidParameter, like every other bad parameter.
func (*DSN) Clone ¶
Clone returns a deep copy of d, including its Options map. Callers may mutate the result without affecting the original.
This is what makes a Connector's DSN a snapshot rather than a shared handle. Every field a Connector reads after construction — Path, Schema, Mode, Host and the Options map that carries cluster_file — must come from the same frozen value as the connection options decoded at OpenConnector time. Anything else is a split brain: the security option validated and frozen up front while the fields around it stay live and mutable, so a mutation between OpenConnector and the first Connect would be honoured by some reads and ignored by others.
func (*DSN) ConnectionOptions ¶
ConnectionOptions converts the DSN's query parameters into the api.Options installed on each connection.
An UNKNOWN parameter is an error naming it and listing the accepted ones, where it used to be ignored: a misspelled `dry_run` or `restrict_ddl_to_session_database` must not silently leave a connection writable or unrestricted. ParseDSN stays a parser and keeps every key.
type Driver ¶
type Driver struct{}
Driver is the database/sql/driver.Driver for fdbsql.
Implements driver.Driver and driver.DriverContext.
func (*Driver) Open ¶
Open satisfies driver.Driver. Prefer OpenConnector (via driver.DriverContext) for lazy connection pooling.
func (*Driver) OpenConnector ¶
OpenConnector parses the DSN and returns a lazy Connector. Parsing errors are reported here so misconfigured DSNs surface at sql.Open time, not at first query.
Connection options are decoded here too, and the decoded value is kept on the Connector. Deferring the decode to Connect would break the contract this doc-comment states: Connect opens FDB before it would ever look at the options, so a misspelled option value would surface as a cluster-connection failure instead of the DSN error it is — misleading exactly when the operator needs a clear message, and a security-relevant option that never took effect.
The DSN is FROZEN here: the Connector keeps a private deep clone, and every later read — Path, Schema, Mode, cluster_file, and the decoded options — comes from that one snapshot. Freezing only the decoded options while the rest of the struct stayed live would be a split brain, and the security-relevant direction is the bad one: a caller holding the *DSN could flip restrict_ddl_to_session_database after OpenConnector, have Connect honour the stale decode (restriction silently not what the DSN now says), and slip a newly-malformed value past validation entirely.