Skip to content

Does tedious support localdb? #348

Open

Description

@beautifulcoder

Hello! I was in the middle of writing a migration script in node when I got this error:

error connecting: Error: getaddrinfo ENOTFOUND (localdb)\MSSQLLocalDB (localdb)\MSSQLLocalDB:1433

So, my question is, does this work with a localdb instance? My gut feeling tells me no but wanted to ask anyway.

Activity

  1. arthurschreiber commented on Dec 7, 2015

    @arthurschreiber
    Collaborator

    I guess you're talking about SQL Server Express LocalDB? If yes, this should be supported, as it's basically a slimmed down version of SQL Server. But no guarantees, as I never used that version before.

    Did connecting to your LocalDB instance work before?

  2. beautifulcoder commented on Dec 7, 2015

    @beautifulcoder
    Author

    I use it for testing, here is the info on it:

    C:\Dev\migratedb>sqllocaldb s
    LocalDB instance "MSSQLLocalDB" started.
    
    C:\Dev\migratedb>sqllocaldb i MSSQLLocalDB
    Name:               MSSQLLocalDB
    Version:            12.0.2000.8
    Shared name:
    Owner:              XXX
    Auto-create:        Yes
    State:              Running
    Last start time:    12/7/2015 17:20:36
    Instance pipe name: np:\\.\pipe\LOCALDB#5E134EF5\tsql\query
    

    I am using this to connect to it:

    var conn = new tds.Connection({
      host: '(localdb)\\MSSQLLocalDB'
    });
    
  3. arthurschreiber commented on Dec 8, 2015

    @arthurschreiber
    Collaborator

    Ah, ok, I now understand your issue.

    tedious currently does not know how to connect to a SQLServer instance through a named pipe. Nodejs supports named pipes on windows (only local named pipes), but correct handling for this was never implemented.

    Another issue is that from my understanding, native TDS drivers from microsoft will start your localdb instance if it's not already running. tedious will never support this.

    So overall, I stand corrected. As-is, tedious won't be able to connect to your LocalDB instance.

  4. beautifulcoder commented on Dec 8, 2015

    @beautifulcoder
    Author

    Ah, awesome to know. Sounds like TDS solves a specific problem which is great. I agree that named pipes are another issue altogether.

  5. duwoodaes commented on May 17, 2016

    @duwoodaes

    Thanks for the information posted here. I too am looking to implement a solution where I would need to connect to localdb.

  6. rcarubbi commented on Mar 10, 2018

    @rcarubbi

    Did anyone solve this question with another approach?

  7. fowl2 commented on Oct 26, 2026

    @fowl2

    For anyone landing here, tedious can't resolve (localdb)\Instance itself, but it can connect to LocalDB if you hand it the instance's named pipe via the connector option (tedious ≥ 16.1.0). This works with mssql too, which is the easier route if that's what you use.

    With mssql (Windows only, SqlLocalDB.exe must be on PATH; top-level await shown for brevity):

    const sql = require("mssql");
    const { execFile } = require("node:child_process");
    const { promisify } = require("node:util");
    const net = require("node:net");
    
    const run = promisify(execFile);
    
    // Starts the instance if needed and returns its named pipe, e.g. \\.\pipe\LOCALDB#5E134EF5\tsql\query
    async function localDbPipe(instance) {
      await run("SqlLocalDB.exe", ["start", instance]);
      const { stdout } = await run("SqlLocalDB.exe", ["info", instance]);
      const match = /Instance pipe name:\s*np:(\S+)/i.exec(stdout);
      if (!match) throw new Error(`LocalDB instance ${instance} not found or has no pipe name`);
      return match[1];
    }
    
    // Called by tedious for every new connection as (options, lookup, signal).
    const localDbConnector = (instance) => (_options, _lookup, signal) =>
      new Promise((resolve, reject) => {
        signal.throwIfAborted();
        signal.addEventListener("abort", () => reject(signal.reason), { once: true });
        localDbPipe(instance).then((pipe) => {
          if (!signal.aborted) resolve(net.connect({ path: pipe }));
        }, reject);
      });
    
    const pool = await new sql.ConnectionPool({
      server: "(localdb)", // exactly this: not "(localdb)\\MSSQLLocalDB" (see below)
      user: "…",
      password: "…",
      database: "master",
      options: {
        encrypt: false, // LocalDB doesn't negotiate encryption
        connector: localDbConnector("MSSQLLocalDB"),
      },
    }).connect();

    Things worth knowing:

    • Use server: "(localdb)", not "(localdb)\\MSSQLLocalDB", and don't use a Server=(localdb)\… connection string. mssql turns anything after the backslash into an instance name, which makes tedious look up a port through SQL Browser (LocalDB has none) before it ever calls the connector. Pass the instance name to the connector instead, as above.
    • You need a SQL login. tedious can't use Windows authentication. The LocalDB owner is a sysadmin, so you can create one with your Windows identity, for example (development only, since it makes the login a sysadmin):
      sqlcmd -S "(localdb)\MSSQLLocalDB" -E -Q "CREATE LOGIN dev WITH PASSWORD = '…', CHECK_POLICY = OFF; ALTER SERVER ROLE sysadmin ADD MEMBER dev"
      Any tool that can connect with Windows authentication (SSMS, Azure Data Studio, PowerShell's Invoke-Sqlcmd) works equally well. If you need Windows authentication in your app, use mssql/msnodesqlv8 instead.
    • encrypt: false is required, otherwise the connection fails with "Server did not negotiate encryption".
    • Look the pipe up for every connection, as above. The instance stops itself about 5 minutes after its last connection closes and comes back on a different pipe, so a remembered pipe name breaks with ENOENT \\.\pipe\LOCALDB#… after your app has been idle.
    Plain tedious (without mssql)
    const { Connection } = require("tedious");
    
    const connection = new Connection({
      server: "(localdb)\\MSSQLLocalDB", // required by tedious but not used when a connector is given
      authentication: { type: "default", options: { userName: "…", password: "…" } },
      options: {
        encrypt: false,
        database: "master",
        connector: localDbConnector("MSSQLLocalDB"), // from above
      },
    });
    connection.connect((err) => { /* … */ });

    Don't set options.instanceName: tedious would query SQL Browser before calling the connector. No port is needed.

    Why the connector looks like this
    • Looking up the pipe costs two process spawns (about 150 ms on a running instance). If your pool opens many connections at once you can share one in-flight lookup between them; avoid caching the result for longer, for the reason above.
    • SqlLocalDB start exits 0 even when the instance doesn't exist, hence the check on info's output.
    • The abort handling is deliberate. tedious passes an AbortSignal (the connectTimeout, or connection.close()) as the third argument. A connector that ignores it isn't covered by connectTimeout (a connector that never settled left connect() hanging, with no callback in my 20 s test). On the other hand, killing SqlLocalDB.exe mid-start (what passing { signal } to execFile does) left an orphaned sqlservr.exe for me, and later starts failed with "SQL Server process failed to start" until I killed that process manually. The connector above stops waiting but lets the start finish. With a 200–300 ms connectTimeout against a stopped instance it gave ETIMEOUT on time, the instance finished starting in the background, and the next connect worked.
    • Timing: the first start of a newly created instance took about 10 s; starting an existing stopped instance and connecting took about 2.5 s. The default 15 s connect timeout is enough, but a first run on a slow machine could get close.
    • Lifecycle: SqlLocalDB start launches a per-user sqlservr.exe that is not a child of your process and outlives it. It stayed up while a connection was open (I held one idle for 7 minutes), and stopped itself about 5 minutes after the last connection closed (measured: 304 s). SqlLocalDB.exe stop <instance> stops it explicitly.
    • Files: a CREATE DATABASE without FILENAME puts the files in the instance's InstanceDefaultDataPath, which was my user profile folder, not the instance's own folder.
    • Instances: SqlLocalDB.exe info lists them; SqlLocalDB.exe create MyInstance 17.0 creates one (re-running create on an existing instance is harmless). If several LocalDB versions are installed, the SqlLocalDB.exe first on PATH managed a newer-version instance fine in my test.
    • msnodesqlv8 connected to LocalDB with Windows authentication using a connection string such as Driver={ODBC Driver 17 for SQL Server};Server=(localdb)\MyInstance;Database=master;Trusted_Connection=yes.
    • tedious still requires a server string even with a connector (server and port properties are required when using the custom connector factory method #1541); mssql supplies one.
    What I tested

    tedious 20.3.8, mssql 12.7.11, Node 24.15.0, Windows, SQL authentication. The mssql example above ran as written against the default MSSQLLocalDB instance (SQL Server 2019) and against SQL Server 2025 LocalDB. Also checked: a 2 MB parameter and a 300 KB result through the pipe, a new connection after the instance stopped and restarted, and connectTimeout against a stopped instance. Not tested: Windows authentication through tedious, shared instances ((localdb)\.\Name), other tedious/mssql versions.

    Edited: removed an unnecessary port option and changed the abort handling (see above).

    drafted by an LLM, reviewed by me

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions