File size: 3,477 Bytes
dcedb25
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
import { afterEach, describe, expect, it } from 'vitest';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { createRequire } from 'node:module';

// node:sqlite ships in Node 22.13+. CI also tests Node 20, where the built-in
// module (and therefore the Android fallback) does not exist — skip the tests
// that open a database there; the factory-selection test still runs everywhere.
const hasNodeSqlite = (() => {
  try {
    createRequire(import.meta.url)('node:sqlite');
    return true;
  } catch {
    return false;
  }
})();
const itWithSqlite = it.skipIf(!hasNodeSqlite);
import { connectDb, defaultDbFactory } from '../../db/index.js';
import { nodeSqliteFactory } from '../../db/node-sqlite.js';
import { runMigrationsSync } from '../../db/migrate/runner.js';
import type { Db } from '../../db/types.js';

describe('node:sqlite Android fallback', () => {
  let db: Db | undefined;
  const tempDirs: string[] = [];

  afterEach(() => {
    db?.close?.();
    db = undefined;
    for (const dir of tempDirs.splice(0)) fs.rmSync(dir, { recursive: true, force: true });
  });

  it('is selected only for Android', () => {
    expect(defaultDbFactory('android')).toBe(nodeSqliteFactory);
    expect(defaultDbFactory('darwin')).not.toBe(nodeSqliteFactory);
    expect(defaultDbFactory('linux')).not.toBe(nodeSqliteFactory);
  });

  itWithSqlite('runs the complete application migration set', () => {
    db = connectDb(':memory:', { factory: nodeSqliteFactory, ensureDir: false });
    runMigrationsSync(db, 'up');

    const tables = db.prepare("SELECT name FROM sqlite_master WHERE type = 'table'").all() as { name: string }[];
    expect(tables.map((row) => row.name)).toContain('api_keys');
    expect((db.prepare('SELECT COUNT(*) AS count FROM models').get() as { count: number }).count).toBeGreaterThan(0);
  });

  itWithSqlite('opens a file-backed database with WAL and file metadata', () => {
    const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'freellmapi-node-sqlite-'));
    tempDirs.push(dir);
    const dbPath = path.join(dir, 'freeapi.db');

    db = connectDb(dbPath, { factory: nodeSqliteFactory });
    db.exec('CREATE TABLE sample (id INTEGER PRIMARY KEY, value TEXT NOT NULL)');
    db.prepare('INSERT INTO sample (value) VALUES (?)').run('persisted');

    expect(db.name).toBe(dbPath);
    expect(db.memory).toBe(false);
    expect(fs.existsSync(dbPath)).toBe(true);
    expect(db.pragma('journal_mode')).toEqual([{ journal_mode: 'wal' }]);
  });

  itWithSqlite('supports nested transaction commit and rollback semantics', () => {
    db = connectDb(':memory:', { factory: nodeSqliteFactory, ensureDir: false });
    db.exec('CREATE TABLE items (id INTEGER PRIMARY KEY, value TEXT NOT NULL)');

    const outer = db.transaction(() => {
      db!.prepare('INSERT INTO items (value) VALUES (?)').run('outer');
      db!.transaction(() => {
        db!.prepare('INSERT INTO items (value) VALUES (?)').run('inner');
      })();

      try {
        db!.transaction(() => {
          db!.prepare('INSERT INTO items (value) VALUES (?)').run('rolled-back');
          throw new Error('rollback inner savepoint');
        })();
      } catch {
        // The outer transaction remains usable and commits.
      }
    });

    outer();

    const rows = db.prepare('SELECT value FROM items ORDER BY id').all() as { value: string }[];
    expect(rows.map((row) => row.value)).toEqual(['outer', 'inner']);
  });
});