Skip to main content
select and execute are the two core SQL commands. They split along read vs write: select returns rows, execute returns an affected-row count. Both forward arbitrary SQL with positional parameter binding.
select is in the default permission set, but execute is not — writes and raw arbitrary SQL must be granted explicitly (better-duck-tauri:allow-execute, or the better-duck-tauri:allow-raw-sql umbrella). And because DuckDB SELECT can reach the filesystem through table functions, treat select itself as filesystem-read-capable — see the select caveat.

select

Run a read query; resolve to an array of row objects.
  • Signature: select<T = Record<string, unknown>>(query: string, values?: unknown[]): Promise<T[]>.
  • values defaults to [] and binds positionally as $1, $2, ….
  • Each row is a JSON object keyed by column name; see the value mapping.
Always parameterize untrusted inputs with $1, $2, … rather than string-concatenating them into the SQL. Parameters are bound by the backend, not interpolated.

execute

Run a write or DDL statement; resolve to an ExecuteResult.
  • Signature: execute(query: string, values?: unknown[]): Promise<ExecuteResult>.
  • ExecuteResult is { rowsAffected: number }. Pure DDL (e.g. CREATE TABLE) reports 0.

Parameter binding

Both commands bind values as consecutive positional parameters. The mapping (JS → DuckDB):

How statement policy applies

Before the backend runs, select and execute pass through the init-time Policy:
  • execute is rejected under read_only(true) with denied by policy: read-only mode: ….
  • With deny_risky_statements(true), both reject a statement whose leading keyword is one of ATTACH / DETACH / INSTALL / LOAD / PRAGMA / COPY / SET / CALL / EXPORT / IMPORT.
deny_risky_statements only inspects the leading keyword. A read like SELECT * FROM read_csv_auto('/etc/passwd') passes it, because the leading keyword is SELECT. This is the central reason to also pin connections and run read-only for untrusted webviews — see Security overview.

Concurrency

Each command hops onto a blocking task (spawn_blocking) so DuckDB work never blocks the Tauri async runtime. The backend holds a cheap, cloneable Database handle (core) or an r2d2 pool (diesel) and opens a short-lived connection per operation, so independent calls do not serialize on each other beyond the registry lock. See Backends.

See also