@ 261105.16
current
// Flags: --expose-gc
'use strict';
const { skipIfSQLiteMissing } = require('../common');
skipIfSQLiteMissing();
const {
  Database,
  Statement,
} = require('node:sqlite');
const { suite, test } = require('node:test');

suite('Statement() constructor', () => {
  test('Statement cannot be constructed directly', (t) => {
    t.assert.throws(() => {
      new Statement();
    }, {
      code: 'ERR_ILLEGAL_CONSTRUCTOR',
      message: /Illegal constructor/,
    });
  });
});

suite('Statement.prototype.get()', () => {
  test('executes a query and returns undefined on no results', (t) => {
    using db = new Database(':memory:');
    let stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)');
    t.assert.strictEqual(stmt.get(), undefined);
    stmt = db.prepare('SELECT * FROM storage');
    t.assert.strictEqual(stmt.get(), undefined);
  });

  test('executes a query and returns the first result', (t) => {
    using db = new Database(':memory:');
    let stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)');
    t.assert.strictEqual(stmt.get(), undefined);
    stmt = db.prepare('INSERT INTO storage (key, val) VALUES (?, ?)');
    t.assert.strictEqual(stmt.get('key1', 'val1'), undefined);
    t.assert.strictEqual(stmt.get('key2', 'val2'), undefined);
    stmt = db.prepare('SELECT * FROM storage ORDER BY key');
    t.assert.deepStrictEqual(stmt.get(), { __proto__: null, key: 'key1', val: 'val1' });
  });

  test('executes a query that returns special columns', (t) => {
    using db = new Database(':memory:');
    const stmt = db.prepare('SELECT 1 as __proto__, 2 as constructor, 3 as toString');
    t.assert.deepStrictEqual(stmt.get(), { __proto__: null, ['__proto__']: 1, constructor: 2, toString: 3 });
  });

  test('reflects an added column after the schema changes', (t) => {
    using db = new Database(':memory:');
    db.exec('CREATE TABLE storage(key TEXT, val TEXT)');
    db.prepare('INSERT INTO storage (key, val) VALUES (?, ?)').run('key1', 'val1');
    const stmt = db.prepare('SELECT * FROM storage ORDER BY key');
    db.exec("ALTER TABLE storage ADD COLUMN extra TEXT DEFAULT 'def'");
    t.assert.deepStrictEqual(stmt.get(), {
      __proto__: null, key: 'key1', val: 'val1', extra: 'def',
    });
  });

  test('reflects a dropped column after the schema changes', (t) => {
    using db = new Database(':memory:');
    db.exec('CREATE TABLE storage(key TEXT, val TEXT, extra TEXT)');
    db.prepare('INSERT INTO storage (key, val, extra) VALUES (?, ?, ?)')
      .run('key1', 'val1', 'x');
    const stmt = db.prepare('SELECT * FROM storage ORDER BY key');
    db.exec('ALTER TABLE storage DROP COLUMN extra');
    t.assert.deepStrictEqual(stmt.get(), {
      __proto__: null, key: 'key1', val: 'val1',
    });
  });

  test('throws if the statement is already finalized', (t) => {
    using db = new Database(':memory:');
    const stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)');
    stmt.close();
    t.assert.throws(() => {
      stmt.get();
    }, {
      code: 'ERR_INVALID_STATE',
      message: /statement has been finalized/,
    });
  });

  test('surfaces a deferred SQLite error from reset() even though a row was already built', (t) => {
    using db = new Database(':memory:');
    db.exec(`
      PRAGMA foreign_keys = ON;
      PRAGMA defer_foreign_keys = ON;
      CREATE TABLE parent(id INTEGER PRIMARY KEY);
      CREATE TABLE child(id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES parent(id));
    `);
    // The FK check is deferred until the implicit transaction commits, which
    // happens inside reset() here because RETURNING leaves the statement's
    // VDBE running after the row is produced.
    const stmt = db.prepare(
      'INSERT INTO child (parent_id) VALUES (999) RETURNING id'
    );
    t.assert.throws(() => {
      stmt.get();
    }, {
      code: 'ERR_SQLITE_ERROR',
      message: /FOREIGN KEY constraint failed/,
    });
  });
});

suite('Statement.prototype.all()', () => {
  test('executes a query and returns an empty array on no results', (t) => {
    using db = new Database(':memory:');
    const stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)');
    t.assert.deepStrictEqual(stmt.all(), []);
  });

  test('executes a query and returns all results', (t) => {
    using db = new Database(':memory:');
    let stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)');
    t.assert.deepStrictEqual(stmt.run(), { changes: 0, lastInsertRowid: 0 });
    stmt = db.prepare('INSERT INTO storage (key, val) VALUES (?, ?)');
    t.assert.deepStrictEqual(
      stmt.run('key1', 'val1'),
      { changes: 1, lastInsertRowid: 1 },
    );
    t.assert.deepStrictEqual(
      stmt.run('key2', 'val2'),
      { changes: 1, lastInsertRowid: 2 },
    );
    stmt = db.prepare('SELECT * FROM storage ORDER BY key');
    t.assert.deepStrictEqual(stmt.all(), [
      { __proto__: null, key: 'key1', val: 'val1' },
      { __proto__: null, key: 'key2', val: 'val2' },
    ]);
  });

  test('reflects an added column after the schema changes', (t) => {
    using db = new Database(':memory:');
    db.exec('CREATE TABLE storage(key TEXT, val TEXT)');
    db.prepare('INSERT INTO storage (key, val) VALUES (?, ?)').run('key1', 'val1');
    const stmt = db.prepare('SELECT * FROM storage ORDER BY key');
    db.exec("ALTER TABLE storage ADD COLUMN extra TEXT DEFAULT 'def'");
    t.assert.deepStrictEqual(stmt.all(), [
      { __proto__: null, key: 'key1', val: 'val1', extra: 'def' },
    ]);
  });

  test('reflects a dropped column after the schema changes', (t) => {
    using db = new Database(':memory:');
    db.exec('CREATE TABLE storage(key TEXT, val TEXT, extra TEXT)');
    db.prepare('INSERT INTO storage (key, val, extra) VALUES (?, ?, ?)')
      .run('key1', 'val1', 'x');
    const stmt = db.prepare('SELECT * FROM storage ORDER BY key');
    db.exec('ALTER TABLE storage DROP COLUMN extra');
    t.assert.deepStrictEqual(stmt.all(), [
      { __proto__: null, key: 'key1', val: 'val1' },
    ]);
  });

  test('throws if the statement is already finalized', (t) => {
    using db = new Database(':memory:');
    const stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)');
    stmt.close();
    t.assert.throws(() => {
      stmt.all();
    }, {
      code: 'ERR_INVALID_STATE',
      message: /statement has been finalized/,
    });
  });

  test('surfaces a deferred SQLite error from reset() even though the array was already built', (t) => {
    using db = new Database(':memory:');
    db.exec(`
      PRAGMA foreign_keys = ON;
      PRAGMA defer_foreign_keys = ON;
      CREATE TABLE parent(id INTEGER PRIMARY KEY);
      CREATE TABLE child(id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES parent(id));
    `);
    const stmt = db.prepare(
      'INSERT INTO child (parent_id) VALUES (999) RETURNING id'
    );
    t.assert.throws(() => {
      stmt.all();
    }, {
      code: 'ERR_SQLITE_ERROR',
      message: /FOREIGN KEY constraint failed/,
    });
  });
});

suite('Statement.prototype.iterate()', () => {
  test('executes a query and returns an empty iterator on no results', (t) => {
    using db = new Database(':memory:');
    const stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)');
    const iter = stmt.iterate();
    t.assert.strictEqual(iter instanceof globalThis.Iterator, true);
    t.assert.ok(iter[Symbol.iterator]);
    t.assert.deepStrictEqual(iter.toArray(), []);
  });

  test('executes a query and returns all results', (t) => {
    using db = new Database(':memory:');
    let stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)');
    t.assert.deepStrictEqual(stmt.run(), { changes: 0, lastInsertRowid: 0 });
    stmt = db.prepare('INSERT INTO storage (key, val) VALUES (?, ?)');
    t.assert.deepStrictEqual(
      stmt.run('key1', 'val1'),
      { changes: 1, lastInsertRowid: 1 },
    );
    t.assert.deepStrictEqual(
      stmt.run('key2', 'val2'),
      { changes: 1, lastInsertRowid: 2 },
    );

    const items = [
      { __proto__: null, key: 'key1', val: 'val1' },
      { __proto__: null, key: 'key2', val: 'val2' },
    ];

    stmt = db.prepare('SELECT * FROM storage ORDER BY key');
    t.assert.deepStrictEqual(stmt.iterate().toArray(), items);

    const itemsLoop = items.slice();
    for (const item of stmt.iterate()) {
      t.assert.deepStrictEqual(item, itemsLoop.shift());
    }
  });

  test('reflects an added column after the schema changes', (t) => {
    using db = new Database(':memory:');
    db.exec('CREATE TABLE storage(key TEXT, val TEXT)');
    db.prepare('INSERT INTO storage (key, val) VALUES (?, ?)').run('key1', 'val1');
    const stmt = db.prepare('SELECT * FROM storage ORDER BY key');
    db.exec("ALTER TABLE storage ADD COLUMN extra TEXT DEFAULT 'def'");
    t.assert.deepStrictEqual(stmt.iterate().toArray(), [
      { __proto__: null, key: 'key1', val: 'val1', extra: 'def' },
    ]);
  });

  test('reflects a dropped column after the schema changes', (t) => {
    using db = new Database(':memory:');
    db.exec('CREATE TABLE storage(key TEXT, val TEXT, extra TEXT)');
    db.prepare('INSERT INTO storage (key, val, extra) VALUES (?, ?, ?)')
      .run('key1', 'val1', 'x');
    const stmt = db.prepare('SELECT * FROM storage ORDER BY key');
    db.exec('ALTER TABLE storage DROP COLUMN extra');
    t.assert.deepStrictEqual(stmt.iterate().toArray(), [
      { __proto__: null, key: 'key1', val: 'val1' },
    ]);
  });

  test('iterator keeps the prepared statement from being collected', (t) => {
    const db = new Database(':memory:');
    db.exec(`
      CREATE TABLE test(key TEXT, val TEXT);
      INSERT INTO test (key, val) VALUES ('key1', 'val1');
      INSERT INTO test (key, val) VALUES ('key2', 'val2');
    `);
    // Do not keep an explicit reference to the prepared statement.
    const iterator = db.prepare('SELECT * FROM test').iterate();
    const results = [];

    global.gc();

    for (const item of iterator) {
      results.push(item);
    }

    t.assert.deepStrictEqual(results, [
      { __proto__: null, key: 'key1', val: 'val1' },
      { __proto__: null, key: 'key2', val: 'val2' },
    ]);
  });

  test('iterator can be exited early', (t) => {
    const db = new Database(':memory:');
    db.exec(`
      CREATE TABLE test(key TEXT, val TEXT);
      INSERT INTO test (key, val) VALUES ('key1', 'val1');
      INSERT INTO test (key, val) VALUES ('key2', 'val2');
    `);
    const iterator = db.prepare('SELECT * FROM test').iterate();
    const results = [];

    for (const item of iterator) {
      results.push(item);
      break;
    }

    t.assert.deepStrictEqual(results, [
      { __proto__: null, key: 'key1', val: 'val1' },
    ]);
    t.assert.deepStrictEqual(
      iterator.next(),
      { __proto__: null, done: true, value: null },
    );
  });

  test('iterator is invalidated when statement is reset by get/all/run/iterate', (t) => {
    const db = new Database(':memory:');
    db.exec('CREATE TABLE test (value INTEGER NOT NULL)');
    for (let i = 0; i < 5; i++) {
      db.prepare('INSERT INTO test (value) VALUES (?)').run(i);
    }
    const stmt = db.prepare('SELECT * FROM test');

    // Invalidated by stmt.get()
    let it = stmt.iterate();
    it.next();
    stmt.get();
    t.assert.throws(() => { it.next(); }, {
      code: 'ERR_INVALID_STATE',
      message: /iterator was invalidated/,
    });

    // Invalidated by stmt.all()
    it = stmt.iterate();
    it.next();
    stmt.all();
    t.assert.throws(() => { it.next(); }, {
      code: 'ERR_INVALID_STATE',
      message: /iterator was invalidated/,
    });

    // Invalidated by stmt.run()
    it = stmt.iterate();
    it.next();
    stmt.run();
    t.assert.throws(() => { it.next(); }, {
      code: 'ERR_INVALID_STATE',
      message: /iterator was invalidated/,
    });

    // Invalidated by a new stmt.iterate()
    it = stmt.iterate();
    it.next();
    const it2 = stmt.iterate();
    t.assert.throws(() => { it.next(); }, {
      code: 'ERR_INVALID_STATE',
      message: /iterator was invalidated/,
    });

    // New iterator works fine
    t.assert.strictEqual(it2.next().done, false);

    // Reset on a different statement does NOT invalidate this iterator
    const stmt2 = db.prepare('SELECT * FROM test');
    it = stmt.iterate();
    it.next();
    stmt2.get();
    it.next();
  });

  test('throws if the statement is already finalized', (t) => {
    using db = new Database(':memory:');
    const stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)');
    stmt.close();
    t.assert.throws(() => {
      stmt.iterate();
    }, {
      code: 'ERR_INVALID_STATE',
      message: /statement has been finalized/,
    });
  });

  test('does not replay results after the iterator is naturally exhausted', (t) => {
    using db = new Database(':memory:');
    db.exec(`
      CREATE TABLE test(key TEXT);
      INSERT INTO test (key) VALUES ('key1');
    `);
    const it = db.prepare('SELECT * FROM test').iterate();
    t.assert.deepStrictEqual(it.next(), {
      __proto__: null, done: false, value: { __proto__: null, key: 'key1' },
    });
    t.assert.deepStrictEqual(
      it.next(), { __proto__: null, done: true, value: null });
    // Calling next() again on an exhausted iterator must keep reporting
    // done, not silently reset the statement and replay from row 1.
    t.assert.deepStrictEqual(
      it.next(), { __proto__: null, done: true, value: null });
  });

  test('propagates a pending exception when the loop body throws mid-iteration', (t) => {
    using db = new Database(':memory:');
    db.exec(`
      CREATE TABLE test(key TEXT);
      INSERT INTO test (key) VALUES ('key1');
      INSERT INTO test (key) VALUES ('key2');
    `);
    const stmt = db.prepare('SELECT * FROM test');
    const userError = new Error('boom');
    t.assert.throws(() => {
      // eslint-disable-next-line no-unused-vars
      for (const row of stmt.iterate()) {
        throw userError;
      }
    }, (err) => err === userError);
  });
});

suite('Statement.prototype.run()', () => {
  test('executes a query and returns change metadata', (t) => {
    using db = new Database(':memory:');
    const setup = db.exec(`
      CREATE TABLE storage(key TEXT, val TEXT);
      INSERT INTO storage (key, val) VALUES ('foo', 'bar');
    `);
    t.assert.strictEqual(setup, undefined);
    const stmt = db.prepare('SELECT * FROM storage');
    t.assert.deepStrictEqual(stmt.run(), { changes: 1, lastInsertRowid: 1 });
  });

  test('SQLite throws when trying to bind too many parameters', (t) => {
    using db = new Database(':memory:');
    const setup = db.exec(
      'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;'
    );
    t.assert.strictEqual(setup, undefined);
    const stmt = db.prepare('INSERT INTO data (key, val) VALUES (?, ?)');
    t.assert.throws(() => {
      stmt.run(1, 2, 3);
    }, {
      code: 'ERR_SQLITE_ERROR',
      message: 'column index out of range',
      errcode: 25,
      errstr: 'column index out of range',
    });
  });

  test('SQLite defaults to NULL for unbound parameters', (t) => {
    using db = new Database(':memory:');
    const setup = db.exec(
      'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER NOT NULL) STRICT;'
    );
    t.assert.strictEqual(setup, undefined);
    const stmt = db.prepare('INSERT INTO data (key, val) VALUES (?, ?)');
    t.assert.throws(() => {
      stmt.run(1);
    }, {
      code: 'ERR_SQLITE_ERROR',
      message: 'NOT NULL constraint failed: data.val',
      errcode: 1299,
      errstr: 'constraint failed',
    });
  });

  test('returns correct metadata when using RETURNING', (t) => {
    const db = new Database(':memory:');
    const setup = db.exec(
      'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER NOT NULL) STRICT;'
    );
    t.assert.strictEqual(setup, undefined);
    const sql = 'INSERT INTO data (key, val) VALUES ($k, $v) RETURNING key';
    const stmt = db.prepare(sql);
    t.assert.deepStrictEqual(
      stmt.run({ k: 1, v: 10 }), { changes: 1, lastInsertRowid: 1 }
    );
    t.assert.deepStrictEqual(
      stmt.run({ k: 2, v: 20 }), { changes: 1, lastInsertRowid: 2 }
    );
    t.assert.deepStrictEqual(
      stmt.run({ k: 3, v: 30 }), { changes: 1, lastInsertRowid: 3 }
    );
  });

  test('SQLite defaults unbound ?NNN parameters', (t) => {
    using db = new Database(':memory:');
    const setup = db.exec(
      'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER NOT NULL) STRICT;'
    );
    t.assert.strictEqual(setup, undefined);
    const stmt = db.prepare('INSERT INTO data (key, val) VALUES (?1, ?3)');

    t.assert.throws(() => {
      stmt.run(1);
    }, {
      code: 'ERR_SQLITE_ERROR',
      message: 'NOT NULL constraint failed: data.val',
      errcode: 1299,
      errstr: 'constraint failed',
    });
  });

  test('binds ?NNN params by position', (t) => {
    using db = new Database(':memory:');
    const setup = db.exec(
      'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER NOT NULL) STRICT;'
    );
    t.assert.strictEqual(setup, undefined);
    const stmt = db.prepare('INSERT INTO data (key, val) VALUES (?1, ?2)');
    t.assert.deepStrictEqual(stmt.run(1, 2), { changes: 1, lastInsertRowid: 1 });
  });

  test('throws if the statement is already finalized', (t) => {
    using db = new Database(':memory:');
    const stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)');
    stmt.close();
    t.assert.throws(() => {
      stmt.run();
    }, {
      code: 'ERR_INVALID_STATE',
      message: /statement has been finalized/,
    });
  });
});

suite('Statement.prototype.sourceSQL', () => {
  test('equals input SQL', (t) => {
    using db = new Database(':memory:');
    const setup = db.exec(
      'CREATE TABLE types(key INTEGER PRIMARY KEY, val INTEGER) STRICT;'
    );
    t.assert.strictEqual(setup, undefined);
    const sql = 'INSERT INTO types (key, val) VALUES ($k, $v)';
    const stmt = db.prepare(sql);
    t.assert.strictEqual(stmt.sourceSQL, sql);
  });

  test('throws if the statement is already finalized', (t) => {
    using db = new Database(':memory:');
    const stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)');
    stmt.close();
    t.assert.throws(() => stmt.sourceSQL, {
      code: 'ERR_INVALID_STATE',
      message: /statement has been finalized/,
    });
  });
});

suite('Statement.prototype.expandedSQL', () => {
  test('equals expanded SQL', (t) => {
    using db = new Database(':memory:');
    const setup = db.exec(
      'CREATE TABLE types(key INTEGER PRIMARY KEY, val INTEGER) STRICT;'
    );
    t.assert.strictEqual(setup, undefined);
    const sql = 'INSERT INTO types (key, val) VALUES ($k, ?)';
    const expanded = 'INSERT INTO types (key, val) VALUES (\'33\', \'42\')';
    const stmt = db.prepare(sql);
    t.assert.deepStrictEqual(
      stmt.run({ $k: '33' }, '42'),
      { changes: 1, lastInsertRowid: 33 },
    );
    t.assert.strictEqual(stmt.expandedSQL, expanded);
  });

  test('throws if the statement is already finalized', (t) => {
    using db = new Database(':memory:');
    const stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)');
    stmt.close();
    t.assert.throws(() => stmt.expandedSQL, {
      code: 'ERR_INVALID_STATE',
      message: /statement has been finalized/,
    });
  });
});

suite('Statement.prototype.stat()', () => {
  const counters = [
    'fullscanStep', 'sort', 'autoindex', 'vmStep', 'reprepare',
    'run', 'memused',
  ];

  // 'filterMiss' and 'filterHit' map to SQLITE_STMTSTATUS_FILTER_MISS and
  // SQLITE_STMTSTATUS_FILTER_HIT, which were added in SQLite 3.38.0. Builds
  // using an older shared SQLite do not expose them.
  const [major, minor] = process.versions.sqlite.split('.').map(Number);
  const hasFilterCounters = major > 3 || (major === 3 && minor >= 38);

  if (hasFilterCounters) {
    counters.push('filterMiss', 'filterHit');
  }

  test('returns a number for every valid counter', (t) => {
    using db = new Database(':memory:');
    db.exec('CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT;');
    const stmt = db.prepare('SELECT * FROM data');
    for (const counter of counters) {
      t.assert.strictEqual(typeof stmt.stat(counter), 'number');
    }
  });

  test('counts virtual machine steps and runs after execution', (t) => {
    using db = new Database(':memory:');
    db.exec('CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT;');
    const insert = db.prepare('INSERT INTO data (key, val) VALUES (?, ?)');
    for (let i = 1; i <= 5; i++) {
      insert.run(i, `val-${i}`);
    }
    const stmt = db.prepare('SELECT * FROM data');
    t.assert.strictEqual(stmt.stat('run'), 0);
    t.assert.strictEqual(stmt.stat('vmStep'), 0);
    stmt.all();
    t.assert.strictEqual(stmt.stat('run'), 1);
    t.assert.ok(stmt.stat('vmStep') > 0);
    t.assert.ok(stmt.stat('memused') > 0);
  });

  test('detects full table scans', (t) => {
    using db = new Database(':memory:');
    db.exec('CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT;');
    const insert = db.prepare('INSERT INTO data (key, val) VALUES (?, ?)');
    for (let i = 1; i <= 10; i++) {
      insert.run(i, `val-${i}`);
    }

    // Filtering on a non-indexed column forces a full table scan.
    const scan = db.prepare('SELECT * FROM data WHERE val = ?');
    scan.all('val-5');
    t.assert.ok(scan.stat('fullscanStep') > 0);

    // Filtering on the primary key uses the index; no full scan occurs.
    const indexed = db.prepare('SELECT * FROM data WHERE key = ?');
    indexed.all(5);
    t.assert.strictEqual(indexed.stat('fullscanStep'), 0);
  });

  test('reading a counter does not reset it', (t) => {
    using db = new Database(':memory:');
    db.exec('CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT;');
    const stmt = db.prepare('SELECT * FROM data');
    stmt.all();
    const first = stmt.stat('run');
    t.assert.strictEqual(stmt.stat('run'), first);
  });

  test('throws if the counter argument is not a string', (t) => {
    using db = new Database(':memory:');
    const stmt = db.prepare('SELECT 1');
    t.assert.throws(() => stmt.stat(), {
      code: 'ERR_INVALID_ARG_TYPE',
      message: /The "counter" argument must be a string/,
    });
    t.assert.throws(() => stmt.stat(42), {
      code: 'ERR_INVALID_ARG_TYPE',
      message: /The "counter" argument must be a string/,
    });
  });

  test('throws if the counter name is unknown', (t) => {
    using db = new Database(':memory:');
    const stmt = db.prepare('SELECT 1');
    t.assert.throws(() => stmt.stat('nope'), {
      code: 'ERR_INVALID_ARG_VALUE',
      message: /The "counter" argument is not a valid statistic name/,
    });
  });

  test('throws if the statement is finalized', (t) => {
    const db = new Database(':memory:');
    const stmt = db.prepare('SELECT 1');
    db.close();
    t.assert.throws(() => stmt.stat('run'), {
      code: 'ERR_INVALID_STATE',
      message: /statement has been finalized/,
    });
  });
});

suite('Statement.prototype.resetStats()', () => {
  test('returns undefined', (t) => {
    using db = new Database(':memory:');
    const stmt = db.prepare('SELECT 1');
    t.assert.strictEqual(stmt.resetStats(), undefined);
  });

  // The column name cache is keyed on the reprepare counter, which
  // resetStats() zeroes. A later re-prepare must not be able to make the
  // counter match the cached generation again and reuse stale names.
  test('invalidates cached iterator column names', (t) => {
    using db = new Database(':memory:');
    db.exec('CREATE TABLE data(a); INSERT INTO data VALUES (1)');
    const stmt = db.prepare('SELECT * FROM data');

    db.exec('ALTER TABLE data RENAME COLUMN a TO b');
    stmt.iterate().toArray();
    stmt.resetStats();
    db.exec('ALTER TABLE data RENAME COLUMN b TO c');

    t.assert.deepStrictEqual(stmt.iterate().toArray(), [
      { __proto__: null, c: 1 },
    ]);
  });

  test('invalidates the cache when the column count grows', (t) => {
    using db = new Database(':memory:');
    db.exec('CREATE TABLE t(a); INSERT INTO t VALUES (1)');
    const stmt = db.prepare('SELECT * FROM t');

    db.exec('ALTER TABLE t ADD COLUMN b DEFAULT 2');
    stmt.iterate().toArray();
    stmt.resetStats();
    db.exec('ALTER TABLE t ADD COLUMN c DEFAULT 3');

    t.assert.deepStrictEqual(stmt.iterate().toArray(), [
      { __proto__: null, a: 1, b: 2, c: 3 },
    ]);
  });

  test('does not reset memused', (t) => {
    using db = new Database(':memory:');
    db.exec('CREATE TABLE t(a); INSERT INTO t VALUES (1),(2),(3)');
    const stmt = db.prepare('SELECT * FROM t ORDER BY a');
    stmt.all();

    // The memused counter reports current memory usage rather than an
    // accumulated total, so SQLite ignores the reset flag for it.
    const before = stmt.stat('memused');
    t.assert.ok(before > 0);
    stmt.resetStats();
    t.assert.strictEqual(stmt.stat('memused'), before);
  });

  test('clears every counter', (t) => {
    using db = new Database(':memory:');
    db.exec('CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT;');
    const insert = db.prepare('INSERT INTO data (key, val) VALUES (?, ?)');
    for (let i = 1; i <= 5; i++) {
      insert.run(i, `val-${i}`);
    }

    // Force a full table scan so more than one counter is non-zero.
    const stmt = db.prepare('SELECT * FROM data WHERE val = ?');
    stmt.all('val-3');
    t.assert.ok(stmt.stat('run') > 0);
    t.assert.ok(stmt.stat('vmStep') > 0);
    t.assert.ok(stmt.stat('fullscanStep') > 0);

    stmt.resetStats();
    t.assert.strictEqual(stmt.stat('run'), 0);
    t.assert.strictEqual(stmt.stat('vmStep'), 0);
    t.assert.strictEqual(stmt.stat('fullscanStep'), 0);
  });

  test('counters accumulate again after a reset', (t) => {
    using db = new Database(':memory:');
    db.exec('CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT;');
    const stmt = db.prepare('SELECT * FROM data');
    stmt.all();
    stmt.resetStats();
    t.assert.strictEqual(stmt.stat('run'), 0);
    stmt.all();
    t.assert.strictEqual(stmt.stat('run'), 1);
  });

  test('is a no-op when no counters have been incremented', (t) => {
    using db = new Database(':memory:');
    const stmt = db.prepare('SELECT 1');
    stmt.resetStats();
    t.assert.strictEqual(stmt.stat('run'), 0);
  });

  test('throws if the statement is finalized', (t) => {
    const db = new Database(':memory:');
    const stmt = db.prepare('SELECT 1');
    db.close();
    t.assert.throws(() => stmt.resetStats(), {
      code: 'ERR_INVALID_STATE',
      message: /statement has been finalized/,
    });
  });
});

suite('Statement.prototype.setReadBigInts()', () => {
  test('BigInts support can be toggled', (t) => {
    using db = new Database(':memory:');
    const setup = db.exec(`
      CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;
      INSERT INTO data (key, val) VALUES (1, 42);
    `);
    t.assert.strictEqual(setup, undefined);

    const query = db.prepare('SELECT val FROM data');
    t.assert.deepStrictEqual(query.get(), { __proto__: null, val: 42 });
    t.assert.strictEqual(query.setReadBigInts(true), undefined);
    t.assert.deepStrictEqual(query.get(), { __proto__: null, val: 42n });
    t.assert.strictEqual(query.setReadBigInts(false), undefined);
    t.assert.deepStrictEqual(query.get(), { __proto__: null, val: 42 });

    const insert = db.prepare('INSERT INTO data (key) VALUES (?)');
    t.assert.deepStrictEqual(
      insert.run(10),
      { changes: 1, lastInsertRowid: 10 },
    );
    t.assert.strictEqual(insert.setReadBigInts(true), undefined);
    t.assert.deepStrictEqual(
      insert.run(20),
      { changes: 1n, lastInsertRowid: 20n },
    );
    t.assert.strictEqual(insert.setReadBigInts(false), undefined);
    t.assert.deepStrictEqual(
      insert.run(30),
      { changes: 1, lastInsertRowid: 30 },
    );
  });

  test('throws when input is not a boolean', (t) => {
    using db = new Database(':memory:');
    const setup = db.exec(
      'CREATE TABLE types(key INTEGER PRIMARY KEY, val INTEGER) STRICT;'
    );
    t.assert.strictEqual(setup, undefined);
    const stmt = db.prepare('INSERT INTO types (key, val) VALUES ($k, $v)');
    t.assert.throws(() => {
      stmt.setReadBigInts();
    }, {
      code: 'ERR_INVALID_ARG_TYPE',
      message: /The "readBigInts" argument must be a boolean/,
    });
  });

  test('BigInt is required for reading large integers', (t) => {
    using db = new Database(':memory:');
    const bad = db.prepare(`SELECT ${Number.MAX_SAFE_INTEGER} + 1`);
    t.assert.throws(() => {
      bad.get();
    }, {
      code: 'ERR_OUT_OF_RANGE',
      message: /^Value is too large to be represented as a JavaScript number: 9007199254740992$/,
    });
    const good = db.prepare(`SELECT ${Number.MAX_SAFE_INTEGER} + 1`);
    good.setReadBigInts(true);
    t.assert.deepStrictEqual(good.get(), {
      __proto__: null,
      [`${Number.MAX_SAFE_INTEGER} + 1`]: 2n ** 53n,
    });
  });

  test('BigInt is required for reading large last insert row IDs', (t) => {
    using db = new Database(':memory:');
    db.exec('CREATE TABLE data(key INTEGER PRIMARY KEY) STRICT');
    const insert = db.prepare('INSERT INTO data VALUES (?)');

    t.assert.throws(() => {
      insert.run(9007199254740993n);
    }, {
      code: 'ERR_OUT_OF_RANGE',
      message: /^Value is too large to be represented as a JavaScript number: 9007199254740993$/,
    });

    insert.setReadBigInts(true);
    t.assert.deepStrictEqual(insert.run(9007199254740995n), {
      changes: 1n,
      lastInsertRowid: 9007199254740995n,
    });
  });

  test('throws if the statement is already finalized', (t) => {
    using db = new Database(':memory:');
    const stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)');
    stmt.close();
    t.assert.throws(() => {
      stmt.setReadBigInts(true);
    }, {
      code: 'ERR_INVALID_STATE',
      message: /statement has been finalized/,
    });
  });
});

suite('Statement.prototype.setReturnArrays()', () => {
  test('throws when input is not a boolean', (t) => {
    using db = new Database(':memory:');
    const setup = db.exec(
      'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;'
    );
    t.assert.strictEqual(setup, undefined);
    const stmt = db.prepare('SELECT key, val FROM data');
    t.assert.throws(() => {
      stmt.setReturnArrays();
    }, {
      code: 'ERR_INVALID_ARG_TYPE',
      message: /The "returnArrays" argument must be a boolean/,
    });
  });

  test('throws if the statement is already finalized', (t) => {
    using db = new Database(':memory:');
    const stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)');
    stmt.close();
    t.assert.throws(() => {
      stmt.setReturnArrays(true);
    }, {
      code: 'ERR_INVALID_STATE',
      message: /statement has been finalized/,
    });
  });
});

suite('Statement.prototype.get() with array output', () => {
  test('returns array row when setReturnArrays is true', (t) => {
    using db = new Database(':memory:');
    const setup = db.exec(`
      CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT;
      INSERT INTO data (key, val) VALUES (1, 'one');
    `);
    t.assert.strictEqual(setup, undefined);

    const query = db.prepare('SELECT key, val FROM data WHERE key = 1');
    t.assert.deepStrictEqual(query.get(), { __proto__: null, key: 1, val: 'one' });

    query.setReturnArrays(true);
    t.assert.deepStrictEqual(query.get(), [1, 'one']);

    query.setReturnArrays(false);
    t.assert.deepStrictEqual(query.get(), { __proto__: null, key: 1, val: 'one' });
  });

  test('returns array rows with BigInts when both flags are set', (t) => {
    const expected = [1n, 9007199254740992n];
    using db = new Database(':memory:');
    const setup = db.exec(`
      CREATE TABLE big_data(id INTEGER, big_num INTEGER);
      INSERT INTO big_data VALUES (1, 9007199254740992);
    `);
    t.assert.strictEqual(setup, undefined);

    const query = db.prepare('SELECT id, big_num FROM big_data');
    query.setReturnArrays(true);
    query.setReadBigInts(true);

    const row = query.get();
    t.assert.deepStrictEqual(row, expected);
  });
});

suite('Statement.prototype.all() with array output', () => {
  test('returns array rows when setReturnArrays is true', (t) => {
    using db = new Database(':memory:');
    const setup = db.exec(`
      CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT;
      INSERT INTO data (key, val) VALUES (1, 'one');
      INSERT INTO data (key, val) VALUES (2, 'two');
    `);
    t.assert.strictEqual(setup, undefined);

    const query = db.prepare('SELECT key, val FROM data ORDER BY key');
    t.assert.deepStrictEqual(query.all(), [
      { __proto__: null, key: 1, val: 'one' },
      { __proto__: null, key: 2, val: 'two' },
    ]);

    query.setReturnArrays(true);
    t.assert.deepStrictEqual(query.all(), [
      [1, 'one'],
      [2, 'two'],
    ]);

    query.setReturnArrays(false);
    t.assert.deepStrictEqual(query.all(), [
      { __proto__: null, key: 1, val: 'one' },
      { __proto__: null, key: 2, val: 'two' },
    ]);
  });

  test('handles array rows with many columns', (t) => {
    const expected = [
      1,
      'text1',
      1.1,
      new Uint8Array([0xde, 0xad, 0xbe, 0xef]),
      5,
      'text2',
      2.2,
      new Uint8Array([0xbe, 0xef, 0xca, 0xfe]),
      9,
      'text3',
    ];
    using db = new Database(':memory:');
    const setup = db.exec(`
      CREATE TABLE wide_table(
        col1 INTEGER, col2 TEXT, col3 REAL, col4 BLOB, col5 INTEGER,
        col6 TEXT, col7 REAL, col8 BLOB, col9 INTEGER, col10 TEXT
      );
      INSERT INTO wide_table VALUES (
        1, 'text1', 1.1, X'DEADBEEF', 5,
        'text2', 2.2, X'BEEFCAFE', 9, 'text3'
      );
    `);
    t.assert.strictEqual(setup, undefined);

    const query = db.prepare('SELECT * FROM wide_table');
    query.setReturnArrays(true);

    const results = query.all();
    t.assert.strictEqual(results.length, 1);
    t.assert.deepStrictEqual(results[0], expected);
  });
});

suite('Statement.prototype.iterate() with array output', () => {
  test('iterates array rows when setReturnArrays is true', (t) => {
    using db = new Database(':memory:');
    const setup = db.exec(`
      CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT;
      INSERT INTO data (key, val) VALUES (1, 'one');
      INSERT INTO data (key, val) VALUES (2, 'two');
    `);
    t.assert.strictEqual(setup, undefined);

    const query = db.prepare('SELECT key, val FROM data ORDER BY key');

    // Test with objects first
    const objectRows = [];
    for (const row of query.iterate()) {
      objectRows.push(row);
    }
    t.assert.deepStrictEqual(objectRows, [
      { __proto__: null, key: 1, val: 'one' },
      { __proto__: null, key: 2, val: 'two' },
    ]);

    // Test with arrays
    query.setReturnArrays(true);
    const arrayRows = [];
    for (const row of query.iterate()) {
      arrayRows.push(row);
    }
    t.assert.deepStrictEqual(arrayRows, [
      [1, 'one'],
      [2, 'two'],
    ]);

    // Test toArray() method
    t.assert.deepStrictEqual(query.iterate().toArray(), [
      [1, 'one'],
      [2, 'two'],
    ]);
  });

  test('iterator can be exited early with array rows', (t) => {
    const db = new Database(':memory:');
    db.exec(`
      CREATE TABLE test(key TEXT, val TEXT);
      INSERT INTO test (key, val) VALUES ('key1', 'val1');
      INSERT INTO test (key, val) VALUES ('key2', 'val2');
    `);
    const stmt = db.prepare('SELECT key, val FROM test');
    stmt.setReturnArrays(true);

    const iterator = stmt.iterate();
    const results = [];

    for (const item of iterator) {
      results.push(item);
      break;
    }

    t.assert.deepStrictEqual(results, [
      ['key1', 'val1'],
    ]);
    t.assert.deepStrictEqual(
      iterator.next(),
      { __proto__: null, done: true, value: null },
    );
  });
});

suite('Statement.prototype.setAllowBareNamedParameters()', () => {
  test('bare named parameter support can be toggled', (t) => {
    using db = new Database(':memory:');
    const setup = db.exec(
      'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;'
    );
    t.assert.strictEqual(setup, undefined);
    const stmt = db.prepare('INSERT INTO data (key, val) VALUES ($k, $v)');
    t.assert.deepStrictEqual(
      stmt.run({ k: 1, v: 2 }),
      { changes: 1, lastInsertRowid: 1 },
    );
    t.assert.strictEqual(stmt.setAllowBareNamedParameters(false), undefined);
    t.assert.throws(() => {
      stmt.run({ k: 2, v: 4 });
    }, {
      code: 'ERR_INVALID_STATE',
      message: /Unknown named parameter 'k'/,
    });
    t.assert.strictEqual(stmt.setAllowBareNamedParameters(true), undefined);
    t.assert.deepStrictEqual(
      stmt.run({ k: 3, v: 6 }),
      { changes: 1, lastInsertRowid: 3 },
    );
  });

  test('throws when input is not a boolean', (t) => {
    using db = new Database(':memory:');
    const setup = db.exec(
      'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;'
    );
    t.assert.strictEqual(setup, undefined);
    const stmt = db.prepare('INSERT INTO data (key, val) VALUES ($k, $v)');
    t.assert.throws(() => {
      stmt.setAllowBareNamedParameters();
    }, {
      code: 'ERR_INVALID_ARG_TYPE',
      message: /The "allowBareNamedParameters" argument must be a boolean/,
    });
  });

  test('throws if the statement is already finalized', (t) => {
    using db = new Database(':memory:');
    const stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)');
    stmt.close();
    t.assert.throws(() => {
      stmt.setAllowBareNamedParameters(true);
    }, {
      code: 'ERR_INVALID_STATE',
      message: /statement has been finalized/,
    });
  });
});

suite('options.readBigInts', () => {
  test('BigInts are returned when input is true', (t) => {
    using db = new Database(':memory:');
    const setup = db.exec(`
      CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;
      INSERT INTO data (key, val) VALUES (1, 42);
    `);
    t.assert.strictEqual(setup, undefined);

    const query = db.prepare('SELECT val FROM data', { readBigInts: true });
    t.assert.deepStrictEqual(query.get(), { __proto__: null, val: 42n });
  });

  test('numbers are returned when input is false', (t) => {
    using db = new Database(':memory:');
    const setup = db.exec(`
      CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;
      INSERT INTO data (key, val) VALUES (1, 42);
    `);
    t.assert.strictEqual(setup, undefined);

    const query = db.prepare('SELECT val FROM data', { readBigInts: false });
    t.assert.deepStrictEqual(query.get(), { __proto__: null, val: 42 });
  });

  test('throws when input is not a boolean', (t) => {
    using db = new Database(':memory:');
    const setup = db.exec(
      'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;'
    );
    t.assert.strictEqual(setup, undefined);
    t.assert.throws(() => {
      db.prepare('SELECT val FROM data', { readBigInts: 'true' });
    }, {
      code: 'ERR_INVALID_ARG_TYPE',
      message: /The "options\.readBigInts" argument must be a boolean/,
    });
  });

  test('setReadBigInts can override prepare option', (t) => {
    using db = new Database(':memory:');
    const setup = db.exec(`
      CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;
      INSERT INTO data (key, val) VALUES (1, 42);
    `);
    t.assert.strictEqual(setup, undefined);

    const query = db.prepare('SELECT val FROM data', { readBigInts: true });
    t.assert.deepStrictEqual(query.get(), { __proto__: null, val: 42n });
    t.assert.strictEqual(query.setReadBigInts(false), undefined);
    t.assert.deepStrictEqual(query.get(), { __proto__: null, val: 42 });
  });
});

suite('options.returnArrays', () => {
  test('arrays are returned when input is true', (t) => {
    using db = new Database(':memory:');
    const setup = db.exec(`
      CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT;
      INSERT INTO data (key, val) VALUES (1, 'one');
    `);
    t.assert.strictEqual(setup, undefined);

    const query = db.prepare(
      'SELECT key, val FROM data WHERE key = 1',
      { returnArrays: true }
    );
    t.assert.deepStrictEqual(query.get(), [1, 'one']);
  });

  test('objects are returned when input is false', (t) => {
    using db = new Database(':memory:');
    const setup = db.exec(`
      CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT;
      INSERT INTO data (key, val) VALUES (1, 'one');
    `);
    t.assert.strictEqual(setup, undefined);

    const query = db.prepare(
      'SELECT key, val FROM data WHERE key = 1',
      { returnArrays: false }
    );
    t.assert.deepStrictEqual(query.get(), { __proto__: null, key: 1, val: 'one' });
  });

  test('throws when input is not a boolean', (t) => {
    using db = new Database(':memory:');
    const setup = db.exec(
      'CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT;'
    );
    t.assert.strictEqual(setup, undefined);
    t.assert.throws(() => {
      db.prepare('SELECT key, val FROM data', { returnArrays: 'true' });
    }, {
      code: 'ERR_INVALID_ARG_TYPE',
      message: /The "options\.returnArrays" argument must be a boolean/,
    });
  });

  test('setReturnArrays can override prepare option', (t) => {
    using db = new Database(':memory:');
    const setup = db.exec(`
      CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT;
      INSERT INTO data (key, val) VALUES (1, 'one');
    `);
    t.assert.strictEqual(setup, undefined);

    const query = db.prepare(
      'SELECT key, val FROM data WHERE key = 1',
      { returnArrays: true }
    );
    t.assert.deepStrictEqual(query.get(), [1, 'one']);
    t.assert.strictEqual(query.setReturnArrays(false), undefined);
    t.assert.deepStrictEqual(query.get(), { __proto__: null, key: 1, val: 'one' });
  });

  test('all() returns arrays when input is true', (t) => {
    using db = new Database(':memory:');
    const setup = db.exec(`
      CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT;
      INSERT INTO data (key, val) VALUES (1, 'one');
      INSERT INTO data (key, val) VALUES (2, 'two');
    `);
    t.assert.strictEqual(setup, undefined);

    const query = db.prepare(
      'SELECT key, val FROM data ORDER BY key',
      { returnArrays: true }
    );
    t.assert.deepStrictEqual(query.all(), [
      [1, 'one'],
      [2, 'two'],
    ]);
  });

  test('iterate() returns arrays when input is true', (t) => {
    using db = new Database(':memory:');
    const setup = db.exec(`
      CREATE TABLE data(key INTEGER PRIMARY KEY, val TEXT) STRICT;
      INSERT INTO data (key, val) VALUES (1, 'one');
      INSERT INTO data (key, val) VALUES (2, 'two');
    `);
    t.assert.strictEqual(setup, undefined);

    const query = db.prepare(
      'SELECT key, val FROM data ORDER BY key',
      { returnArrays: true }
    );
    t.assert.deepStrictEqual(query.iterate().toArray(), [
      [1, 'one'],
      [2, 'two'],
    ]);
  });
});

suite('options.allowBareNamedParameters', () => {
  test('bare named parameters are allowed when input is true', (t) => {
    using db = new Database(':memory:');
    const setup = db.exec(
      'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;'
    );
    t.assert.strictEqual(setup, undefined);
    const stmt = db.prepare(
      'INSERT INTO data (key, val) VALUES ($k, $v)',
      { allowBareNamedParameters: true }
    );
    t.assert.deepStrictEqual(
      stmt.run({ k: 1, v: 2 }),
      { changes: 1, lastInsertRowid: 1 },
    );
  });

  test('bare named parameters throw when input is false', (t) => {
    using db = new Database(':memory:');
    const setup = db.exec(
      'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;'
    );
    t.assert.strictEqual(setup, undefined);
    const stmt = db.prepare(
      'INSERT INTO data (key, val) VALUES ($k, $v)',
      { allowBareNamedParameters: false }
    );
    t.assert.throws(() => {
      stmt.run({ k: 1, v: 2 });
    }, {
      code: 'ERR_INVALID_STATE',
      message: /Unknown named parameter 'k'/,
    });
  });

  test('throws when input is not a boolean', (t) => {
    using db = new Database(':memory:');
    const setup = db.exec(
      'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;'
    );
    t.assert.strictEqual(setup, undefined);
    t.assert.throws(() => {
      db.prepare(
        'INSERT INTO data (key, val) VALUES ($k, $v)',
        { allowBareNamedParameters: 'true' }
      );
    }, {
      code: 'ERR_INVALID_ARG_TYPE',
      message: /The "options\.allowBareNamedParameters" argument must be a boolean/,
    });
  });

  test('setAllowBareNamedParameters can override prepare option', (t) => {
    using db = new Database(':memory:');
    const setup = db.exec(
      'CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;'
    );
    t.assert.strictEqual(setup, undefined);
    const stmt = db.prepare(
      'INSERT INTO data (key, val) VALUES ($k, $v)',
      { allowBareNamedParameters: false }
    );
    t.assert.throws(() => {
      stmt.run({ k: 1, v: 2 });
    }, {
      code: 'ERR_INVALID_STATE',
      message: /Unknown named parameter 'k'/,
    });
    t.assert.strictEqual(stmt.setAllowBareNamedParameters(true), undefined);
    t.assert.deepStrictEqual(
      stmt.run({ k: 2, v: 4 }),
      { changes: 1, lastInsertRowid: 2 },
    );
  });
});


suite('Statement.prototype.close()', () => {
  test('finalizes an open statement', (t) => {
    using db = new Database(':memory:');
    db.exec('CREATE TABLE storage(key TEXT, val TEXT)');
    const stmt = db.prepare('SELECT * FROM storage');
    t.assert.strictEqual(stmt.close(), undefined);
    t.assert.throws(() => stmt.get(), {
      code: 'ERR_INVALID_STATE',
      message: /statement has been finalized/,
    });
  });

  test('throws if the statement is already finalized', (t) => {
    using db = new Database(':memory:');
    const stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)');
    stmt.close();
    t.assert.throws(() => {
      stmt.close();
    }, {
      code: 'ERR_INVALID_STATE',
      message: /statement has been finalized/,
    });
  });
});

suite('Statement.prototype[Symbol.dispose]()', () => {
  test('finalizes an open statement', (t) => {
    using db = new Database(':memory:');
    db.exec('CREATE TABLE storage(key TEXT, val TEXT)');
    const stmt = db.prepare('SELECT * FROM storage');
    stmt[Symbol.dispose]();
    t.assert.throws(() => stmt.get(), {
      code: 'ERR_INVALID_STATE',
      message: /statement has been finalized/,
    });
  });

  test('does not throw on an already-finalized statement', () => {
    using db = new Database(':memory:');
    const stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)');
    stmt[Symbol.dispose]();
    stmt[Symbol.dispose]();
  });

  test('works with a using declaration', (t) => {
    using db = new Database(':memory:');
    db.exec('CREATE TABLE storage(key TEXT, val TEXT)');
    let captured;
    {
      using stmt = db.prepare('SELECT * FROM storage');
      captured = stmt;
      t.assert.deepStrictEqual(stmt.all(), []);
    }
    t.assert.throws(() => captured.get(), {
      code: 'ERR_INVALID_STATE',
      message: /statement has been finalized/,
    });
  });

  test('closing the database after dispose does not double-finalize', () => {
    using db = new Database(':memory:');
    const stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)');
    stmt[Symbol.dispose]();
    db.close();
  });
});

suite('options.persistent', () => {
  test('statement executes correctly when persistent is true', (t) => {
    using db = new Database(':memory:');
    db.exec('CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;');
    db.exec('INSERT INTO data (key, val) VALUES (1, 42);');
    using stmt = db.prepare('SELECT val FROM data', { persistent: true });
    t.assert.deepStrictEqual(stmt.get(), { __proto__: null, val: 42 });
  });

  test('statement executes correctly when persistent is false', (t) => {
    using db = new Database(':memory:');
    db.exec('CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;');
    db.exec('INSERT INTO data (key, val) VALUES (1, 42);');
    using stmt = db.prepare('SELECT val FROM data', { persistent: false });
    t.assert.deepStrictEqual(stmt.get(), { __proto__: null, val: 42 });
  });

  test('throws when input is not a boolean', (t) => {
    using db = new Database(':memory:');
    t.assert.throws(() => {
      db.prepare('SELECT 1', { persistent: 'yes' });
    }, {
      code: 'ERR_INVALID_ARG_TYPE',
      message: /The "options\.persistent" argument must be a boolean/,
    });
  });

  test('can be combined with other options', (t) => {
    using db = new Database(':memory:');
    db.exec('CREATE TABLE data(key INTEGER PRIMARY KEY, val INTEGER) STRICT;');
    db.exec('INSERT INTO data (key, val) VALUES (1, 42);');
    using stmt = db.prepare(
      'SELECT val FROM data',
      { persistent: true, readBigInts: true }
    );
    t.assert.deepStrictEqual(stmt.get(), { __proto__: null, val: 42n });
  });
});