Tedious 20.3.0 streams big SQL Server parameters and table rows from async sources

1 hour ago

Tedious 20.3.0 lets you stream a parameter into SQL Server. A varchar(max), nvarchar(max) or varbinary(max) value, or the rows of a table-valued parameter, can now come from an async iterable, such as a Node readable stream. Tedious reads the source while the request is being sent, so the value doesn't have to fit in memory.

Ask about this presentation

Chapters

  1. 0:00Stream big parameters
  2. 0:23Pass an async iterable
  3. 0:46Stream table-valued parameter rows
  4. 1:06When the source throws
  5. 1:32What a streamed source must promise
  6. 1:57Bulk load errors name the column
  7. 2:23Upgrade
Show transcript
  1. Tedious 20.3.0 lets you stream a parameter into SQL Server. A varchar(max), nvarchar(max) or varbinary(max) value, or the rows of a table-valued parameter, can now come from an async iterable, such as a Node readable stream. Tedious reads the source while the request is being sent, so the value doesn't have to fit in memory.

    Slide text
    Tedious
    v20.3.0

    Stream big parameters and table rows into SQL Server

    varchar(max) nvarchar(max) varbinary(max) table-valued parameter rows
  2. Here's what that looks like. You build the request the usual way, and where the value would go, you pass an async iterable of buffers or strings. A readable stream works. A streamed value has no known length, so Tedious declares it as the max form of the type, here varbinary(max). It works with execSql, with callProcedure, and with a prepared statement through execute.

    Slide text
    Tedious
    v20.3.0

    Pass an async iterable where the value goes

    const request = new Request('select @param', callback); // an async iterable of chunks, e.g. a Readable request.addParameter('param', TYPES.VarBinary, source); connection.execSql(request);
    Sent as varbinary(max). Works with execSql, callProcedure and execute.
  3. Table-valued parameters get the same treatment. The rows property can now be an async generator instead of an array. Each row is still an array of values, one for each declared column, and Tedious writes the rows as it reads them. The request it sends is byte for byte what the same rows would produce from an array.

    Slide text
    Tedious
    v20.3.0
    async function * rows() { for (let i = 0; i < rowCount; i++) { yield [i, `row ${i}`]; } } request.addParameter('tvp', TYPES.TVP, { columns: [ { name: 'a', type: TYPES.Int }, { name: 'b', type: TYPES.NVarChar, length: 50 } ], rows: rows() });

    Table rows can arrive one at a time

    Same bytes on the wire as the same rows in an array.

  4. A source can fail partway through. Say your generator yields twenty thousand bytes, then throws. The request ends with an InputError that names the parameter, and your original error is on its cause property. A bad row in a streamed table-valued parameter fails the same way, and the cause names the column and the row. The connection is still usable. The next request on it, a plain select of 42, comes back with 42.

    Slide text
    Tedious
    v20.3.0

    If the source throws, the request fails and the connection keeps working

    async function * failing() { yield randomBytes(20_000); throw cause; }
    The request's error
    InputError: Input parameter 'param' could not be validated
    err.cause === cause
    Next request, same connection
    select 42 returns 42
  5. A request that carries a streamed value can be executed only once. Each string chunk is encoded on its own, so a chunk can't end in the middle of a UTF-16 surrogate pair. Text that Node decoded from UTF-8 is always safe. A string you sliced by index might not be. And a buffer chunk of 8 kilobytes or more is sent by reference, so don't reuse or change it until the request has completed.

    Slide text
    Tedious
    v20.3.0

    What a streamed source has to promise

    Run once
    A request that carries a streamed value can be executed only once.
    Whole characters
    A string chunk can't end halfway through a UTF-16 surrogate pair. Text Node decoded from UTF-8 never does.
    Hands off buffers
    A Buffer chunk of 8 KB or more is sent by reference. Don't reuse or change it until the request completes.
  6. The fix in this release is for bulk loads. Until now, a value the column type couldn't serialize surfaced as the raw error, like this range error for a number that doesn't fit a decimal column. In 20.3.0, it's an InputError that names the column, with the original error kept as its cause. InputError is also exported from the package now, next to ConnectionError and RequestError, so you can check for it directly.

    Slide text
    Tedious
    v20.3.0

    A bulk load error now names the column

    Before
    RangeError: Value -3.4028234663852886e+38 is out of range for DECIMAL(7, 4).
    In 20.3.0
    InputError: Column 'value' could not be serialized cause: RangeError: Value -3.4028234663852886e+38 is out of range for DECIMAL(7, 4).

    InputError is now exported from tedious, next to ConnectionError and RequestError.

  7. To upgrade, run npm install tedious. Check one thing first. If your bulk load code tests for a range error, that check stops matching, because the error is now an InputError. Read its cause instead. Values you already pass as buffers or strings aren't streamed. Streaming only starts when you hand Tedious an async iterable. So in 20.3.0, you can stream big parameters and table rows into SQL Server.

    Slide text
    Tedious
    v20.3.0
    npm install tedious
    Check first: bulk load code that tests for RangeError should read err.cause.

    Buffers and strings you pass today aren't streamed.

    Stream big parameters and table rows into SQL Server

What happened in AI today, in your inbox

One short email each evening. Read it and you’re caught up.

Built in New York with ❤️