denoland/deno · error · rusqlite::Error::SqliteFailure

SQLITE_CANTOPEN

SQLITE_CANTOPEN

Error message

unable to open database file: "{}" is a symlink

What it means

The node:sqlite compatibility layer (ext/node_sqlite) applies the same symlink-refusal hardening as ext/kv: refuse_reparse_point_components checks each path component with symlink_metadata before opening the database, and returns rusqlite's SQLITE_CANTOPEN with this message if any component is a symlink, so node:sqlite operates only on the real filesystem.

Source

Thrown at ext/node_sqlite/database.rs:110

  path.to_path_buf()
}

/// SQLite does not enforce `SQLITE_OPEN_NOFOLLOW` on Windows (its
/// `winFullPathname` never resolves reparse points), so reject symlinks and
/// junctions in every path component manually before opening.
#[cfg(windows)]
fn refuse_reparse_point_components(path: &Path) -> Result<(), rusqlite::Error> {
  let mut current = PathBuf::new();
  for component in path.components() {
    current.push(component);
    #[allow(
      clippy::disallowed_methods,
      reason = "node:sqlite operates on the real file system"
    )]
    match std::fs::symlink_metadata(&current) {
      Ok(metadata) if metadata.file_type().is_symlink() => {
        return Err(rusqlite::Error::SqliteFailure(
          rusqlite::ffi::Error::new(rusqlite::ffi::SQLITE_CANTOPEN),
          Some(format!(
            "unable to open database file: \"{}\" is a symlink",
            current.display()
          )),
        ));
      }
      Ok(_) => {}
      // Missing components are created (or rejected) by SQLite itself.
      Err(_) => break,
    }
  }
  Ok(())
}

/// Static mapping of JavaScript property names to SQLite limits.
/// Order matches SQLite limit constant values (0-10).
/// Keep in sync with LIMIT_NAMES in ext/node/polyfills/sqlite.ts.
const LIMIT_MAPPING: [(&str, Limit); NUM_LIMITS] = [

View on GitHub (pinned to 336da420f4)

Solutions

  1. Pass the fully resolved (realpath) database path to DatabaseSync instead of a symlinked one.
  2. Replace the symlink with a real file/directory or a bind mount.
  3. Create the database directly at its final location rather than linking to it.
  4. Run the same app under Node.js if symlinked DB paths are a hard requirement.

Example fix

// before
const db = new DatabaseSync("/var/app/current/db.sqlite"); // 'current' is a symlink
// after
import { realpathSync } from "node:fs";
const db = new DatabaseSync(realpathSync("/var/app/current/db.sqlite"));
Defensive patterns

Strategy: validation

Validate before calling

import { lstatSync, realpathSync } from "node:fs";
if (lstatSync(dbPath).isSymbolicLink()) {
  dbPath = realpathSync(dbPath);
}

Type guard

function isRealFile(p: string): boolean {
  const st = lstatSync(p, { throwIfNoEntry: false });
  return st !== undefined && !st.isSymbolicLink();
}

Try / catch

try {
  db = new DatabaseSync(dbPath);
} catch (e) {
  if (String(e).includes("is a symlink")) {
    db = new DatabaseSync(realpathSync(dbPath));
  } else throw e;
}

Prevention

When it happens

Trigger: new DatabaseSync(path) (node:sqlite) where path or an ancestor is a symlink — e.g. a symlinked database file or a symlinked data directory; also hit via sqlite3 CLI-style paths passed through the Node compat API.

Common situations: Node apps migrated to Deno where the DB lives behind a symlink (versioned db files, ln -s deployments, macOS /tmp, symlinked home dirs in containers).

Understand the failure class

Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.

Related errors


AI-assisted analysis of denoland/deno@336da420f4 (2026-09-11). Data as JSON: /api/errors/3694a29a78e204bb. Report an issue: GitHub.