current commit
260926.1 · sqlite: rename DatabaseSync and StatementSync
'use strict';

const { skipIfSQLiteMissing } = require('../common');
skipIfSQLiteMissing();
const assert = require('node:assert');
const { test } = require('node:test');
const { Database } = require('node:sqlite');

for (const method of ['all', 'get', 'run', 'iterate']) {
  test(`database.close() from a UDF during statement.${method}()`, () => {
    const db = new Database(':memory:');
    db.exec(`
      CREATE TABLE data (value INTEGER);
      INSERT INTO data VALUES (1), (2), (3);
    `);

    db.function('close_db', (value) => {
      db.close();
      return value;
    });

    const statement = db.prepare('SELECT close_db(value) FROM data');
    assert.throws(() => {
      if (method === 'iterate') {
        for (const row of statement.iterate()) {
          assert.ok(row);
        }
      } else {
        statement[method]();
      }
    }, {
      code: 'ERR_INVALID_STATE',
      message: 'database cannot be closed while in a callback',
    });

    assert.strictEqual(db.isOpen, true);
    db.close();
  });

  // Finalizing the statement being stepped frees the virtual machine that
  // sqlite3_step() is still running, so this must throw rather than crash.
  test(`statement.close() from a UDF during statement.${method}()`, () => {
    const db = new Database(':memory:');
    db.exec(`
      CREATE TABLE data (value INTEGER);
      INSERT INTO data VALUES (1), (2), (3);
    `);

    let statement;
    db.function('close_stmt', (value) => {
      statement.close();
      return value;
    });

    statement = db.prepare('SELECT close_stmt(value) FROM data');
    assert.throws(() => {
      if (method === 'iterate') {
        for (const row of statement.iterate()) {
          assert.ok(row);
        }
      } else {
        statement[method]();
      }
    }, {
      code: 'ERR_INVALID_STATE',
      message: 'statement is already being executed',
    });

    db.close();
  });

  // Re-running the statement being stepped resets its virtual machine
  // mid-execution, which is the same use-after-free as finalizing it.
  for (const reentrant of ['run', 'get', 'all', 'iterate']) {
    test(`statement.${reentrant}() from a UDF during ` +
         `statement.${method}()`, () => {
      const db = new Database(':memory:');
      db.exec(`
        CREATE TABLE data (value INTEGER, padding TEXT);
        INSERT INTO data VALUES (1, '${'x'.repeat(400)}'),
                                (2, '${'y'.repeat(400)}'),
                                (3, '${'z'.repeat(400)}');
      `);

      let statement;
      let thrown;
      db.function('reenter', (value) => {
        if (thrown === undefined) {
          try {
            statement[reentrant]();
            thrown = null;
          } catch (err) {
            thrown = err;
          }
        }
        return value;
      });

      statement = db.prepare('SELECT reenter(value), padding FROM data');
      if (method === 'iterate') {
        for (const row of statement.iterate()) {
          assert.ok(row);
        }
      } else {
        statement[method]();
      }

      assert.ok(thrown, `${reentrant}() was not rejected`);
      assert.strictEqual(thrown.code, 'ERR_INVALID_STATE');
      assert.strictEqual(thrown.message, 'statement is already being executed');

      db.close();
    });
  }

  // Tag store methods resolve to a cached statement, which may be the one
  // currently being stepped. Each reentrant method has its own guard, so all
  // four are exercised.
  for (const reentrant of ['run', 'get', 'all', 'iterate']) {
    test(`tag store ${reentrant} reentry during statement.${method}()`, () => {
      const db = new Database(':memory:');
      const sql = db.createTagStore(10);
      db.exec(`
        CREATE TABLE data (value INTEGER, padding TEXT);
        INSERT INTO data VALUES (1, '${'x'.repeat(400)}'),
                                (2, '${'y'.repeat(400)}');
      `);

      let thrown;
      db.function('reenter_tag', (value) => {
        if (thrown === undefined) {
          try {
            // The identical tagged literal resolves to the same cached
            // statement that is mid-execution.
            // All four reject at call time, iterate() included, so the
            // result is never consumed.
            // eslint-disable-next-line no-unused-expressions
            sql[reentrant]`SELECT reenter_tag(value), padding FROM data`;
            thrown = null;
          } catch (err) {
            thrown = err;
          }
        }
        return value;
      });

      if (method === 'iterate') {
        for (const row of sql.iterate`SELECT reenter_tag(value), padding FROM data`) {
          assert.ok(row);
        }
      } else {
        // eslint-disable-next-line no-unused-expressions
        sql[method]`SELECT reenter_tag(value), padding FROM data`;
      }

      assert.ok(thrown, `tag store ${reentrant} reentry was not rejected`);
      assert.strictEqual(thrown.code, 'ERR_INVALID_STATE');
      assert.strictEqual(thrown.message,
                         'statement is already being executed');

      db.close();
    });
  }

  // A UDF may prepare and finalize its own helper statements. Only the
  // statement being stepped is off limits.
  test(`UDF finalizes its own statement during statement.${method}()`, () => {
    const db = new Database(':memory:');
    db.exec(`
      CREATE TABLE data (value INTEGER);
      INSERT INTO data VALUES (1), (2), (3);
      CREATE TABLE lookup (key INTEGER, label TEXT);
      INSERT INTO lookup VALUES (1, 'one'), (2, 'two'), (3, 'three');
    `);

    db.function('lookup_label', (value) => {
      const helper = db.prepare('SELECT label FROM lookup WHERE key = ?');
      const label = helper.get(value).label;
      helper.close();
      return label;
    });

    const statement = db.prepare('SELECT lookup_label(value) AS l FROM data');
    if (method === 'iterate') {
      const labels = [];
      for (const row of statement.iterate()) {
        labels.push(row.l);
      }
      assert.deepStrictEqual(labels, ['one', 'two', 'three']);
    } else if (method === 'all') {
      assert.deepStrictEqual(statement.all().map((r) => r.l),
                             ['one', 'two', 'three']);
    } else if (method === 'get') {
      assert.strictEqual(statement.get().l, 'one');
    } else {
      statement.run();
    }

    db.close();
  });
}

// iterator.return() resets the statement it is iterating, and next() steps it
// again, so both reach the virtual machine that is mid-execution.
for (const op of ['next', 'return']) {
  test(`iterator.${op}() from a UDF during iteration`, () => {
    const db = new Database(':memory:');
    db.exec(`
      CREATE TABLE data (value INTEGER, padding TEXT);
      INSERT INTO data VALUES (1, '${'x'.repeat(400)}'),
                              (2, '${'y'.repeat(400)}'),
                              (3, '${'z'.repeat(400)}');
    `);

    let iterator;
    let thrown;
    db.function('reenter_iter', (value) => {
      if (thrown === undefined && iterator !== undefined) {
        try {
          iterator[op]();
          thrown = null;
        } catch (err) {
          thrown = err;
        }
      }
      return value;
    });

    const statement = db.prepare(
      'SELECT reenter_iter(value) AS v, padding FROM data');
    iterator = statement.iterate();
    for (const row of iterator) {
      assert.ok(row);
    }

    assert.ok(thrown, `iterator.${op}() was not rejected`);
    assert.strictEqual(thrown.code, 'ERR_INVALID_STATE');
    assert.strictEqual(thrown.message, 'statement is already being executed');

    db.close();
  });
}