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
8 changes: 8 additions & 0 deletions packages/core/src/flag/flag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,14 @@ export const Flag = {
get ALTIMATE_WORKSPACE() {
return truthy("ALTIMATE_WORKSPACE")
},
/**
* Workspace precedence escape hatch, set by `--integrations=local`. When on, the
* native warehouse tools serve every local connection themselves and nothing is
* redirected to the bound workspace's integration engine, for the whole session.
*/
get ALTIMATE_INTEGRATIONS_LOCAL() {
return process.env["ALTIMATE_INTEGRATIONS"]?.toLowerCase() === "local"
},
// altimate_change end

// Evaluated at access time (not module load) because tests, the CLI, and
Expand Down
221 changes: 180 additions & 41 deletions packages/opencode/src/altimate/native/connections/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,57 +43,178 @@ import { Telemetry } from "../../../telemetry"
/** Cached dbt adapter (lazily created on first use). */
let dbtAdapter: any | null | undefined = undefined

// altimate_change start — single-flight adapter creation.
// Two concurrent `warehouse`-less calls used to construct the adapter twice, and
// construction is expensive: it spawns a detached Python bridge, rebuilds the
// manifest and starts file watchers. Share one in-flight promise instead. The
// permanent negative cache (`dbtAdapter === null`) is unchanged — a project that
// becomes valid mid-session is still not retried.
let dbtAdapterInflight: Promise<any | null> | undefined

/** Test seams for the single-flight. Production leaves `readConfig` unset. */
export const dbtAdapterInternals: {
/** Replaces the dbt config read, so a test can hold an attempt open. */
readConfig?: () => Promise<unknown>
/** Adapter creation attempts since the last reset — what single-flight bounds. */
attempts: number
/** Cache writes since the last reset: only the attempt that owns the slot writes. */
writes: number
/** Whether an attempt is currently in flight. */
inflight: () => boolean
} = { attempts: 0, writes: 0, inflight: () => dbtAdapterInflight !== undefined }

/**
* Try to execute SQL via dbt's adapter (which uses profiles.yml for connection).
* Returns null if dbt is not available or not configured — caller should fall back
* to native driver.
*
* This is the preferred path when working in a dbt project: dbt already knows
* how to connect, so users don't need to configure a separate connection.
* Resolve the dbt adapter for this project, or null when there is no usable dbt
* project. Idempotent, single-flight, and permanently negative once it has failed.
*/
async function tryExecuteViaDbt(
sql: string,
limit?: number,
): Promise<SqlExecuteResult | null> {
// Only attempt dbt once — if it's not configured, don't retry on every query
if (dbtAdapter === null) return null

if (dbtAdapter === undefined) {
async function ensureDbtAdapter(): Promise<any | null> {
if (dbtAdapter !== undefined) return dbtAdapter
if (dbtAdapterInflight) return dbtAdapterInflight

// The slot is released only by the attempt that owns it: `resetDbtAdapter()` mid-flight
// lets a second attempt start, and the first one's settle must not clear the second's
// slot — that would hand the next caller a third adapter behind the second's back.
let mine: Promise<any | null> | undefined
// Likewise the cache: a superseded attempt returns its result to the callers that
// awaited it but must not publish it, or it would overwrite what the newer attempt
// stored. Every write goes through here.
const publish = (value: any | null): any | null => {
if (dbtAdapterInflight === mine) {
dbtAdapter = value
dbtAdapterInternals.writes += 1
}
return value
}
mine = (async () => {
dbtAdapterInternals.attempts += 1
try {
// Check if dbt config exists
const { read: readDbtConfig } = await import(
"../../../../../dbt-tools/src/config"
)
const dbtConfig = await readDbtConfig()
if (!dbtConfig) {
dbtAdapter = null
return null
}
const dbtConfig = dbtAdapterInternals.readConfig
? ((await dbtAdapterInternals.readConfig()) as Awaited<ReturnType<typeof readDbtConfig>>)
: await readDbtConfig()
if (!dbtConfig) return publish(null)

// Check if dbt_project.yml exists
const fs = await import("fs")
const path = await import("path")
if (
!fs.existsSync(path.join(dbtConfig.projectRoot, "dbt_project.yml"))
) {
dbtAdapter = null
return null
return publish(null)
}

// Create the adapter
const { create } = await import("../../../../../dbt-tools/src/adapter")
dbtAdapter = await create(dbtConfig)
return publish(await create(dbtConfig))
} catch {
// dbt-tools not available or config invalid — fall back to native
dbtAdapter = null
return null
return publish(null)
} finally {
if (dbtAdapterInflight === mine) dbtAdapterInflight = undefined
}
})()
dbtAdapterInflight = mine
return mine
}

/** Where a `warehouse`-less call would actually go. */
export type DefaultTarget =
| {
source: "dbt"
type?: string
/** Where execution actually lands if the dbt attempt yields nothing. `sql.execute`
* falls back to the registry not only when dbt is absent, but whenever
* `tryExecuteViaDbt` returns null — an unrecognised result shape, or any throw.
* A caller deciding anything about this call has to consider both targets. */
fallback?: { type: string; name: string }
}
| { source: "registry"; type: string; name: string }
| { source: "none" }

/**
* Resolve the target a call with no `warehouse` would reach, mirroring the resolution
* the handler for `op` performs itself — so a caller inspecting the target ahead of
* time cannot disagree with where execution actually lands.
*
* Only `sql.execute` consults dbt. `sql.explain` and `schema.inspect` are
* registry-only, and must stay that way: resolving them through dbt would drag
* adapter construction (Python bridge, manifest rebuild, file watchers) onto paths
* that never touch dbt today.
*
* For the dbt path the reported `type` is the project's adapter type, which is what
* decides *which* warehouse the profile reaches. It is left undefined when it cannot
* be established — the adapter coalesces an unknown type to the string "unknown", and
* the call can throw before initialisation completes.
*/
export async function resolveDefaultTarget(
op: "sql.execute" | "sql.explain" | "schema.inspect",
): Promise<DefaultTarget> {
if (op === "sql.execute") {
const adapter = await ensureDbtAdapter()
if (adapter) {
let type: string | undefined
try {
const reported = adapter.getAdapterType?.()
if (typeof reported === "string" && reported && reported.toLowerCase() !== "unknown") type = reported
} catch {
// Adapter not initialised far enough to answer; leave the type undetermined.
}
if (!type) type = await adapterTypeFromManifest()
const warehouses = Registry.list().warehouses
const fallback = warehouses.length > 0 ? { type: warehouses[0].type, name: warehouses[0].name } : undefined
return { source: "dbt", type, fallback }
}
}

const warehouses = Registry.list().warehouses
if (warehouses.length === 0) return { source: "none" }
return { source: "registry", type: warehouses[0].type, name: warehouses[0].name }
}

/** Fallback adapter type: the dbt manifest records it as `metadata.adapter_type`. */
async function adapterTypeFromManifest(): Promise<string | undefined> {
try {
const { read: readDbtConfig } = await import("../../../../../dbt-tools/src/config")
const dbtConfig = await readDbtConfig()
if (!dbtConfig) return undefined
const fs = await import("fs")
const path = await import("path")
const manifestPath = path.join(dbtConfig.projectRoot, "target", "manifest.json")
if (!fs.existsSync(manifestPath)) return undefined
const raw = JSON.parse(fs.readFileSync(manifestPath, "utf8"))
const adapter = String(raw?.metadata?.adapter_type ?? "").toLowerCase()
return adapter || undefined
} catch {
return undefined
}
}
// altimate_change end

/**
* Try to execute SQL via dbt's adapter (which uses profiles.yml for connection).
* Returns null if dbt is not available or not configured — caller should fall back
* to native driver.
*
* This is the preferred path when working in a dbt project: dbt already knows
* how to connect, so users don't need to configure a separate connection.
*/
async function tryExecuteViaDbt(
sql: string,
limit?: number,
): Promise<SqlExecuteResult | null> {
// altimate_change start — share the single-flight creation path with resolveDefaultTarget;
// execute on the adapter this call was handed, not on a global a reset may have moved.
const adapter = await ensureDbtAdapter()
if (!adapter) return null
// altimate_change end

try {
const raw = limit
? await dbtAdapter.immediatelyExecuteSQLWithLimit(sql, "", limit)
: await dbtAdapter.immediatelyExecuteSQL(sql, "")
? await adapter.immediatelyExecuteSQLWithLimit(sql, "", limit)
: await adapter.immediatelyExecuteSQL(sql, "")

// QueryExecutionResult has: { columnNames, columnTypes, data, rawSql, compiledSql }
// where data is Record<string, unknown>[] (array of row objects)
Expand Down Expand Up @@ -146,6 +267,11 @@ async function tryExecuteViaDbt(
/** Reset dbt adapter (for testing). */
export function resetDbtAdapter(): void {
dbtAdapter = undefined
// altimate_change — drop any in-flight creation too, or a test that resets mid-flight
// would still receive the previous adapter.
dbtAdapterInflight = undefined
Comment thread
coderabbitai[bot] marked this conversation as resolved.
dbtAdapterInternals.attempts = 0
dbtAdapterInternals.writes = 0
}

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -369,29 +495,42 @@ register("sql.execute", async (params: SqlExecuteParams): Promise<SqlExecuteResu
const startTime = Date.now()
const warehouseType = getWarehouseType(params.warehouse)
try {
// altimate_change start — resolve the fallback connection before the dbt attempt.
// `tryExecuteViaDbt` awaits, and the registry is mutable: re-reading it afterwards
// could pick a different connection than the one the caller's routing decision was
// computed against (a concurrent `warehouse.add` can change which name sorts first).
// Reading once here pins the fallback across the dbt await. It narrows, not
// closes, the decision/execution window: `Precedence.check()` reads the registry
// independently before this handler runs. The dbt-first ordering is unchanged.
const fallbackName = params.warehouse || Registry.list().warehouses[0]?.name

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MAJOR — time-of-check/time-of-use between the routing decision and the executed target

This pin, and the check at :497-501, close the window across the dbt await. But the routing decision was made earlier and elsewhere: Precedence.check()resolveDefaultTarget (register.ts:139-160) does its own Registry.list().warehouses[0] read from inside the tool body, and the handler then resolves the target again, independently. The await Dispatcher.call(...) boundary and the handler's own awaits are enough for a queued concurrent mutation to land in between, so the comment's claim that this makes the decided and executed connection "the same by construction" is stronger than what the pin actually does.

Concretely:

  • the guard sees an unserved DuckDB default; a concurrent warehouse.remove drops it; sql.explain or schema.inspect then picks the newly-first Snowflake connection and executes it locally, despite Snowflake being shadowed — unaudited execution on a served connection, the exact outcome this design exists to prevent;
  • for an explicit name, a concurrent warehouse.add can replace that name with a served type after check() read it. The handler pins the already-replaced type and sees no subsequent change, so this check cannot detect that window.

Note also that this pin exists only in register("sql.execute")sql.explain (:552-570) and schema.inspect (:678-691) have no equivalent guard at all.

Fix: make the decision and the target acquisition atomic — move the precedence check into the handler after it pins the target (passing sessionID through), or return a lease {name, canonicalType, generation} that handlers must revalidate. Apply it to all three ops, explicit names included.

Related, same seam: Precedence.check()'s await import("../native/connections/register") (precedence.ts:586-588) has no try/catch, and check() is called outside the surrounding try in all three tool bodies — so a throw there takes out sql_execute, sql_explain and schema_inspect together instead of failing open.

default-target.test.ts:123-151 does not prove its stated invariant: it calls the dispatcher directly, omitting the preceding precedence decision, which is where the race actually is.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partially addressed in 81d5e402f: the overclaiming comment is softened to what the pin guarantees, and check() now fails open with a stated reason on any internal throw (covers the uncaught lazy import ahead of all three tool try blocks). The pin extension to explain/inspect and the atomic-lease design are recorded residuals.

// Pinning the name is not enough on its own: the same name can be re-added against a
// different warehouse while this call is suspended, and the routing decision made for
// it is a function of that connection's canonical type. Pin the type too, so a
// replacement is caught rather than executed under a decision that never covered it.
const fallbackType = fallbackName ? Registry.canonicalType(Registry.getConfig(fallbackName)?.type) : undefined
// altimate_change end

// Strategy: try dbt adapter first (if in a dbt project), then fall back to native driver.
// dbt knows how to connect using profiles.yml — no separate connection config needed.
if (!params.warehouse) {
const dbtResult = await tryExecuteViaDbt(params.sql, params.limit)
if (dbtResult) return dbtResult
}

const warehouseName = params.warehouse
let result: SqlExecuteResult
if (!warehouseName) {
const warehouses = Registry.list().warehouses
if (warehouses.length === 0) {
throw new Error(
"No warehouse configured. Use warehouse.add, set ALTIMATE_CODE_CONN_* env vars, or configure a dbt profile.",
)
}
// Use the first warehouse as default
const connector = await Registry.get(warehouses[0].name)
result = await connector.execute(params.sql, params.limit)
} else {
const connector = await Registry.get(warehouseName)
result = await connector.execute(params.sql, params.limit)
if (!fallbackName) {
throw new Error(
"No warehouse configured. Use warehouse.add, set ALTIMATE_CODE_CONN_* env vars, or configure a dbt profile.",
)
}
// altimate_change start — refuse rather than execute under a stale decision.
if (Registry.canonicalType(Registry.getConfig(fallbackName)?.type) !== fallbackType) {
throw new Error(
`Connection "${fallbackName}" changed while this query was being prepared, so the routing decided for it no longer applies. Re-run the query.`,
)
}
// altimate_change end
const connector = await Registry.get(fallbackName)
const result: SqlExecuteResult = await connector.execute(params.sql, params.limit)
try {
Telemetry.track({
type: "warehouse_query",
Expand Down
23 changes: 23 additions & 0 deletions packages/opencode/src/altimate/native/connections/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,29 @@ const DRIVER_MAP: Record<string, string> = {
trino: "@altimateai/drivers/trino",
}

// altimate_change start — canonical driver identity for workspace precedence.
/**
* Collapse a `config.type` onto the canonical name of the driver that serves it, so
* callers reasoning about "which database is this really" cannot be fooled by an
* alias: `postgresql` and `postgres` are one driver, as are `mariadb`/`mysql`,
* `mssql`/`fabric`/`sqlserver`, and `mongo`/`mongodb`.
*
* Derived by inverting `DRIVER_MAP` rather than restating it, so a type added there
* cannot silently desync from everything keyed on driver identity. Returns null for a
* type no driver serves.
*
* `redshift` maps to its own driver and therefore stays distinct from `postgres`: a
* different service with different credentials and endpoints, where Postgres
* wire-compatibility is an implementation detail rather than an identity.
*/
export function canonicalType(type: string | undefined | null): string | null {
if (!type) return null
const driverPath = DRIVER_MAP[type.toLowerCase()]
if (!driverPath) return null
return driverPath.slice(driverPath.lastIndexOf("/") + 1)
}
// altimate_change end

async function createConnector(name: string, config: ConnectionConfig): Promise<Connector> {
const driverPath = DRIVER_MAP[config.type.toLowerCase()]
if (!driverPath) {
Expand Down
40 changes: 40 additions & 0 deletions packages/opencode/src/altimate/tools/input-validation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// altimate_change - new file
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
//
// Pre-flight input checks shared by `sql_explain` and `schema_inspect`. They run
// BEFORE workspace precedence is consulted there: a redirect must never forward
// malformed input to the engine tool, and an empty or placeholder warehouse name
// must not be read as "use the default" by the routing decision. `sql_execute` has
// its own guard order (hard deny, write prompt, then precedence) and does not use
// these; its warehouse resolution is unchanged.

/** Warehouse names that models produce by mistake: empty strings and unsubstituted
* placeholders. Both would otherwise fall through to unhelpful registry errors. */
export function validateWarehouseName(warehouse: string | undefined): string | null {
if (warehouse === undefined) return null
if (typeof warehouse !== "string") {
return "warehouse must be a string"
}
const trimmed = warehouse.trim()
if (trimmed.length === 0) {
return "warehouse is an empty string — omit the parameter to use the default warehouse, or pass a configured connection name"
}
if (/^[?$:@]/.test(trimmed)) {
return (
"warehouse name looks like an unsubstituted placeholder (" +
JSON.stringify(trimmed) +
"). Use `warehouse_list` to see configured warehouses."
)
}
return null
}

/** Table names get the same two checks; the schema tool has nothing to inspect otherwise. */
export function validateTableName(table: unknown): string | null {
if (typeof table !== "string" || table.trim().length === 0) {
return "table is required — pass a table name, optionally schema-qualified"
}
if (/^[?$:@]/.test(table.trim())) {
return "table name looks like an unsubstituted placeholder (" + JSON.stringify(table.trim()) + ")"
}
return null
}
Loading
Loading