Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions docs/docs/configure/warehouses.md
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,26 @@ If you're already authenticated via `gcloud`, omit `credentials_path`:
| Field | Required | Description |
|-------|----------|-------------|
| `path` | No | Database file path. Omit or use `":memory:"` for in-memory |
Comment thread
anandgupta42 marked this conversation as resolved.
| `create` | No | Create the database file if it is missing (default: `false`) |

!!! warning "The store must already exist"
Connecting never creates the database. If the file is missing, the connection
fails with an error naming the path it looked for — an empty database would
otherwise answer every query with no rows and no error, which reads as a
successful query against an empty warehouse. Set `"create": true` only when
you intend this store to be created.

!!! note "How a relative `path` is resolved"
A relative `path` is resolved once, when the config is loaded, against the
directory that declares it:

- `~/.altimate-code/connections.json` → resolved against `~/.altimate-code`
- `<project>/.altimate-code/connections.json` → resolved against `<project>`
- `ALTIMATE_CODE_CONN_*` environment variables → resolved against the project root

It is never resolved against the current working directory, so `--dir` cannot
re-point an existing connection at a different file. Absolute paths are always
safest.

!!! note "Concurrent access"
DuckDB does not support concurrent write access to the same file. If another process holds a write lock, Altimate Code automatically retries the connection in **read-only** mode so you can still query the data. A clear error message is shown if read-only access also fails.
Expand Down Expand Up @@ -443,10 +463,16 @@ If you're already authenticated via `gcloud`, omit `credentials_path`:
|-------|----------|-------------|
| `path` | No | Database file path. Omit or use `":memory:"` for in-memory |
| `readonly` | No | Open in read-only mode (default: `false`) |
| `create` | No | Create the database file if it is missing (default: `false`) |

!!! note
SQLite uses Bun's built-in `bun:sqlite` driver. WAL journal mode is enabled automatically for writable databases.

!!! warning "The store must already exist"
As with DuckDB, connecting never creates the database, and a relative `path`
resolves against the directory of the config that declares it — not the current
working directory. See the DuckDB section above for the full rules.

## SQL Server

```json
Expand Down
5 changes: 5 additions & 0 deletions docs/docs/drivers.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,11 @@ MongoDB supports server versions 3.6 through 8.0. Queries use MQL (MongoDB Query
|--------|--------------|
| File | `path: "./my-database.sqlite"` |

For both file-backed drivers, the database must already exist — connecting never
creates it. Pass `create: true` to create it deliberately. A relative `path`
resolves against the directory of the config that declares it, not the current
working directory. See [Warehouses](configure/warehouses.md#duckdb) for the full rules.

## SSH Tunneling

Connect through a bastion host by adding SSH config to any connection:
Expand Down
8 changes: 7 additions & 1 deletion packages/drivers/src/duckdb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* DuckDB driver using the `duckdb` package.
*/

import { assertStoreExists, requireStorePath } from "./file-store"
import type { ConnectionConfig, Connector, ConnectorResult, ExecuteOptions, SchemaColumn } from "./types"
import { loadOptionalDriver } from "./resolve"

Expand Down Expand Up @@ -55,7 +56,9 @@ export async function connect(config: ConnectionConfig): Promise<Connector> {
duckdb = await loadOptionalDriver("duckdb", "duckdb")
duckdb = duckdb.default || duckdb

const dbPath = (config.path as string) ?? ":memory:"
// altimate_change start — a missing path must fail loudly, not become :memory:
const dbPath = requireStorePath(config, "DuckDB")
Comment thread
anandgupta42 marked this conversation as resolved.
// altimate_change end
// altimate_change start — configurable open budget
const { ms: openTimeoutMs, source: openTimeoutSource } = resolveOpenTimeoutMs(config)
// altimate_change end
Expand Down Expand Up @@ -118,6 +121,9 @@ export async function connect(config: ConnectionConfig): Promise<Connector> {

return {
async connect() {
// altimate_change start — never conjure an empty store on open
assertStoreExists(config, dbPath, "DuckDB")
Comment thread
anandgupta42 marked this conversation as resolved.
Comment thread
anandgupta42 marked this conversation as resolved.
// altimate_change end
// altimate_change start — retry with read-only on lock errors
const tryConnect = (accessMode?: string): Promise<any> =>
new Promise<any>((resolve, reject) => {
Expand Down
96 changes: 96 additions & 0 deletions packages/drivers/src/file-store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/**
* Shared guards for file-backed drivers (DuckDB, SQLite).
*
* Both engines create an empty database when asked to open a file that does
* not exist. For a warehouse connection that is never what the caller wants:
* a mistyped or mis-resolved path then yields a working connector over an
* empty database, so every query succeeds and returns nothing. An agent handed
* that result reports "no tables" instead of an error.
*
* Opening a store is therefore read-or-fail by default. Creation is opt-in via
* `create: true`, which the tools that deliberately materialize a local store
* (local test scratch databases, schema sync targets) pass explicitly.
*/

import * as fs from "fs"
import type { ConnectionConfig } from "./types"

/**
* Whether `dbPath` names a file on the local filesystem, and so can be
* existence-checked before the driver opens it.
*
* Only the exact string `:memory:` is an in-memory database. Both engines
* treat `:memory:named` — and any other colon-prefixed name — as an ordinary
* (if oddly named) file: DuckDB really does write a file called
* `:memory:named`, so those must stay inside the guard. An empty path is
* DuckDB's in-memory database and SQLite's anonymous temporary one; neither
* touches disk.
*
* A scheme-qualified target is not a local file: MotherDuck (`md:`), object
* storage (`s3://`), DuckLake, and any other scheme a DuckDB extension
* provides. Those are left to the driver, which reports an unknown scheme as a
* missing-extension error rather than silently creating anything. The pattern
* requires two or more characters before the colon so a Windows drive letter
* (`C:\data\wh.duckdb`) stays a path.
*/
export function isLocalFilePath(dbPath: string): boolean {
if (dbPath === "" || dbPath === ":memory:") return false
if (/^[a-zA-Z][a-zA-Z0-9+.-]+:/.test(dbPath)) return false
Comment thread
anandgupta42 marked this conversation as resolved.
Comment thread
anandgupta42 marked this conversation as resolved.
return true
}

/**
* The store path a file-backed connection names, or a loud failure.
*
* Both drivers used to read `(config.path as string) ?? ":memory:"`. That turns
* ANY failure to carry a path — a config the registry never loaded, a field
* under the wrong name, a lookup that fell through — into a successful
* connection over an empty in-memory database. Every query then returns no rows
* and no error, which reads as a healthy warehouse that happens to be empty.
*
* It is a worse failure than creating a store on disk: a stray file can at
* least be found afterwards, whereas an in-memory database leaves nothing
* behind to explain the empty answer. `:memory:` remains available, but only
* when a caller asks for it by name.
*/
export function requireStorePath(config: ConnectionConfig, engine: string): string {
const value = config.path
if (typeof value === "string" && value !== "") return value
throw new Error(
Comment thread
anandgupta42 marked this conversation as resolved.
`${engine} connection is missing its "path". A file-backed warehouse must name its database explicitly — ` +
`falling back to an in-memory database would answer every query with no rows and no error, ` +
`which is indistinguishable from a healthy but empty warehouse. ` +
`Set "path" to the database file, or to ":memory:" if a throwaway empty database is genuinely what you want.`,
)
}

/** Whether the caller explicitly opted in to creating the store. */
export function allowsCreate(config: ConnectionConfig): boolean {
return config.create === true
}

/**
* Throw unless the store is safe to open: it already exists, the caller opted
* in to creating it, or the path is not a local file at all.
*
* @param engine Human-readable engine name used in the error message.
* @param allowCreate Whether this open will actually create the store. Defaults
* to the config's `create` flag; a driver passes it explicitly when its own
* options can veto creation — SQLite never creates a read-only connection.
*/
export function assertStoreExists(
config: ConnectionConfig,
dbPath: string,
engine: string,
allowCreate: boolean = allowsCreate(config),
): void {
if (allowCreate) return
if (!isLocalFilePath(dbPath)) return
if (fs.existsSync(dbPath)) return
Comment thread
anandgupta42 marked this conversation as resolved.
Comment thread
anandgupta42 marked this conversation as resolved.
throw new Error(
`${engine} database file not found: "${dbPath}". ` +
`Opening a warehouse connection never creates the database — an empty store would answer every query with no rows. ` +
`Check the "path" in your connection config (relative paths resolve against the config file's directory, not the current directory), ` +
`or pass "create": true if this store is meant to be created.`,
)
}
3 changes: 3 additions & 0 deletions packages/drivers/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ export type { Connector, ConnectorResult, SchemaColumn, ConnectionConfig } from
// Re-export config normalization
export { normalizeConfig, sanitizeConnectionString } from "./normalize"

// Re-export file-backed store guards
export { allowsCreate, assertStoreExists, isLocalFilePath, requireStorePath } from "./file-store"

// Re-export driver connect functions
export { connect as connectPostgres } from "./postgres"
export { connect as connectSnowflake } from "./snowflake"
Expand Down
17 changes: 15 additions & 2 deletions packages/drivers/src/sqlite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,32 @@
*/

import { Database } from "bun:sqlite"
import { allowsCreate, assertStoreExists, requireStorePath } from "./file-store"
import type { ConnectionConfig, Connector, ConnectorResult, ExecuteOptions, SchemaColumn } from "./types"

export async function connect(config: ConnectionConfig): Promise<Connector> {
const dbPath = (config.path as string) ?? ":memory:"
// altimate_change start — a missing path must fail loudly, not become :memory:
const dbPath = requireStorePath(config, "SQLite")
// altimate_change end
let db: Database | null = null

return {
async connect() {
const isReadonly = config.readonly === true
// altimate_change start — never conjure an empty store on open.
// A read-only connection never creates, so `create: true` cannot excuse a
// missing file there; the guard is told the effective decision.
const willCreate = !isReadonly && allowsCreate(config)
assertStoreExists(config, dbPath, "SQLite", willCreate)
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
db = new Database(dbPath, {
readonly: isReadonly,
create: !isReadonly,
// `create` alone is no longer implied by "not readonly", so the
// read-write flag has to be explicit — bun:sqlite rejects an options
// object that sets no open flag at all.
readwrite: !isReadonly,
create: willCreate,
Comment thread
anandgupta42 marked this conversation as resolved.
})
// altimate_change end
if (!isReadonly) {
db.exec("PRAGMA journal_mode = WAL")
}
Expand Down
23 changes: 17 additions & 6 deletions packages/drivers/test/driver-security.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ describe("DuckDB driver", () => {
}))

const { connect } = await import("../src/duckdb")
const connector = await connect({ type: "duckdb", path: "/tmp/test.duckdb" })
const connector = await connect({ type: "duckdb", path: "/tmp/test.duckdb", create: true })
await connector.connect()
expect(connectAttempts).toBe(2) // First failed, second succeeded in READ_ONLY
// The retry must specifically request READ_ONLY — two attempts alone don't
Expand Down Expand Up @@ -219,7 +219,11 @@ describe("DuckDB driver", () => {
}))

const { connect } = await import("../src/duckdb")
const connector = await connect({ type: "duckdb", path: "/tmp/test.duckdb" })
// altimate_change start — bypass the store-existence guard for this mock:
// it never touches the real filesystem, and the test targets lock-error
// classification, not the existence check.
const connector = await connect({ type: "duckdb", path: "/tmp/test.duckdb", create: true })
// altimate_change end

try {
await connector.connect()
Expand Down Expand Up @@ -267,7 +271,10 @@ describe("DuckDB driver", () => {
}))

const { connect } = await import("../src/duckdb")
const connector = await connect({ type: "duckdb", path: "/tmp/test.duckdb" })
// altimate_change start — bypass the store-existence guard for this mock;
// see the identical note above.
const connector = await connect({ type: "duckdb", path: "/tmp/test.duckdb", create: true })
// altimate_change end
await connector.connect()

expect(attempts).toBe(2)
Expand Down Expand Up @@ -306,7 +313,11 @@ describe("DuckDB driver", () => {
}))

const { connect } = await import("../src/duckdb")
const connector = await connect({ type: "duckdb", path: "/tmp/test.duckdb", readonly: true })
// altimate_change start — bypass the store-existence guard for this mock;
// see the identical note above. `create` and `readonly` are independent
// flags: `create` only controls the existence check, not access mode.
const connector = await connect({ type: "duckdb", path: "/tmp/test.duckdb", readonly: true, create: true })
// altimate_change end

try {
await connector.connect()
Expand Down Expand Up @@ -405,7 +416,7 @@ describe("DuckDB driver", () => {
}))

const { connect } = await import("../src/duckdb")
const connector = await connect({ type: "duckdb", path: "/tmp/sync-lock.duckdb" })
const connector = await connect({ type: "duckdb", path: "/tmp/sync-lock.duckdb", create: true })
await connector.connect()
expect(connectAttempts).toBe(2)
expect(await connector.execute("SELECT 1")).toMatchObject({ columns: ["ok"], rows: [[1]], row_count: 1 })
Expand All @@ -430,7 +441,7 @@ describe("DuckDB driver", () => {
}))

const { connect } = await import("../src/duckdb")
const connector = await connect({ type: "duckdb", path: "/tmp/corrupt.duckdb" })
const connector = await connect({ type: "duckdb", path: "/tmp/corrupt.duckdb", create: true })
await expect(connector.connect()).rejects.toThrow("catalog is corrupt")
})
})
Expand Down
Loading
Loading