import { SQL } from "bun"; import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, mock, test } from "bun:test"; import { tempDirWithFiles } from "harness"; import { existsSync } from "node:fs"; import { rm, stat } from "node:fs/promises"; import { join } from "node:path"; import path from "path"; describe("Connection & Initialization", () => { describe("common default connection strings", () => { test("should parse common connection strings", () => { const memory = new SQL(":memory:"); expect(memory.options.adapter).toBe("sqlite"); expect(memory.options.filename).toBe(":memory:"); const myapp = new SQL("sqlite://myapp.db"); expect(myapp.options.adapter).toBe("sqlite"); expect(myapp.options.filename).toBe("myapp.db"); const postgres = new SQL("postgres://user1:pass2@localhost:5432/mydb"); expect(postgres.options.adapter).not.toBe("sqlite"); }); }); test("should connect to in-memory SQLite database", async () => { const sql = new SQL("sqlite://:memory:"); expect(sql).toBeDefined(); expect(sql.options.adapter).toBe("sqlite"); await sql.close(); }); test("should connect to file-based SQLite database", async () => { const dir = tempDirWithFiles("sqlite-db-test", {}); const dbPath = path.join(dir, "test.db"); const sql = new SQL(`sqlite://${dbPath}`); expect(sql).toBeDefined(); expect(sql.options.adapter).toBe("sqlite"); expect(sql.options.filename).toBe(dbPath); await sql.close(); await rm(dir, { recursive: true }); }); test("should handle connection with options object", async () => { const sql = new SQL({ adapter: "sqlite", filename: ":memory:", }); expect(sql.options.adapter).toBe("sqlite"); expect(sql.options.filename).toBe(":memory:"); await sql`CREATE TABLE test (id INTEGER)`; await sql`INSERT INTO test VALUES (1)`; const result = await sql`SELECT * FROM test`; expect(result).toHaveLength(1); await sql.close(); }); test("onconnect and onclose callbacks are invoked for SQLite", async () => { const onconnect = mock((err?: any) => {}); const onclose = mock((err?: any) => {}); const sql = new SQL({ adapter: "sqlite", filename: ":memory:", onconnect, onclose, }); // onconnect should run during creation with null error expect(onconnect).toHaveBeenCalled(); const onconnectArg = onconnect.mock.calls[0]?.[0]; expect(onconnectArg === null).toBe(true); await sql`SELECT 1 as x`; await sql.close(); expect(onclose).toHaveBeenCalled(); const oncloseArg = onclose.mock.calls[0]?.[0]; expect(oncloseArg instanceof Error).toBe(true); }); test("onconnect receives Error when open fails (readonly non-existent)", async () => { const dir = tempDirWithFiles("sqlite-onconnect-error", {}); const dbPath = join(dir, "missing.db"); const onconnect = mock((err?: any) => {}); const onclose = mock((err?: any) => {}); const sql = new SQL({ adapter: "sqlite", filename: dbPath, // open in readonly so creation is not allowed readonly: true, onconnect, onclose, }); // Should have been called with an Error expect(onconnect).toHaveBeenCalled(); const arg = onconnect.mock.calls[0]?.[0]; expect(arg instanceof Error).toBe(true); await sql.close(); expect(onclose).toHaveBeenCalled(); await rm(dir, { recursive: true }); }); test("should create database file if it doesn't exist", async () => { const dir = tempDirWithFiles("sqlite-create-test", {}); const dbPath = path.join(dir, "new.db"); const sql = new SQL(`sqlite://${dbPath}`); await sql`CREATE TABLE test (id INTEGER)`; const stats = await stat(dbPath); expect(stats.isFile()).toBe(true); await sql.close(); await rm(dir, { recursive: true }); }); test("should work with relative paths", async () => { const dir = tempDirWithFiles("sqlite-test", {}); const sql = new SQL({ adapter: "sqlite", filename: path.join(dir, "test.db"), }); await sql`CREATE TABLE test (id INTEGER)`; const stats = await stat(path.join(dir, "test.db")); expect(stats.isFile()).toBe(true); await sql.close(); await rm(dir, { recursive: true }); }); describe("Environment Variable Handling", () => { let originalEnv: NodeJS.ProcessEnv; beforeEach(() => { originalEnv = { ...Bun.env }; }); afterEach(() => { // Restore original env vars for (const key in Bun.env) { if (!(key in originalEnv)) { delete Bun.env[key]; } } for (const key in originalEnv) { Bun.env[key] = originalEnv[key]; } }); test("should use DATABASE_URL for SQLite when it's a SQLite URL", async () => { const dir = tempDirWithFiles("sqlite-env-test", {}); const dbPath = path.join(dir, "env-test.db"); Bun.env.DATABASE_URL = `sqlite://${dbPath}`; const sql = new SQL(); expect(sql.options.adapter).toBe("sqlite"); expect(sql.options.filename).toBe(dbPath); await sql`CREATE TABLE test (id INTEGER)`; const stats = await stat(dbPath); expect(stats.isFile()).toBe(true); await sql.close(); await rm(dir, { recursive: true }); }); test("should handle DATABASE_URL with :memory:", async () => { Bun.env.DATABASE_URL = "sqlite://:memory:"; const sql = new SQL(); expect(sql.options.adapter).toBe("sqlite"); expect(sql.options.filename).toBe(":memory:"); await sql`CREATE TABLE test (id INTEGER)`; await sql`INSERT INTO test VALUES (1)`; const result = await sql`SELECT * FROM test`; expect(result).toHaveLength(1); await sql.close(); }); test("should handle SQLITE_URL env var when specified", async () => { const dir = tempDirWithFiles("sqlite-url-test", {}); const dbPath = path.join(dir, "sqlite-url.db"); // This doesn't exist in the current implementation but testing anyway Bun.env.SQLITE_URL = `sqlite://${dbPath}`; Bun.env.DATABASE_URL = "postgres://localhost/test"; // Should be ignored when adapter is sqlite const sql = new SQL("sqlite://:memory:"); expect(sql.options.adapter).toBe("sqlite"); expect(sql.options.filename).toBe(":memory:"); await sql.close(); await rm(dir, { recursive: true }); }); test("should NOT use POSTGRES_URL for SQLite", async () => { Bun.env.POSTGRES_URL = "postgres://user:pass@localhost:5432/mydb"; // When explicitly using sqlite adapter, POSTGRES_URL should be ignored const sql = new SQL({ adapter: "sqlite", filename: ":memory:" }); expect(sql.options.adapter).toBe("sqlite"); expect(sql.options.filename).toBe(":memory:"); await sql.close(); }); test("should NOT use PGURL for SQLite", async () => { Bun.env.PGURL = "postgres://user:pass@localhost:5432/mydb"; const sql = new SQL({ adapter: "sqlite", filename: ":memory:" }); expect(sql.options.adapter).toBe("sqlite"); expect(sql.options.filename).toBe(":memory:"); await sql.close(); }); }); describe("options.url overrides first argument", () => { test("should use options.url for postgres when it overrides first argument", () => { const sql = new SQL("http://wrong-host/db", { adapter: "postgres", url: "postgres://correct-host:5432/mydb", }); expect(sql.options.adapter).toBe("postgres"); expect(sql.options.hostname).toBe("correct-host"); expect(sql.options.port).toBe(5432); expect(sql.options.database).toBe("mydb"); sql.close(); }); test("should use options.url for mysql when it overrides first argument", () => { const sql = new SQL("http://wrong-host/wrongdb", { adapter: "mysql", url: "mysql://user:pass@mysql-host:3306/correctdb", }); expect(sql.options.adapter).toBe("mysql"); expect(sql.options.hostname).toBe("mysql-host"); expect(sql.options.port).toBe(3306); expect(sql.options.database).toBe("correctdb"); sql.close(); }); test("should use options.url for mariadb when it overrides first argument", () => { const sql = new SQL("http://wrong-host:1234/wrongdb", { adapter: "mariadb", url: "mariadb://maria-host:3307/mariadb", }); expect(sql.options.adapter).toBe("mariadb"); expect(sql.options.hostname).toBe("maria-host"); expect(sql.options.port).toBe(3307); expect(sql.options.database).toBe("mariadb"); sql.close(); }); test("should use first argument when options.url is not provided", () => { const sql = new SQL("postgres://first-arg-host:5432/firstdb", { adapter: "postgres", }); expect(sql.options.adapter).toBe("postgres"); expect(sql.options.hostname).toBe("first-arg-host"); expect(sql.options.port).toBe(5432); expect(sql.options.database).toBe("firstdb"); sql.close(); }); }); describe("file:// Protocol URLs", () => { test("should handle file:// URLs", async () => { const dir = tempDirWithFiles("file-protocol-test", {}); const dbPath = path.join(dir, "file-test.db"); const sql = new SQL(`file://${dbPath}`); expect(sql.options.adapter).toBe("sqlite"); expect(sql.options.filename).toBe(dbPath); await sql`CREATE TABLE test (id INTEGER)`; const stats = await stat(dbPath); expect(stats.isFile()).toBe(true); await sql.close(); await rm(dir, { recursive: true }); }); test("should handle file: URLs without slashes", async () => { const dir = tempDirWithFiles("file-no-slash-test", {}); const dbPath = path.join(dir, "file-noslash.db"); const sql = new SQL(`file:${dbPath}`); expect(sql.options.adapter).toBe("sqlite"); expect(sql.options.filename).toBe(dbPath); await sql`CREATE TABLE test (id INTEGER)`; const stats = await stat(dbPath); expect(stats.isFile()).toBe(true); await sql.close(); await rm(dir, { recursive: true }); }); test("should handle file:// with :memory:", () => { const sql = new SQL("file://:memory:"); expect(sql.options.adapter).toBe("sqlite"); expect(sql.options.filename).toBe(":memory:"); sql.close(); }); }); describe("Query Parameters in SQLite URLs", () => { test("should handle mode=ro (readonly)", async () => { const dir = tempDirWithFiles("readonly-test", {}); const dbPath = path.join(dir, "readonly.db"); // First create a database const createSql = new SQL(`sqlite://${dbPath}`); await createSql`CREATE TABLE test (id INTEGER)`; await createSql`INSERT INTO test VALUES (1)`; await createSql.close(); // Now open in readonly mode const sql = new SQL(`sqlite://${dbPath}?mode=ro`); expect(sql.options.adapter).toBe("sqlite"); expect(sql.options.readonly).toBe(true); // Should be able to read const result = await sql`SELECT * FROM test`; expect(result).toHaveLength(1); expect(async () => await sql`INSERT INTO test VALUES (2)`.execute()).toThrowErrorMatchingInlineSnapshot( `"attempt to write a readonly database"`, ); await sql.close(); await rm(dir, { recursive: true }); }); test("should handle mode=rw (read-write)", async () => { const dir = tempDirWithFiles("readwrite-test", {}); const dbPath = path.join(dir, "readwrite.db"); // Create database first const createSql = new SQL(`sqlite://${dbPath}`); await createSql`CREATE TABLE test (id INTEGER)`; await createSql.close(); const sql = new SQL(`sqlite://${dbPath}?mode=rw`); expect(sql.options.adapter).toBe("sqlite"); expect(sql.options.readonly).toBe(false); await sql`INSERT INTO test VALUES (1)`; const result = await sql`SELECT * FROM test`; expect(result).toHaveLength(1); await sql.close(); await rm(dir, { recursive: true }); }); test("should handle mode=rwc (read-write-create)", async () => { const dir = tempDirWithFiles("rwc-test", {}); const dbPath = path.join(dir, "rwc.db"); const sql = new SQL(`sqlite://${dbPath}?mode=rwc`); expect(sql.options.adapter).toBe("sqlite"); expect(sql.options.readonly).toBe(false); expect(sql.options.create).toBe(true); await sql`CREATE TABLE test (id INTEGER)`; await sql`INSERT INTO test VALUES (1)`; const result = await sql`SELECT * FROM test`; expect(result).toHaveLength(1); await sql.close(); await rm(dir, { recursive: true }); }); test("should handle multiple query parameters", async () => { const dir = tempDirWithFiles("multi-param-test", {}); const dbPath = path.join(dir, "multi.db"); // Note: Only mode is supported for SQLite, other params should be ignored const sql = new SQL(`sqlite://${dbPath}?mode=rwc&cache=shared&timeout=5000`); expect(sql.options.adapter).toBe("sqlite"); expect(sql.options.create).toBe(true); await sql`CREATE TABLE test (id INTEGER)`; await sql.close(); await rm(dir, { recursive: true }); }); test("should ignore fragment in URL", async () => { const dir = tempDirWithFiles("fragment-test", {}); const dbPath = path.join(dir, "test#file.db"); // # is a valid filename character in SQLite const sql = new SQL(`sqlite://${dbPath}`); expect(sql.options.adapter).toBe("sqlite"); expect(sql.options.filename).toBe(dbPath); await sql`CREATE TABLE test (id INTEGER)`; const stats = await stat(dbPath); expect(stats.isFile()).toBe(true); await sql.close(); await rm(dir, { recursive: true }); }); }); describe("Special Characters in Filenames", () => { test("should handle spaces in filename", async () => { const dir = tempDirWithFiles("space-test", {}); const dbPath = path.join(dir, "test database.db"); const sql = new SQL(`sqlite://${dbPath}`); expect(sql.options.adapter).toBe("sqlite"); expect(sql.options.filename).toBe(dbPath); await sql`CREATE TABLE test (id INTEGER)`; const stats = await stat(dbPath); expect(stats.isFile()).toBe(true); await sql.close(); await rm(dir, { recursive: true }); }); test("should handle special characters # and % in filename", async () => { const dir = tempDirWithFiles("special-chars-test", {}); const dbPath = path.join(dir, "test#123%data.db"); const sql = new SQL(`sqlite://${dbPath}`); expect(sql.options.adapter).toBe("sqlite"); expect(sql.options.filename).toBe(dbPath); await sql`CREATE TABLE test (id INTEGER)`; const stats = await stat(dbPath); expect(stats.isFile()).toBe(true); await sql.close(); await rm(dir, { recursive: true }); }); test("should handle unicode characters in filename", async () => { const dir = tempDirWithFiles("unicode-test", {}); const dbPath = path.join(dir, "测试数据库.db"); const sql = new SQL(`sqlite://${dbPath}`); expect(sql.options.adapter).toBe("sqlite"); expect(sql.options.filename).toBe(dbPath); await sql`CREATE TABLE test (id INTEGER)`; const stats = await stat(dbPath); expect(stats.isFile()).toBe(true); await sql.close(); await rm(dir, { recursive: true }); }); test("should handle dots in filename", async () => { const dir = tempDirWithFiles("dots-test", {}); const dbPath = path.join(dir, "my.app.v2.0.db"); const sql = new SQL(`sqlite://${dbPath}`); expect(sql.options.adapter).toBe("sqlite"); expect(sql.options.filename).toBe(dbPath); await sql`CREATE TABLE test (id INTEGER)`; const stats = await stat(dbPath); expect(stats.isFile()).toBe(true); await sql.close(); await rm(dir, { recursive: true }); }); }); describe("Path Handling", () => { test("should handle absolute paths", async () => { const dir = tempDirWithFiles("absolute-test", {}); const dbPath = path.resolve(dir, "absolute.db"); const sql = new SQL(`sqlite://${dbPath}`); expect(sql.options.adapter).toBe("sqlite"); expect(sql.options.filename).toBe(dbPath); await sql`CREATE TABLE test (id INTEGER)`; const stats = await stat(dbPath); expect(stats.isFile()).toBe(true); await sql.close(); await rm(dir, { recursive: true }); }); test("should handle relative paths with ./", async () => { const dir = tempDirWithFiles("relative-dot-test", {}); const originalCwd = process.cwd(); try { process.chdir(dir); const sql = new SQL("sqlite://./test.db"); expect(sql.options.adapter).toBe("sqlite"); expect(sql.options.filename).toBe("./test.db"); await sql`CREATE TABLE test (id INTEGER)`; const stats = await stat(path.join(dir, "test.db")); expect(stats.isFile()).toBe(true); await sql.close(); } finally { process.chdir(originalCwd); await rm(dir, { recursive: true }); } }); test("should handle paths with ../", async () => { const parentDir = tempDirWithFiles("parent-test", {}); const childDir = path.join(parentDir, "child"); await Bun.$`mkdir -p ${childDir}`; const originalCwd = process.cwd(); try { process.chdir(childDir); const sql = new SQL("sqlite://../parent.db"); expect(sql.options.adapter).toBe("sqlite"); expect(sql.options.filename).toBe("../parent.db"); await sql`CREATE TABLE test (id INTEGER)`; const stats = await stat(path.join(parentDir, "parent.db")); expect(stats.isFile()).toBe(true); await sql.close(); } finally { process.chdir(originalCwd); await rm(parentDir, { recursive: true }); } }); test("should handle nested paths", async () => { const dir = tempDirWithFiles("nested-test", {}); const nestedPath = path.join(dir, "data", "databases", "app.db"); await Bun.$`mkdir -p ${path.dirname(nestedPath)}`; const sql = new SQL(`sqlite://${nestedPath}`); expect(sql.options.adapter).toBe("sqlite"); expect(sql.options.filename).toBe(nestedPath); await sql`CREATE TABLE test (id INTEGER)`; const stats = await stat(nestedPath); expect(stats.isFile()).toBe(true); await sql.close(); await rm(dir, { recursive: true }); }); }); describe("Empty and Invalid URLs", () => { test("should handle empty string with adapter specified", () => { const sql = new SQL("", { adapter: "sqlite" }); expect(sql.options.adapter).toBe("sqlite"); expect(sql.options.filename).toBe(":memory:"); sql.close(); }); test("should handle null/undefined with adapter specified", () => { const sql1 = new SQL(undefined as never, { adapter: "sqlite" }); expect(sql1.options.adapter).toBe("sqlite"); expect(sql1.options.filename).toBe(":memory:"); sql1.close(); const sql2 = new SQL({ adapter: "sqlite" }); expect(sql2.options.adapter).toBe("sqlite"); expect(sql2.options.filename).toBe(":memory:"); sql2.close(); }); test("should handle sqlite: without path", () => { const sql = new SQL("sqlite:"); expect(sql.options.adapter).toBe("sqlite"); expect(sql.options.filename).toBe(":memory:"); sql.close(); }); test("should handle sqlite:// without path", () => { const sql = new SQL("sqlite://"); expect(sql.options.adapter).toBe("sqlite"); expect(sql.options.filename).toBe(":memory:"); sql.close(); }); test("should handle just :memory: without prefix", () => { const sql = new SQL(":memory:"); expect(sql.options.adapter).toBe("sqlite"); expect(sql.options.filename).toBe(":memory:"); sql.close(); }); test("should handle sqlite:memory without :", () => { const sql = new SQL("sqlite:memory"); expect(sql.options.adapter).toBe("sqlite"); expect(sql.options.filename).toBe(":memory:"); sql.close(); }); }); describe("Mixed Configurations", () => { test("should prefer explicit URL over env var", async () => { const dir = tempDirWithFiles("explicit-test", {}); const envPath = path.join(dir, "env.db"); const explicitPath = path.join(dir, "explicit.db"); Bun.env.DATABASE_URL = `sqlite://${envPath}`; const sql = new SQL(`sqlite://${explicitPath}`); expect(sql.options.adapter).toBe("sqlite"); expect(sql.options.filename).toBe(explicitPath); await sql`CREATE TABLE test (id INTEGER)`; expect(existsSync(explicitPath)).toBe(true); expect(existsSync(envPath)).toBe(false); await sql.close(); delete Bun.env.DATABASE_URL; await rm(dir, { recursive: true }); }); test("should prefer options object over URL", async () => { const dir = tempDirWithFiles("options-override-test", {}); const urlPath = path.join(dir, "url.db"); const optionsPath = path.join(dir, "options.db"); const sql = new SQL(`sqlite://${urlPath}`, { adapter: "sqlite", filename: optionsPath, } as { adapter: "sqlite" }); expect(sql.options.adapter).toBe("sqlite"); expect(sql.options.filename).toBe(optionsPath); await sql`CREATE TABLE test (id INTEGER)`; expect(existsSync(optionsPath)).toBe(true); expect(existsSync(urlPath)).toBe(false); await sql.close(); await rm(dir, { recursive: true }); }); test("should handle URL in options object", async () => { const dir = tempDirWithFiles("url-in-options-test", {}); const dbPath = path.join(dir, "url-options.db"); const sql = new SQL({ adapter: "sqlite", filename: `sqlite://${dbPath}`, }); expect(sql.options.adapter).toBe("sqlite"); expect(sql.options.filename).toBe(dbPath); await sql`CREATE TABLE test (id INTEGER)`; const stats = await stat(dbPath); expect(stats.isFile()).toBe(true); await sql.close(); await rm(dir, { recursive: true }); }); }); describe("Error Cases", () => { test("should throw for unsupported adapter", () => { expect(() => new SQL({ adapter: "mssql" as any })).toThrowErrorMatchingInlineSnapshot( `"Unsupported adapter: mssql. Supported adapters: "postgres", "sqlite", "mysql", "mariadb""`, ); }); test("should interpret ambiguous strings as postgres connection", () => { // This gets interpreted as postgres with hostname "localhost" and database "432/mydb" const sql = new SQL("localhost:5432/mydb"); expect(sql.options.adapter).toBe("postgres"); sql.close(); }); test("SQLite readonly mode creates connection but fails on missing file access", async () => { const dir = tempDirWithFiles("ro-nonexist-test", {}); const dbPath = path.join(dir, "nonexistent.db"); const sql = new SQL(`sqlite://${dbPath}?mode=ro`); expect(sql.options.adapter).toBe("sqlite"); expect(sql.options.readonly).toBe(true); expect(sql.options.filename).toBe(dbPath); expect(async () => await sql`SELECT 1`.execute()).toThrowErrorMatchingInlineSnapshot( `"unable to open database file"`, ); await sql.close(); await rm(dir, { recursive: true }); }); }); }); describe("Data Types & Values", () => { let sql: SQL; beforeAll(async () => { sql = new SQL("sqlite://:memory:"); }); afterAll(async () => { await sql?.close(); }); test("handles NULL values", async () => { await sql`CREATE TABLE nulls (id INTEGER, value TEXT)`; await sql`INSERT INTO nulls (id, value) VALUES (1, ${null})`; const result = await sql`SELECT * FROM nulls`; expect(result[0].value).toBeNull(); }); test("handles INTEGER values", async () => { const values = [0, 1, -1, 2147483647, -2147483648]; await sql`CREATE TABLE integers (value INTEGER)`; for (const val of values) { await sql`INSERT INTO integers VALUES (${val})`; } const results = await sql<{ value: number }[]>`SELECT * FROM integers`; expect(results.map(r => r.value)).toEqual(values); }); test("INSERT containing literal 'RETURNING' should not be treated as RETURNING", async () => { await sql`CREATE TABLE insert_literal_returning (name TEXT)`; const res = await sql`INSERT INTO insert_literal_returning (name) VALUES ('RETURNING')`; expect(res.command).toBe("INSERT"); expect(res.count).toBe(1); const rows = await sql`SELECT COUNT(*) AS c FROM insert_literal_returning`; expect(rows[0].c).toBe(1); }); test("handles REAL values", async () => { const values = [0.0, 1.1, -1.1, 3.14159, Number.MAX_SAFE_INTEGER + 0.1]; await sql`CREATE TABLE reals (value REAL)`; for (const val of values) { await sql`INSERT INTO reals VALUES (${val})`; } const results = await sql`SELECT * FROM reals`; results.forEach((r, i) => { expect(r.value).toBeCloseTo(values[i], 10); }); }); test("handles TEXT values", async () => { const values = ["", "hello", "hello world", "unicode: 你好 🌍", "'quotes'", '"double quotes"']; await sql`CREATE TABLE texts (value TEXT)`; for (const val of values) { await sql`INSERT INTO texts VALUES (${val})`; } const results = await sql`SELECT * FROM texts`; expect(results.map(r => r.value)).toEqual(values); }); test("handles BLOB values", async () => { const buffer = Buffer.from([0x00, 0x01, 0x02, 0x03, 0xff]); await sql`CREATE TABLE blobs (value BLOB)`; await sql`INSERT INTO blobs VALUES (${buffer})`; const result = await sql`SELECT * FROM blobs`; expect(Buffer.from(result[0].value)).toEqual(buffer); }); test("handles boolean values (stored as INTEGER)", async () => { await sql`CREATE TABLE bools (value INTEGER)`; await sql`INSERT INTO bools VALUES (${true}), (${false})`; const results = await sql`SELECT * FROM bools`; expect(results[0].value).toBe(1); expect(results[1].value).toBe(0); }); test("handles Date values (stored as TEXT)", async () => { const date = new Date("2024-01-01T12:00:00Z"); await sql`CREATE TABLE dates (value TEXT)`; await sql`INSERT INTO dates VALUES (${date.toISOString()})`; const result = await sql`SELECT * FROM dates`; expect(new Date(result[0].value)).toEqual(date); }); test("handles JSON values (stored as TEXT)", async () => { const jsonData = { name: "Test", values: [1, 2, 3], nested: { key: "value" } }; await sql`CREATE TABLE json_data (value TEXT)`; await sql`INSERT INTO json_data VALUES (${JSON.stringify(jsonData)})`; const result = await sql`SELECT * FROM json_data`; expect(JSON.parse(result[0].value)).toEqual(jsonData); }); }); describe("Query Execution", () => { let sql: SQL; beforeAll(async () => { sql = new SQL("sqlite://:memory:"); }); afterAll(async () => { await sql?.close(); }); test("CREATE TABLE", async () => { const result = await sql`CREATE TABLE users ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, email TEXT UNIQUE, age INTEGER CHECK (age >= 0), created_at TEXT DEFAULT CURRENT_TIMESTAMP )`; expect(result.command).toBe("CREATE"); }); test("INSERT with RETURNING", async () => { await sql`CREATE TABLE items (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT)`; const result = await sql`INSERT INTO items (name) VALUES (${"Item1"}) RETURNING *`; expect(result).toHaveLength(1); expect(result[0].id).toBe(1); expect(result[0].name).toBe("Item1"); expect(result.command).toBe("INSERT"); }); test("UPDATE with affected rows", async () => { await sql`CREATE TABLE products (id INTEGER PRIMARY KEY, price REAL)`; await sql`INSERT INTO products VALUES (1, 10.0), (2, 20.0), (3, 30.0)`; const result = await sql`UPDATE products SET price = price * 1.1 WHERE price < 25`; expect(result.count).toBe(2); expect(result.command).toBe("UPDATE"); }); test("DELETE with affected rows", async () => { await sql`CREATE TABLE tasks (id INTEGER PRIMARY KEY, done INTEGER)`; await sql`INSERT INTO tasks VALUES (1, 0), (2, 1), (3, 0), (4, 1)`; const result = await sql`DELETE FROM tasks WHERE done = 1`; expect(result.count).toBe(2); expect(result.command).toBe("DELETE"); }); test("SELECT with various clauses", async () => { await sql`CREATE TABLE scores (id INTEGER, player TEXT, score INTEGER, team TEXT)`; await sql`INSERT INTO scores VALUES (1, 'Alice', 100, 'Red'), (2, 'Bob', 85, 'Blue'), (3, 'Charlie', 95, 'Red'), (4, 'Diana', 110, 'Blue')`; const ordered = await sql`SELECT * FROM scores ORDER BY score DESC`; expect(ordered[0].player).toBe("Diana"); const filtered = await sql`SELECT * FROM scores WHERE score > ${90}`; expect(filtered).toHaveLength(3); const grouped = await sql` SELECT team, COUNT(*) as count, AVG(score) as avg_score FROM scores GROUP BY team `; expect(grouped).toHaveLength(2); const limited = await sql`SELECT * FROM scores ORDER BY score DESC LIMIT 2 OFFSET 1`; expect(limited).toHaveLength(2); expect(limited[0].player).toBe("Alice"); }); test("handles multiple statements with unsafe", async () => { await sql.unsafe(` CREATE TABLE multi1 (id INTEGER); CREATE TABLE multi2 (id INTEGER); INSERT INTO multi1 VALUES (1); INSERT INTO multi2 VALUES (2); `); const result1 = await sql`SELECT * FROM multi1`; const result2 = await sql`SELECT * FROM multi2`; expect(result1).toHaveLength(1); expect(result1[0].id).toBe(1); expect(result2).toHaveLength(1); expect(result2[0].id).toBe(2); }); }); describe("Parameterized Queries", () => { let sql: SQL; beforeAll(async () => { sql = new SQL("sqlite://:memory:"); await sql`CREATE TABLE params_test (id INTEGER, text_val TEXT, num_val REAL)`; }); afterAll(async () => { await sql?.close(); }); test("converts PostgreSQL $N style to SQLite ? style", async () => { await sql`INSERT INTO params_test VALUES (${1}, ${"test"}, ${3.14})`; const result = await sql`SELECT * FROM params_test WHERE id = ${1}`; expect(result[0].text_val).toBe("test"); expect(result[0].num_val).toBeCloseTo(3.14); }); test("handles many parameters", async () => { const values = Array.from({ length: 20 }, (_, i) => i); const columns = values.map(i => `col${i} INTEGER`).join(", "); const tableName = "many_params"; await sql.unsafe(`CREATE TABLE ${tableName} (${columns})`); const placeholders = values.map(() => "?").join(", "); await sql.unsafe(`INSERT INTO ${tableName} VALUES (${placeholders})`, values); const result = await sql.unsafe(`SELECT * FROM ${tableName}`); expect(Object.values(result[0])).toEqual(values); }); test("escapes special characters in parameters", async () => { const specialStrings = [ "'; DROP TABLE users; --", '" OR "1"="1', "\\'; DROP TABLE users; --", "\x00\x01\x02", "Robert'); DROP TABLE Students;--", ]; for (const str of specialStrings) { await sql`INSERT INTO params_test (id, text_val) VALUES (${100}, ${str})`; const result = await sql`SELECT text_val FROM params_test WHERE id = ${100}`; expect(result[0].text_val).toBe(str); await sql`DELETE FROM params_test WHERE id = ${100}`; } }); }); describe("Template Literal Security", () => { let sql: SQL; beforeAll(async () => { sql = new SQL("sqlite://:memory:"); }); afterAll(async () => { await sql?.close(); }); test("dynamic table names are not allowed in template literals", async () => { const tableName = "users"; expect(async () => await sql`CREATE TABLE ${tableName} (id INTEGER)`.execute()).toThrowErrorMatchingInlineSnapshot( `"near "?": syntax error"`, ); await sql.unsafe(`CREATE TABLE ${tableName} (id INTEGER)`); const tables = await sql`SELECT name FROM sqlite_master WHERE type='table' AND name='users'`; expect(tables).toHaveLength(1); }); test("dynamic column names are not allowed in template literals", async () => { await sql`CREATE TABLE test_security (id INTEGER, name TEXT)`; await sql`INSERT INTO test_security VALUES (1, 'test')`; const columnName = "name"; const result = await sql`SELECT ${columnName} FROM test_security`; expect(result[0]).toEqual({ "?": "name" }); const unsafeResult = await sql.unsafe(`SELECT ${columnName} FROM test_security`); expect(unsafeResult[0].name).toBe("test"); }); test("dynamic SQL structure is not allowed in template literals", async () => { const columns = "id INTEGER, name TEXT"; expect( async () => await sql`CREATE TABLE dynamic_structure (${columns})`.execute(), ).toThrowErrorMatchingInlineSnapshot(`"near "?": syntax error"`); await sql.unsafe(`CREATE TABLE dynamic_structure (${columns})`); const tables = await sql`SELECT name FROM sqlite_master WHERE type='table' AND name='dynamic_structure'`; expect(tables).toHaveLength(1); }); test("SQL injection is prevented with template literals", async () => { await sql`CREATE TABLE injection_test (id INTEGER, value TEXT)`; await sql`INSERT INTO injection_test VALUES (1, 'safe')`; const maliciousInput = "'; DROP TABLE injection_test; --"; await sql`INSERT INTO injection_test VALUES (2, ${maliciousInput})`; const result = await sql`SELECT * FROM injection_test WHERE id = 2`; expect(result[0].value).toBe("'; DROP TABLE injection_test; --"); const tables = await sql`SELECT name FROM sqlite_master WHERE type='table' AND name='injection_test'`; expect(tables).toHaveLength(1); }); test("parameters work correctly for VALUES but not for identifiers", async () => { await sql`CREATE TABLE identifier_test (id INTEGER, name TEXT)`; const id = 1; const name = "Alice"; await sql`INSERT INTO identifier_test VALUES (${id}, ${name})`; const result = await sql`SELECT * FROM identifier_test WHERE id = ${id}`; expect(result[0].name).toBe("Alice"); const table = "identifier_test"; expect(async () => await sql`SELECT * FROM ${table}`.execute()).toThrowErrorMatchingInlineSnapshot( `"near "?": syntax error"`, ); }); test("sql([...]) helper not allowed when 'where in' appears only in string literal", async () => { const sql = new SQL("sqlite://:memory:"); expect( async () => await sql`SELECT 'this has where in inside a string' ${sql([1, 2])}`.execute(), ).toThrowErrorMatchingInlineSnapshot(`"Helpers are only allowed for INSERT, UPDATE and WHERE IN commands"`); await sql.close(); }); }); describe("Transactions", () => { let sql: SQL; beforeEach(async () => { sql = new SQL("sqlite://:memory:"); await sql`CREATE TABLE accounts (id INTEGER PRIMARY KEY, balance REAL)`; await sql`INSERT INTO accounts VALUES (1, 1000), (2, 500)`; }); afterEach(async () => { await sql?.close(); }); test("successful transaction commits", async () => { const result = await sql.begin(async tx => { await tx`UPDATE accounts SET balance = balance - 100 WHERE id = 1`; await tx`UPDATE accounts SET balance = balance + 100 WHERE id = 2`; return "success"; }); expect(result).toBe("success"); const accounts = await sql`SELECT * FROM accounts ORDER BY id`; expect(accounts[0].balance).toBe(900); expect(accounts[1].balance).toBe(600); }); test("failed transaction rolls back", async () => { try { await sql.begin(async tx => { await tx`UPDATE accounts SET balance = balance - 2000 WHERE id = 1`; await tx`UPDATE accounts SET balance = balance + 2000 WHERE id = 2`; throw new Error("Insufficient funds"); }); } catch (err) { expect(err).toBeInstanceOf(Error); expect((err as Error).message).toBe("Insufficient funds"); } const accounts = await sql`SELECT * FROM accounts ORDER BY id`; expect(accounts[0].balance).toBe(1000); expect(accounts[1].balance).toBe(500); }); test("nested transactions (savepoints)", async () => { await sql.begin(async tx => { await tx`UPDATE accounts SET balance = balance - 100 WHERE id = 1`; try { await tx.savepoint(async sp => { await sp`UPDATE accounts SET balance = balance - 200 WHERE id = 1`; throw new Error("Inner! transaction failed"); }); } catch (err) {} await tx`UPDATE accounts SET balance = balance + 100 WHERE id = 2`; }); const accounts = await sql`SELECT * FROM accounts ORDER BY id`; expect(accounts[0].balance).toBe(900); expect(accounts[1].balance).toBe(600); }); // SQLite doesn't support read-only transactions via BEGIN syntax // It only supports DEFERRED (default), IMMEDIATE, and EXCLUSIVE test("read-only transactions throw appropriate error", async () => { expect( async () => await sql.begin("readonly", async tx => { return await tx`SELECT * FROM accounts`; }), ).toThrowErrorMatchingInlineSnapshot( `"SQLite doesn't support 'readonly' transaction mode. Use DEFERRED, IMMEDIATE, or EXCLUSIVE."`, ); }); test("deferred vs immediate transactions", async () => { await sql.begin("deferred", async tx => { await tx`SELECT * FROM accounts`; await tx`UPDATE accounts SET balance = balance + 1`; }); await sql.begin("immediate", async tx => { await tx`UPDATE accounts SET balance = balance + 1`; }); const accounts = await sql`SELECT * FROM accounts WHERE id = 1`; expect(accounts[0].balance).toBe(1002); }); }); describe("SQLite-specific features", () => { let sql: SQL; beforeAll(async () => { sql = new SQL("sqlite://:memory:"); }); afterAll(async () => { await sql?.close(); }); test("PRAGMA statements", async () => { const version = await sql`PRAGMA compile_options`; expect(version.length).toBeGreaterThan(0); const journalMode = await sql`PRAGMA journal_mode`; expect(journalMode[0].journal_mode).toBeDefined(); await sql`PRAGMA synchronous = NORMAL`; const syncMode = await sql`PRAGMA synchronous`; expect(syncMode[0].synchronous).toBe(1); }); test("AUTOINCREMENT behavior", async () => { await sql`CREATE TABLE auto_test ( id INTEGER PRIMARY KEY AUTOINCREMENT, value TEXT )`; await sql`INSERT INTO auto_test (value) VALUES ('first')`; await sql`INSERT INTO auto_test (value) VALUES ('second')`; await sql`DELETE FROM auto_test WHERE id = 2`; await sql`INSERT INTO auto_test (value) VALUES ('third')`; const results = await sql`SELECT * FROM auto_test ORDER BY id`; expect(results[0].id).toBe(1); expect(results[1].id).toBe(3); }); test("last_insert_rowid()", async () => { await sql`CREATE TABLE rowid_test (id INTEGER PRIMARY KEY, value TEXT)`; await sql`INSERT INTO rowid_test (value) VALUES ('test')`; const result = await sql`SELECT last_insert_rowid() as id`; expect(result[0].id).toBe(1); }); test("changes() function", async () => { await sql`CREATE TABLE changes_test (id INTEGER, value TEXT)`; await sql`INSERT INTO changes_test VALUES (1, 'a'), (2, 'b'), (3, 'c')`; await sql`UPDATE changes_test SET value = 'updated' WHERE id > 1`; const changes = await sql`SELECT changes() as count`; expect(changes[0].count).toBe(2); }); test("ATTACH DATABASE", async () => { const dir = tempDirWithFiles("sqlite-attach-test", {}); const attachPath = path.join(dir, "attached.db"); await sql`ATTACH DATABASE ${attachPath} AS attached`; await sql`CREATE TABLE attached.other_table (id INTEGER)`; await sql`INSERT INTO attached.other_table VALUES (1)`; const result = await sql`SELECT * FROM attached.other_table`; expect(result).toHaveLength(1); await sql`DETACH DATABASE attached`; await rm(dir, { recursive: true }); }); test("Common Table Expressions (CTEs)", async () => { await sql`CREATE TABLE employees (id INTEGER, name TEXT, manager_id INTEGER)`; await sql`INSERT INTO employees VALUES (1, 'CEO', NULL), (2, 'VP1', 1), (3, 'VP2', 1), (4, 'Manager1', 2), (5, 'Manager2', 3)`; const result = await sql` WITH RECURSIVE org_chart AS ( SELECT id, name, manager_id, 0 as level FROM employees WHERE manager_id IS NULL UNION ALL SELECT e.id, e.name, e.manager_id, oc.level + 1 FROM employees e JOIN org_chart oc ON e.manager_id = oc.id ) SELECT * FROM org_chart ORDER BY level, id `; expect(result).toHaveLength(5); expect(result[0].level).toBe(0); expect(result[result.length - 1].level).toBe(2); }); test("Full-text search (FTS5)", async () => { await sql`CREATE VIRTUAL TABLE docs USING fts5(title, content)`; await sql`INSERT INTO docs VALUES ('First Document', 'This is the content of the first document'), ('Second Document', 'This document contains different content'), ('Third Document', 'Another document with unique text')`; const results = await sql`SELECT * FROM docs WHERE docs MATCH 'content'`; expect(results).toHaveLength(2); await sql`DROP TABLE docs`; }); test("JSON functions", async () => { await sql`CREATE TABLE json_test (id INTEGER, data TEXT)`; const jsonData = { name: "Test", values: [1, 2, 3] }; await sql`INSERT INTO json_test VALUES (1, ${JSON.stringify(jsonData)})`; const name = await sql`SELECT json_extract(data, '$.name') as name FROM json_test`; expect(name[0].name).toBe("Test"); const arrayLength = await sql`SELECT json_array_length(data, '$.values') as len FROM json_test`; expect(arrayLength[0].len).toBe(3); }); }); describe("SQL helpers", () => { let sql: SQL; beforeAll(async () => { sql = new SQL("sqlite://:memory:"); }); afterAll(async () => { await sql.close(); }); test("bulk insert with sql() helper", async () => { await sql`CREATE TABLE bulk_test (id INTEGER, name TEXT, value REAL)`; const data = [ { id: 1, name: "Item1", value: 10.5 }, { id: 2, name: "Item2", value: 20.5 }, { id: 3, name: "Item3", value: 30.5 }, ]; await sql`INSERT INTO bulk_test ${sql(data)}`; const results = await sql`SELECT * FROM bulk_test ORDER BY id`; expect(results).toHaveLength(3); expect(results[0].name).toBe("Item1"); }); test("unsafe with parameters", async () => { await sql`CREATE TABLE unsafe_test (id INTEGER, value TEXT)`; const query = "INSERT INTO unsafe_test VALUES (?, ?)"; await sql.unsafe(query, [1, "test"]); const selectQuery = "SELECT * FROM unsafe_test WHERE id = ?"; const results = await sql.unsafe(selectQuery, [1]); expect(results[0].value).toBe("test"); }); test("file execution", async () => { const dir = tempDirWithFiles("sql-files", { "schema.sql": ` CREATE TABLE file_test ( id INTEGER PRIMARY KEY, created_at TEXT DEFAULT CURRENT_TIMESTAMP ); INSERT INTO file_test (id) VALUES (1), (2), (3); `, "query.sql": `SELECT COUNT(*) as count FROM file_test`, }); await sql.file(path.join(dir, "schema.sql")); const result = await sql.file(path.join(dir, "query.sql")); expect(result[0].count).toBe(3); }); test("file with parameters", async () => { const dir = tempDirWithFiles("sql-params", { "query.sql": `SELECT ? as param1, ? as param2`, }); const result = await sql.file(path.join(dir, "query.sql"), ["value1", "value2"]); expect(result[0].param1).toBe("value1"); expect(result[0].param2).toBe("value2"); }); }); describe("Helper argument validation", () => { let sql: SQL; beforeAll(async () => { sql = new SQL("sqlite://:memory:"); await sql`CREATE TABLE helper_invalid ( id INTEGER PRIMARY KEY, text_val TEXT, blob_val BLOB, num_val REAL )`; }); afterAll(async () => { await sql?.close(); }); test("functions are invalid values in helper", async () => { const fn = () => 123; expect( async () => await sql`INSERT INTO helper_invalid ${sql({ id: 1, text_val: fn })}`.execute(), ).toThrowErrorMatchingInlineSnapshot(`"Binding expected string, TypedArray, boolean, number, bigint or null"`); }); test("plain objects (JSON) are invalid values in helper", async () => { const obj = { a: 1, b: "two" }; expect( async () => await sql`INSERT INTO helper_invalid ${sql({ id: 2, text_val: obj as any })}`.execute(), ).toThrowErrorMatchingInlineSnapshot(`"Binding expected string, TypedArray, boolean, number, bigint or null"`); }); test("Map and Set are invalid values in helper", async () => { expect( async () => await sql`INSERT INTO helper_invalid ${sql({ id: 3, text_val: new Map([["k", "v"]]) as any })}`.execute(), ).toThrowErrorMatchingInlineSnapshot(`"Binding expected string, TypedArray, boolean, number, bigint or null"`); expect( async () => await sql`INSERT INTO helper_invalid ${sql({ id: 4, text_val: new Set([1, 2, 3]) as any })}`.execute(), ).toThrowErrorMatchingInlineSnapshot(`"Binding expected string, TypedArray, boolean, number, bigint or null"`); }); test("Response, Request, Blob, File are invalid values in helper", async () => { expect( async () => await sql`INSERT INTO helper_invalid ${sql({ id: 5, text_val: new Response("ok") as any })}`.execute(), ).toThrowErrorMatchingInlineSnapshot(`"Binding expected string, TypedArray, boolean, number, bigint or null"`); expect( async () => await sql`INSERT INTO helper_invalid ${sql({ id: 6, text_val: new Request("https://example.com") as any })}`.execute(), ).toThrowErrorMatchingInlineSnapshot(`"Binding expected string, TypedArray, boolean, number, bigint or null"`); expect( async () => await sql`INSERT INTO helper_invalid ${sql({ id: 7, blob_val: new Blob(["hello"]) as any })}`.execute(), ).toThrowErrorMatchingInlineSnapshot(`"Binding expected string, TypedArray, boolean, number, bigint or null"`); expect( async () => await sql`INSERT INTO helper_invalid ${sql({ id: 8, blob_val: new File(["body"], "a.txt") as any })}`.execute(), ).toThrowErrorMatchingInlineSnapshot(`"Binding expected string, TypedArray, boolean, number, bigint or null"`); }); test("ArrayBuffer (not a view) is invalid in helper", async () => { const ab = new ArrayBuffer(8); expect( async () => await sql`INSERT INTO helper_invalid ${sql({ id: 9, blob_val: ab as any })}`.execute(), ).toThrowErrorMatchingInlineSnapshot(`"Binding expected string, TypedArray, boolean, number, bigint or null"`); }); test("Promise, Date, RegExp are invalid in helper", async () => { expect( async () => await sql`INSERT INTO helper_invalid ${sql({ id: 10, text_val: Promise.resolve("x") as any })}`.execute(), ).toThrowErrorMatchingInlineSnapshot(`"Binding expected string, TypedArray, boolean, number, bigint or null"`); expect( async () => await sql`INSERT INTO helper_invalid ${sql({ id: 11, text_val: new Date() as any })}`.execute(), ).toThrowErrorMatchingInlineSnapshot(`"Binding expected string, TypedArray, boolean, number, bigint or null"`); expect( async () => await sql`INSERT INTO helper_invalid ${sql({ id: 12, text_val: /abc/ as any })}`.execute(), ).toThrowErrorMatchingInlineSnapshot(`"Binding expected string, TypedArray, boolean, number, bigint or null"`); }); test("BigInt values are accepted when in range", async () => { const id = 42n; await sql`INSERT INTO helper_invalid ${sql({ id, text_val: "ok" })}`; const rows = await sql`SELECT id, text_val FROM helper_invalid WHERE id = ${Number(id)}`; expect(rows[0].text_val).toBe("ok"); }); test("BigInt out of range rejects when safeIntegers is enabled", async () => { const sqlSafe = new SQL({ adapter: "sqlite", filename: ":memory:", safeIntegers: true }); await sqlSafe`CREATE TABLE t (id INTEGER PRIMARY KEY, n INTEGER)`; const big = BigInt("9223372036854775808"); // 2^63, just out of int64 range expect( async () => await sqlSafe`INSERT INTO t ${sql({ id: 1, n: big })}`.execute(), ).toThrowErrorMatchingInlineSnapshot(`"BigInt value '9223372036854775808' is out of range"`); await sqlSafe.close(); }); test("invalid keys for helper throw immediately", () => { const obj = { id: 1, text_val: "x" }; expect(() => sql`INSERT INTO helper_invalid ${sql(obj, Symbol("k") as any)}`).toThrowErrorMatchingInlineSnapshot( `"Keys must be strings or numbers: Symbol(k)"`, ); expect(() => sql`UPDATE helper_invalid SET ${sql(obj, 1n as any)} WHERE id = 1`).toThrowErrorMatchingInlineSnapshot( `"Keys must be strings or numbers: 1"`, ); expect( () => sql`INSERT INTO helper_invalid ${sql(obj, function bad() {} as any)}`, ).toThrowErrorMatchingInlineSnapshot(`"Keys must be strings or numbers: function bad() {}"`); }); test("WHERE IN helper accepts both arrays and single values", async () => { const result = await sql`SELECT 1 as num WHERE 1 IN ${sql([1, 2, 3])}`.execute(); expect(result).toBeArray(); expect(result.length).toBe(1); expect(result[0]).toEqual({ num: 1 }); const singleObj = { id: 1, name: "test" }; const result2 = await sql`SELECT 1 as num WHERE 1 IN ${sql(singleObj, "id")}`.execute(); expect(result2).toBeArray(); expect(result2.length).toBe(1); expect(result2[0]).toEqual({ num: 1 }); const singleObj2 = { id: 2, name: "test2" }; const result3 = await sql`SELECT 1 as num WHERE 1 IN ${sql(singleObj2, "id")}`.execute(); expect(result3).toBeArray(); expect(result3.length).toBe(0); }); test("WHERE IN helper rejects multiple columns", async () => { const items = [{ a: 1, b: 2 }]; expect( async () => await sql`SELECT 1 WHERE 1 IN ${sql(items, "a", "b")}`.execute(), ).toThrowErrorMatchingInlineSnapshot(`"Cannot use WHERE IN helper with multiple columns"`); }); test("UPDATE helper rejects array of objects", async () => { const items = [{ text_val: "a" }, { text_val: "b" }]; expect( async () => await sql`UPDATE helper_invalid SET ${sql(items)} WHERE id = 1`.execute(), ).toThrowErrorMatchingInlineSnapshot(`"Cannot use array of objects for UPDATE"`); }); test("invalid values in WHERE IN helper are rejected", async () => { expect(async () => await sql`SELECT 1 WHERE 1 IN ${sql([() => {}])}`.execute()).toThrowErrorMatchingInlineSnapshot( `"Binding expected string, TypedArray, boolean, number, bigint or null"`, ); }); }); describe("Error handling", () => { let sql: SQL; beforeAll(async () => { sql = new SQL("sqlite://:memory:"); }); afterAll(async () => { await sql.close(); }); test("syntax errors", async () => { try { await sql`SELCT * FROM nonexistent`; expect(true).toBe(false); } catch (err) { expect(err).toBeInstanceOf(Error); expect((err as Error).message).toContain("syntax error"); } }); test("constraint violations", async () => { await sql`CREATE TABLE constraints ( id INTEGER PRIMARY KEY, value TEXT NOT NULL, unique_val TEXT UNIQUE )`; try { await sql`INSERT INTO constraints (id, value) VALUES (1, ${null})`; expect(true).toBe(false); } catch (err) { expect(err).toBeInstanceOf(Error); expect((err as Error).message).toContain("NOT NULL"); } await sql`INSERT INTO constraints VALUES (1, 'test', 'unique')`; try { await sql`INSERT INTO constraints VALUES (2, 'test2', 'unique')`; expect(true).toBe(false); } catch (err) { expect(err).toBeInstanceOf(Error); expect((err as Error).message).toContain("UNIQUE"); } }); test("foreign key violations", async () => { await sql`PRAGMA foreign_keys = ON`; await sql`CREATE TABLE parent (id INTEGER PRIMARY KEY)`; await sql`CREATE TABLE child ( id INTEGER PRIMARY KEY, parent_id INTEGER, FOREIGN KEY (parent_id) REFERENCES parent(id) )`; await sql`INSERT INTO parent VALUES (1)`; await sql`INSERT INTO child VALUES (1, 1)`; try { await sql`INSERT INTO child VALUES (2, 999)`; expect(true).toBe(false); } catch (err) { expect(err).toBeInstanceOf(Error); expect((err as Error).message).toContain("FOREIGN KEY"); } }); }); describe("Connection management", () => { test("close() prevents further queries", async () => { const sql = new SQL("sqlite://:memory:"); await sql`CREATE TABLE test (id INTEGER)`; await sql.close(); try { await sql`SELECT * FROM test`; expect(true).toBe(false); } catch (err) { expect(err).toBeInstanceOf(Error); expect((err as Error).message).toMatchInlineSnapshot(`"Connection closed"`); } }); test("reserve throws for SQLite", async () => { const sql = new SQL("sqlite://:memory:"); expect(async () => await sql.reserve()).toThrowErrorMatchingInlineSnapshot( `"This adapter doesn't support connection reservation"`, ); await sql.close(); }); test("distributed transactions throw for SQLite", async () => { const sql = new SQL("sqlite://:memory:"); expect(() => sql.beginDistributed("test-tx", async () => {})).toThrowErrorMatchingInlineSnapshot( `"This adapter doesn't support distributed transactions."`, ); expect(() => sql.commitDistributed("test-tx")).toThrowErrorMatchingInlineSnapshot( `"SQLite doesn't support distributed transactions."`, ); expect(() => sql.rollbackDistributed("test-tx")).toThrowErrorMatchingInlineSnapshot( `"SQLite doesn't support distributed transactions."`, ); await sql.close(); }); test("flush throws for SQLite", async () => { const sql = new SQL("sqlite://:memory:"); expect(() => sql.flush()).toThrowErrorMatchingInlineSnapshot( `"SQLite doesn't support flush() - queries are executed synchronously"`, ); await sql.close(); }); }); describe("Performance & Edge Cases", () => { test("handles large datasets", async () => { const sql = new SQL("sqlite://:memory:"); await sql`CREATE TABLE large (id INTEGER PRIMARY KEY, data TEXT)`; const rowCount = 1000; const data = Buffer.alloc(100, "x").toString(); await sql.begin(async tx => { for (let i = 0; i < rowCount; i++) { await tx`INSERT INTO large VALUES (${i}, ${data})`; } }); const count = await sql`SELECT COUNT(*) as count FROM large`; expect(count[0].count).toBe(rowCount); await sql.close(); }); test("handles many columns", async () => { const sql = new SQL(":memory:"); const columnCount = 100; const columns = Array.from({ length: columnCount }, (_, i) => `col${i} INTEGER`).join(", "); await sql.unsafe(`CREATE TABLE wide (${columns})`); const values = Array.from({ length: columnCount }, (_, i) => i); const placeholders = values.map(() => "?").join(", "); await sql.unsafe(`INSERT INTO wide VALUES (${placeholders})`, values); const result = await sql`SELECT * FROM wide`; expect(Object.keys(result[0])).toHaveLength(columnCount); await sql.close(); }); test("handles concurrent queries", async () => { const sql = new SQL("sqlite://:memory:"); await sql`CREATE TABLE concurrent (id INTEGER PRIMARY KEY, value INTEGER)`; const promises = Array.from({ length: 10 }, (_, i) => sql`INSERT INTO concurrent VALUES (${i}, ${i * 10})`); await Promise.all(promises); const count = await sql`SELECT COUNT(*) as count FROM concurrent`; expect(count[0].count).toBe(10); await sql.close(); }); test("handles empty results", async () => { const sql = new SQL("sqlite://:memory:"); await sql`CREATE TABLE empty (id INTEGER)`; const results = await sql`SELECT * FROM empty`; expect(results).toHaveLength(0); expect(results.command).toBe("SELECT"); expect(results.count).toBe(0); await sql.close(); }); test("handles special table names", async () => { const sql = new SQL("sqlite://:memory:"); const specialNames = ["table-with-dash", "table.with.dots", "table with spaces", "123numeric", "SELECT"]; for (const name of specialNames) { await sql.unsafe(`CREATE TABLE "${name}" (id INTEGER)`); await sql.unsafe(`INSERT INTO "${name}" VALUES (1)`); const result = await sql.unsafe(`SELECT * FROM "${name}"`); expect(result).toHaveLength(1); await sql.unsafe(`DROP TABLE "${name}"`); } await sql.close(); }); }); describe("WAL mode and concurrency", () => { test("can enable WAL mode", async () => { const dir = tempDirWithFiles("sqlite-wal-test", {}); const dbPath = path.join(dir, "wal-test.db"); const sql = new SQL(`sqlite://${dbPath}`); await sql`PRAGMA journal_mode = WAL`; const mode = await sql`PRAGMA journal_mode`; expect(mode[0].journal_mode).toBe("wal"); await sql`CREATE TABLE wal_test (id INTEGER)`; await sql`INSERT INTO wal_test VALUES (1)`; const walPath = `${dbPath}-wal`; const shmPath = `${dbPath}-shm`; const walStats = await stat(walPath); expect(walStats.isFile()).toBe(true); expect(walStats.size).toBeGreaterThan(0); const shmStats = await stat(shmPath); expect(shmStats.isFile()).toBe(true); expect(shmStats.size).toBeGreaterThan(0); await sql.close(); await rm(dir, { recursive: true }); }); }); describe("Memory and resource management", () => { test("properly releases resources on close", async () => { const sql = new SQL("sqlite://:memory:"); await sql`CREATE TABLE resource_test (id INTEGER, data TEXT)`; for (let i = 0; i < 100; i++) { await sql`INSERT INTO resource_test VALUES (${i}, ${"x".repeat(1000)})`; } await sql.close(); try { await sql`SELECT * FROM resource_test`; expect(true).toBe(false); } catch (err) { expect(err).toBeInstanceOf(Error); expect((err as Error).message).toMatchInlineSnapshot(`"Connection closed"`); } }); test("properly finalizes prepared statements", async () => { const sql = new SQL("sqlite://:memory:"); await sql`CREATE TABLE stmt_test (id INTEGER PRIMARY KEY, value TEXT)`; const iterations = 10000; for (let i = 0; i < iterations; i++) { await sql`INSERT INTO stmt_test (id, value) VALUES (${i}, ${"test" + i})`; if (i % 100 === 0) { const result = await sql`SELECT COUNT(*) as count FROM stmt_test`; expect(result[0].count).toBe(i + 1); } } await sql` DELETE FROM stmt_test WHERE id < 100; DELETE FROM stmt_test WHERE id < 200; DELETE FROM stmt_test WHERE id < 300; `; const finalCount = await sql`SELECT COUNT(*) as count FROM stmt_test`; expect(finalCount[0].count).toBe(iterations - 300); await sql.close(); }); test("handles many concurrent prepared statements", async () => { const sql = new SQL("sqlite://:memory:"); await sql`CREATE TABLE concurrent_test (id INTEGER, value TEXT)`; const promises: Promise[] = []; for (let i = 0; i < 1000; i++) { promises.push(sql`INSERT INTO concurrent_test VALUES (${i}, ${"value" + i})`); } await Promise.all(promises); const result = await sql`SELECT COUNT(*) as count FROM concurrent_test`; expect(result[0].count).toBe(1000); const selectPromises: Promise[] = []; for (let i = 0; i < 100; i++) { selectPromises.push(sql`SELECT * FROM concurrent_test WHERE id = ${i}`); } const results = await Promise.all(selectPromises); results.forEach((result, i) => { expect(result).toHaveLength(1); expect(result[0].id).toBe(i); }); await sql.close(); }); }); describe("Connection URL Edge Cases", () => { test("handles various file:// URL formats", async () => { const dir = tempDirWithFiles("sqlite-url-test", {}); const dbPath1 = path.join(dir, "test1.db"); const sql1 = new SQL(`file://${dbPath1}`); await sql1`CREATE TABLE test (id INTEGER)`; await sql1`INSERT INTO test VALUES (1)`; const result1 = await sql1`SELECT * FROM test`; expect(result1).toHaveLength(1); await sql1.close(); const dbPath2 = path.join(dir, "test2.db"); const sql2 = new SQL(`file:${dbPath2}`); await sql2`CREATE TABLE test (id INTEGER)`; await sql2.close(); await rm(dir, { recursive: true }); }); test("handles special characters in database paths", async () => { const specialNames = [ "test with spaces.db", "test-with-dash.db", "test.with.dots.db", "test_underscore.db", "test@symbol.db", "test#hash.db", "test%percent.db", "test&ersand.db", "test(parens).db", "test[brackets].db", "test{braces}.db", "test'quote.db", ]; for (const name of specialNames) { const dir = tempDirWithFiles(`sqlite-special-${Math.random()}`, {}); const dbPath = path.join(dir, name); const sql = new SQL(`sqlite://${dbPath}`); await sql`CREATE TABLE test (id INTEGER)`; await sql`INSERT INTO test VALUES (1)`; const result = await sql`SELECT * FROM test`; expect(result).toHaveLength(1); expect(sql.options.filename).toBe(join(dir, name)); await sql.close(); await rm(dir, { recursive: true }); } }); test("handles relative vs absolute paths", async () => { const dir = tempDirWithFiles("sqlite-path-test", {}); const originalCwd = process.cwd(); try { process.chdir(dir); const sql1 = new SQL("sqlite://./relative.db"); await sql1`CREATE TABLE test (id INTEGER)`; await sql1.close(); expect(existsSync(path.join(dir, "relative.db"))).toBe(true); const absPath = path.join(dir, "absolute.db"); const sql2 = new SQL(`sqlite://${absPath}`); await sql2`CREATE TABLE test (id INTEGER)`; await sql2.close(); expect(existsSync(absPath)).toBe(true); } finally { process.chdir(originalCwd); await rm(dir, { recursive: true }); } }); test("handles readonly mode via URL parameters", async () => { const dir = tempDirWithFiles("sqlite-readonly-test", {}); const dbPath = path.join(dir, "readonly.db"); const sql1 = new SQL(`sqlite://${dbPath}`); await sql1`CREATE TABLE test (id INTEGER)`; await sql1`INSERT INTO test VALUES (1)`; await sql1.close(); const sql2 = new SQL(`sqlite://${dbPath}?mode=ro`); const result = await sql2`SELECT * FROM test`; expect(result).toHaveLength(1); try { await sql2`INSERT INTO test VALUES (2)`; expect(true).toBe(false); } catch (err) { expect(err).toBeInstanceOf(Error); expect((err as Error).message).toContain("readonly"); } await sql2.close(); await rm(dir, { recursive: true }); }); test("handles URI parameters for cache and other settings", async () => { const dir = tempDirWithFiles("sqlite-uri-test", {}); const dbPath = path.join(dir, "uri.db"); const sql = new SQL(`sqlite://${dbPath}?cache=shared&mode=rwc`); await sql`CREATE TABLE test (id INTEGER)`; await sql`INSERT INTO test VALUES (1)`; const pragmas = await sql`PRAGMA cache_size`; expect(pragmas).toBeDefined(); await sql.close(); await rm(dir, { recursive: true }); }); }); describe("BLOB Edge Cases and Binary Data", () => { let sql: SQL; beforeAll(async () => { sql = new SQL("sqlite://:memory:"); }); afterAll(async () => { await sql?.close(); }); test("handles zero-length BLOBs", async () => { await sql`CREATE TABLE blob_test (id INTEGER, data BLOB)`; const emptyBuffer = Buffer.alloc(0); await sql`INSERT INTO blob_test VALUES (1, ${emptyBuffer})`; const result = await sql`SELECT * FROM blob_test`; expect(Buffer.from(result[0].data)).toHaveLength(0); }); test("handles large BLOBs", async () => { await sql`CREATE TABLE large_blob (id INTEGER, data BLOB)`; const sizes = [1024 * 1024, 10 * 1024 * 1024]; for (const size of sizes) { const largeBuffer = Buffer.alloc(size); for (let i = 0; i < size; i++) { largeBuffer[i] = i % 256; } await sql`INSERT INTO large_blob VALUES (${size}, ${largeBuffer})`; const result = await sql`SELECT * FROM large_blob WHERE id = ${size}`; const retrieved = Buffer.from(result[0].data); expect(retrieved.length).toBe(size); for (let i = 0; i < Math.min(100, size); i++) { expect(retrieved[i]).toBe(i % 256); } } }); test("handles binary data with all byte values", async () => { await sql`CREATE TABLE binary_test (id INTEGER, data BLOB)`; const allBytes = Buffer.alloc(256); for (let i = 0; i < 256; i++) { allBytes[i] = i; } await sql`INSERT INTO binary_test VALUES (1, ${allBytes})`; const result = await sql`SELECT * FROM binary_test`; const retrieved = Buffer.from(result[0].data); expect(retrieved.length).toBe(256); for (let i = 0; i < 256; i++) { expect(retrieved[i]).toBe(i); } }); test("handles Uint8Array and ArrayBuffer", async () => { await sql`CREATE TABLE array_test (id INTEGER, data BLOB)`; const uint8 = new Uint8Array([1, 2, 3, 4, 5]); await sql`INSERT INTO array_test VALUES (1, ${uint8})`; const arrayBuffer = new ArrayBuffer(8); const view = new DataView(arrayBuffer); view.setInt32(0, 0x12345678); view.setInt32(4, 0x9abcdef0); await sql`INSERT INTO array_test VALUES (2, ${Buffer.from(arrayBuffer)})`; const results = await sql`SELECT * FROM array_test ORDER BY id`; expect(Buffer.from(results[0].data)).toEqual(Buffer.from([1, 2, 3, 4, 5])); expect(Buffer.from(results[1].data).length).toBe(8); }); }); describe("Special Characters and Escape Sequences", () => { let sql: SQL; beforeAll(async () => { sql = new SQL("sqlite://:memory:"); await sql`CREATE TABLE special_chars (id INTEGER, text_val TEXT)`; }); afterAll(async () => { await sql?.close(); }); test("handles various quote types", async () => { const quotes = [ `Single ' quote`, `Double " quote`, `Both ' and " quotes`, `Backtick \` quote`, `'Multiple' "quote" 'types'`, `It's a "test"`, `\\'escaped\\' quotes`, `"""triple quotes"""`, `'''triple single'''`, ]; for (let i = 0; i < quotes.length; i++) { await sql`INSERT INTO special_chars VALUES (${i}, ${quotes[i]})`; const result = await sql`SELECT text_val FROM special_chars WHERE id = ${i}`; expect(result[0].text_val).toBe(quotes[i]); } }); test("handles control characters and escape sequences", async () => { const controls = ["\n\r\t", "\x00\x01\x02", "\b\f\v", "\\n\\r\\t", "\u0000\u001F", "\x1B[31mANSI\x1B[0m"]; await sql`CREATE TABLE control_chars (id INTEGER, val TEXT)`; for (let i = 0; i < controls.length; i++) { await sql`INSERT INTO control_chars VALUES (${i}, ${controls[i]})`; const result = await sql`SELECT val FROM control_chars WHERE id = ${i}`; expect(result[0].val).toBe(controls[i]); } }); test("handles Unicode and emoji", async () => { const unicode = [ "Hello 世界", "مرحبا بالعالم", "שלום עולם", "Здравствуй мир", "🚀🎉🌟", "👨‍👩‍👧‍👦", "𝓗𝓮𝓵𝓵𝓸", "A\u0301", "🏴󠁧󠁢󠁥󠁮󠁧󠁿", ]; await sql`CREATE TABLE unicode_test (id INTEGER, val TEXT)`; for (let i = 0; i < unicode.length; i++) { await sql`INSERT INTO unicode_test VALUES (${i}, ${unicode[i]})`; const result = await sql`SELECT val FROM unicode_test WHERE id = ${i}`; expect(result[0].val).toBe(unicode[i]); } }); test("handles very long strings", async () => { await sql`CREATE TABLE long_strings (id INTEGER, val TEXT)`; const lengths = [1000, 10000, 100000, 1000000]; for (const len of lengths) { const longString = Buffer.alloc(len, "a").toString(); await sql`INSERT INTO long_strings VALUES (${len}, ${longString})`; const result = await sql`SELECT LENGTH(val) as len FROM long_strings WHERE id = ${len}`; expect(result[0].len).toBe(len); } }); }); describe("Triggers and Views", () => { let sql: SQL; beforeEach(async () => { sql = new SQL("sqlite://:memory:"); }); afterEach(async () => { await sql?.close(); }); test("CREATE and use TRIGGER", async () => { await sql`CREATE TABLE audit_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, table_name TEXT, operation TEXT, timestamp TEXT DEFAULT CURRENT_TIMESTAMP )`; await sql`CREATE TABLE users ( id INTEGER PRIMARY KEY, name TEXT, updated_at TEXT )`; await sql`CREATE TRIGGER user_update_trigger AFTER UPDATE ON users BEGIN INSERT INTO audit_log (table_name, operation) VALUES ('users', 'UPDATE'); UPDATE users SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id; END`; await sql`INSERT INTO users (id, name) VALUES (1, 'Alice')`; await sql`UPDATE users SET name = 'Alice Updated' WHERE id = 1`; const logs = await sql`SELECT * FROM audit_log`; expect(logs).toHaveLength(1); expect(logs[0].operation).toBe("UPDATE"); const user = await sql`SELECT * FROM users WHERE id = 1`; expect(user[0].updated_at).toBeDefined(); }); test("CREATE and query VIEW", async () => { await sql`CREATE TABLE orders ( id INTEGER PRIMARY KEY, customer_id INTEGER, amount REAL, status TEXT )`; await sql`INSERT INTO orders VALUES (1, 1, 100.0, 'completed'), (2, 1, 50.0, 'pending'), (3, 2, 200.0, 'completed'), (4, 2, 75.0, 'cancelled')`; await sql`CREATE VIEW customer_summary AS SELECT customer_id, COUNT(*) as total_orders, SUM(CASE WHEN status = 'completed' THEN amount ELSE 0 END) as total_spent, AVG(amount) as avg_order_value FROM orders GROUP BY customer_id`; const summary = await sql`SELECT * FROM customer_summary ORDER BY customer_id`; expect(summary).toHaveLength(2); expect(summary[0].total_orders).toBe(2); expect(summary[0].total_spent).toBe(100.0); expect(summary[1].total_orders).toBe(2); expect(summary[1].total_spent).toBe(200.0); }); test("triggers with WHEN conditions", async () => { await sql`CREATE TABLE inventory ( id INTEGER PRIMARY KEY, product TEXT, quantity INTEGER, reorder_level INTEGER DEFAULT 10 )`; await sql`CREATE TABLE reorder_alerts ( id INTEGER PRIMARY KEY AUTOINCREMENT, product TEXT, quantity INTEGER, created_at TEXT DEFAULT CURRENT_TIMESTAMP )`; await sql`CREATE TRIGGER low_stock_trigger AFTER UPDATE OF quantity ON inventory WHEN NEW.quantity < NEW.reorder_level BEGIN INSERT INTO reorder_alerts (product, quantity) VALUES (NEW.product, NEW.quantity); END`; await sql`INSERT INTO inventory VALUES (1, 'Widget', 100, 10)`; await sql`UPDATE inventory SET quantity = 5 WHERE id = 1`; const alerts = await sql`SELECT * FROM reorder_alerts`; expect(alerts).toHaveLength(1); expect(alerts[0].product).toBe("Widget"); expect(alerts[0].quantity).toBe(5); await sql`UPDATE inventory SET quantity = 15 WHERE id = 1`; const alerts2 = await sql`SELECT * FROM reorder_alerts`; expect(alerts2).toHaveLength(1); }); }); describe("Indexes and Query Optimization", () => { let sql: SQL; beforeEach(async () => { sql = new SQL("sqlite://:memory:"); }); afterEach(async () => { await sql?.close(); }); test("CREATE various types of indexes", async () => { await sql`CREATE TABLE products ( id INTEGER PRIMARY KEY, name TEXT, category TEXT, price REAL, sku TEXT UNIQUE, description TEXT )`; await sql`CREATE INDEX idx_category ON products(category)`; await sql`CREATE INDEX idx_category_price ON products(category, price DESC)`; await sql`CREATE UNIQUE INDEX idx_sku ON products(sku)`; await sql`CREATE INDEX idx_expensive ON products(price) WHERE price > 100`; await sql`CREATE INDEX idx_name_lower ON products(LOWER(name))`; for (let i = 1; i <= 100; i++) { await sql`INSERT INTO products VALUES ( ${i}, ${"Product " + i}, ${"Category " + (i % 10)}, ${i * 10.5}, ${"SKU-" + i.toString().padStart(5, "0")}, ${"Description for product " + i} )`; } try { await sql`INSERT INTO products VALUES (101, 'Test', 'Test', 10, 'SKU-00001', 'Duplicate SKU')`; expect(true).toBe(false); } catch (err) { expect(err).toBeInstanceOf(Error); expect((err as Error).message).toContain("UNIQUE"); } const results = await sql`SELECT * FROM products WHERE category = 'Category 5'`; expect(results.length).toBeGreaterThan(0); const expensive = await sql`SELECT * FROM products WHERE price > 500`; expect(expensive.length).toBeGreaterThan(0); }); test("ANALYZE and query planning", async () => { await sql`CREATE TABLE stats_test ( id INTEGER PRIMARY KEY, type TEXT, value INTEGER )`; await sql`CREATE INDEX idx_type ON stats_test(type)`; for (let i = 1; i <= 1000; i++) { const type = i <= 900 ? "common" : i <= 990 ? "uncommon" : "rare"; await sql`INSERT INTO stats_test VALUES (${i}, ${type}, ${i})`; } await sql`ANALYZE`; const stats = await sql`SELECT * FROM sqlite_stat1`; expect(stats.length).toBeGreaterThan(0); }); test("covering indexes", async () => { await sql`CREATE TABLE users ( id INTEGER PRIMARY KEY, email TEXT, username TEXT, created_at TEXT )`; await sql`CREATE INDEX idx_email_username ON users(email, username)`; for (let i = 1; i <= 100; i++) { await sql`INSERT INTO users VALUES ( ${i}, ${"user" + i + "@example.com"}, ${"user" + i}, ${new Date().toISOString()} )`; } const result = await sql`SELECT email, username FROM users WHERE email LIKE 'user1%'`; expect(result.length).toBeGreaterThan(0); }); }); describe("VACUUM and Database Maintenance", () => { test("VACUUM command", async () => { const dir = tempDirWithFiles("sqlite-vacuum-test", {}); const dbPath = path.join(dir, "vacuum.db"); const sql = new SQL(`sqlite://${dbPath}`); await sql`CREATE TABLE vacuum_test (id INTEGER, data TEXT)`; for (let i = 0; i < 1000; i++) { await sql`INSERT INTO vacuum_test VALUES (${i}, ${Buffer.alloc(100, "x").toString()})`; } await sql`DELETE FROM vacuum_test WHERE id % 2 = 0`; const statsBefore = await stat(dbPath); const sizeBefore = statsBefore.size; await sql`VACUUM`; const statsAfter = await stat(dbPath); const sizeAfter = statsAfter.size; expect(sizeAfter).toBeLessThanOrEqual(sizeBefore); await sql.close(); await rm(dir, { recursive: true }); }); test("incremental VACUUM with auto_vacuum", async () => { const dir = tempDirWithFiles("sqlite-auto-vacuum-test", {}); const dbPath = path.join(dir, "auto_vacuum.db"); const sql = new SQL(`sqlite://${dbPath}`); await sql`PRAGMA auto_vacuum = 2`; await sql`CREATE TABLE test (id INTEGER, data TEXT)`; for (let i = 0; i < 100; i++) { await sql`INSERT INTO test VALUES (${i}, ${Buffer.alloc(1000, "x").toString()})`; } await sql`DELETE FROM test WHERE id < 50`; await sql`PRAGMA incremental_vacuum(10)`; const pageCount = await sql`PRAGMA page_count`; expect(pageCount[0].page_count).toBeGreaterThan(0); await sql.close(); await rm(dir, { recursive: true }); }); }); describe("Backup and Restore Operations", () => { test("backup to another file", async () => { const dir = tempDirWithFiles("sqlite-backup-test", {}); const sourcePath = path.join(dir, "source.db"); const backupPath = path.join(dir, "backup.db"); const source = new SQL(`sqlite://${sourcePath}`); await source`CREATE TABLE backup_test (id INTEGER PRIMARY KEY, data TEXT)`; for (let i = 1; i <= 10; i++) { await source`INSERT INTO backup_test VALUES (${i}, ${"Data " + i})`; } await source.unsafe(`VACUUM INTO '${backupPath}'`); await source.close(); const backup = new SQL(`sqlite://${backupPath}`); const data = await backup`SELECT * FROM backup_test`; expect(data).toHaveLength(10); expect(data[0].data).toBe("Data 1"); await backup.close(); await rm(dir, { recursive: true }); }); }); describe("Custom Collations and Functions", () => { let sql: SQL; beforeEach(async () => { sql = new SQL("sqlite://:memory:"); }); afterEach(async () => { await sql?.close(); }); test("case-insensitive collation with NOCASE", async () => { await sql`CREATE TABLE collation_test ( id INTEGER PRIMARY KEY, name TEXT COLLATE NOCASE )`; await sql`INSERT INTO collation_test VALUES (1, 'Alice'), (2, 'alice'), (3, 'ALICE'), (4, 'Bob')`; const result = await sql`SELECT * FROM collation_test WHERE name = 'alice'`; expect(result).toHaveLength(3); expect(result.map(r => r.name).sort()).toEqual(["ALICE", "Alice", "alice"]); }); test("binary collation", async () => { await sql`CREATE TABLE binary_collation ( id INTEGER PRIMARY KEY, data TEXT COLLATE BINARY )`; await sql`INSERT INTO binary_collation VALUES (1, 'A'), (2, 'a'), (3, 'B'), (4, 'b')`; const result = await sql`SELECT * FROM binary_collation ORDER BY data`; expect(result.map(r => r.data)).toEqual(["A", "B", "a", "b"]); }); }); describe("Window Functions", () => { let sql: SQL; beforeAll(async () => { sql = new SQL("sqlite://:memory:"); await sql`CREATE TABLE sales ( id INTEGER PRIMARY KEY, employee TEXT, department TEXT, amount REAL, sale_date TEXT )`; const sales = [ ["Alice", "Sales", 1000, "2024-01-01"], ["Alice", "Sales", 1500, "2024-01-02"], ["Bob", "Sales", 800, "2024-01-01"], ["Bob", "Sales", 1200, "2024-01-02"], ["Charlie", "Marketing", 900, "2024-01-01"], ["Charlie", "Marketing", 1100, "2024-01-02"], ]; for (const [employee, department, amount, date] of sales) { await sql`INSERT INTO sales (employee, department, amount, sale_date) VALUES (${employee}, ${department}, ${amount}, ${date})`; } }); afterAll(async () => { await sql?.close(); }); test("ROW_NUMBER window function", async () => { const result = await sql` SELECT employee, amount, ROW_NUMBER() OVER (ORDER BY amount DESC) as rank FROM sales ORDER BY rank `; expect(result[0].rank).toBe(1); expect(result[0].amount).toBe(1500); expect(result[result.length - 1].rank).toBe(6); }); test("partition by with window functions", async () => { const result = await sql` SELECT employee, department, amount, SUM(amount) OVER (PARTITION BY department) as dept_total, AVG(amount) OVER (PARTITION BY employee) as employee_avg FROM sales ORDER BY department, employee `; const marketingRows = result.filter(r => r.department === "Marketing"); expect(marketingRows[0].dept_total).toBe(2000); const salesRows = result.filter(r => r.department === "Sales"); expect(salesRows[0].dept_total).toBe(4500); }); test("running totals with window functions", async () => { const result = await sql` SELECT employee, sale_date, amount, SUM(amount) OVER ( PARTITION BY employee ORDER BY sale_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW ) as running_total FROM sales WHERE employee = 'Alice' ORDER BY sale_date `; expect(result[0].running_total).toBe(1000); expect(result[1].running_total).toBe(2500); }); }); describe("Check Constraints and Complex Validations", () => { let sql: SQL; beforeEach(async () => { sql = new SQL("sqlite://:memory:"); }); afterEach(async () => { await sql?.close(); }); test("CHECK constraints on columns", async () => { await sql`CREATE TABLE validated ( id INTEGER PRIMARY KEY, age INTEGER CHECK (age >= 0 AND age <= 150), email TEXT CHECK (email LIKE '%@%.%'), status TEXT CHECK (status IN ('active', 'inactive', 'pending')), percentage REAL CHECK (percentage >= 0 AND percentage <= 100) )`; await sql`INSERT INTO validated VALUES (1, 25, 'test@example.com', 'active', 50.5)`; try { await sql`INSERT INTO validated VALUES (2, -1, 'test@example.com', 'active', 50)`; expect(true).toBe(false); } catch (err) { expect((err as Error).message).toContain("CHECK"); } try { await sql`INSERT INTO validated VALUES (3, 25, 'notanemail', 'active', 50)`; expect(true).toBe(false); } catch (err) { expect((err as Error).message).toContain("CHECK"); } try { await sql`INSERT INTO validated VALUES (4, 25, 'test@example.com', 'invalid', 50)`; expect(true).toBe(false); } catch (err) { expect((err as Error).message).toContain("CHECK"); } try { await sql`INSERT INTO validated VALUES (5, 25, 'test@example.com', 'active', 101)`; expect(true).toBe(false); } catch (err) { expect((err as Error).message).toContain("CHECK"); } }); test("table-level CHECK constraints", async () => { await sql`CREATE TABLE orders ( id INTEGER PRIMARY KEY, start_date TEXT, end_date TEXT, quantity INTEGER, price REAL, CHECK (end_date >= start_date), CHECK (quantity * price >= 0) )`; await sql`INSERT INTO orders VALUES (1, '2024-01-01', '2024-01-31', 10, 9.99)`; try { await sql`INSERT INTO orders VALUES (2, '2024-02-01', '2024-01-01', 10, 9.99)`; expect(true).toBe(false); } catch (err) { expect((err as Error).message).toContain("CHECK"); } }); }); describe("Generated Columns", () => { let sql: SQL; beforeEach(async () => { sql = new SQL("sqlite://:memory:"); }); afterEach(async () => { await sql?.close(); }); test("GENERATED ALWAYS AS virtual columns", async () => { await sql`CREATE TABLE products ( id INTEGER PRIMARY KEY, price REAL, tax_rate REAL, total_price REAL GENERATED ALWAYS AS (price * (1 + tax_rate)) VIRTUAL, price_category TEXT GENERATED ALWAYS AS ( CASE WHEN price < 10 THEN 'cheap' WHEN price < 100 THEN 'moderate' ELSE 'expensive' END ) VIRTUAL )`; await sql`INSERT INTO products (id, price, tax_rate) VALUES (1, 5.00, 0.1), (2, 50.00, 0.2), (3, 500.00, 0.15)`; const results = await sql`SELECT * FROM products ORDER BY id`; expect(results[0].total_price).toBeCloseTo(5.5, 2); expect(results[0].price_category).toBe("cheap"); expect(results[1].total_price).toBeCloseTo(60.0, 2); expect(results[1].price_category).toBe("moderate"); expect(results[2].total_price).toBeCloseTo(575.0, 2); expect(results[2].price_category).toBe("expensive"); }); test("GENERATED ALWAYS AS stored columns", async () => { await sql`CREATE TABLE rectangles ( id INTEGER PRIMARY KEY, width REAL, height REAL, area REAL GENERATED ALWAYS AS (width * height) STORED, perimeter REAL GENERATED ALWAYS AS (2 * (width + height)) STORED )`; await sql`INSERT INTO rectangles (id, width, height) VALUES (1, 10, 20), (2, 5.5, 3.2)`; const results = await sql`SELECT * FROM rectangles ORDER BY id`; expect(results[0].area).toBe(200); expect(results[0].perimeter).toBe(60); expect(results[1].area).toBeCloseTo(17.6, 2); expect(results[1].perimeter).toBeCloseTo(17.4, 2); await sql`UPDATE rectangles SET width = 15 WHERE id = 1`; const updated = await sql`SELECT * FROM rectangles WHERE id = 1`; expect(updated[0].area).toBe(300); expect(updated[0].perimeter).toBe(70); }); }); describe("Partial Indexes", () => { let sql: SQL; beforeEach(async () => { sql = new SQL("sqlite://:memory:"); }); afterEach(async () => { await sql?.close(); }); test("partial index with WHERE clause", async () => { await sql`CREATE TABLE tasks ( id INTEGER PRIMARY KEY, title TEXT, status TEXT, priority INTEGER, due_date TEXT )`; await sql`CREATE INDEX idx_urgent_tasks ON tasks(due_date, priority) WHERE status != 'completed' AND priority > 3`; const tasks = [ ["Task 1", "pending", 5, "2024-01-01"], ["Task 2", "completed", 5, "2024-01-01"], ["Task 3", "pending", 2, "2024-01-01"], ["Task 4", "pending", 4, "2024-01-02"], ]; for (let i = 0; i < tasks.length; i++) { const [title, status, priority, due_date] = tasks[i]; await sql`INSERT INTO tasks VALUES (${i + 1}, ${title}, ${status}, ${priority}, ${due_date})`; } const urgent = await sql` SELECT * FROM tasks WHERE status != 'completed' AND priority > 3 ORDER BY due_date, priority `; expect(urgent).toHaveLength(2); expect(urgent[0].title).toBe("Task 1"); expect(urgent[1].title).toBe("Task 4"); }); }); describe("UPSERT Operations", () => { let sql: SQL; beforeEach(async () => { sql = new SQL("sqlite://:memory:"); }); afterEach(async () => { await sql?.close(); }); test("INSERT OR REPLACE", async () => { await sql`CREATE TABLE users ( id INTEGER PRIMARY KEY, email TEXT UNIQUE, name TEXT, login_count INTEGER DEFAULT 0 )`; await sql`INSERT INTO users VALUES (1, 'alice@example.com', 'Alice', 1)`; await sql`INSERT OR REPLACE INTO users VALUES (1, 'alice@example.com', 'Alice Updated', 5)`; const result = await sql`SELECT * FROM users WHERE id = 1`; expect(result[0].name).toBe("Alice Updated"); expect(result[0].login_count).toBe(5); }); test("INSERT ON CONFLICT DO UPDATE", async () => { await sql`CREATE TABLE inventory ( product_id INTEGER PRIMARY KEY, name TEXT, quantity INTEGER, last_updated TEXT )`; await sql`INSERT INTO inventory VALUES (1, 'Widget', 100, '2024-01-01')`; await sql` INSERT INTO inventory VALUES (1, 'Widget', 50, '2024-01-02') ON CONFLICT(product_id) DO UPDATE SET quantity = quantity + excluded.quantity, last_updated = excluded.last_updated `; const result = await sql`SELECT * FROM inventory WHERE product_id = 1`; expect(result[0].quantity).toBe(150); expect(result[0].last_updated).toBe("2024-01-02"); await sql` INSERT INTO inventory VALUES (2, 'Gadget', 75, '2024-01-02') ON CONFLICT(product_id) DO UPDATE SET quantity = quantity + excluded.quantity `; const all = await sql`SELECT * FROM inventory ORDER BY product_id`; expect(all).toHaveLength(2); }); test("INSERT ON CONFLICT DO NOTHING", async () => { await sql`CREATE TABLE settings ( key TEXT PRIMARY KEY, value TEXT )`; await sql`INSERT INTO settings VALUES ('theme', 'dark')`; const result = await sql` INSERT INTO settings VALUES ('theme', 'light') ON CONFLICT(key) DO NOTHING RETURNING * `; expect(result).toHaveLength(0); const setting = await sql`SELECT * FROM settings WHERE key = 'theme'`; expect(setting[0].value).toBe("dark"); }); }); describe("WITHOUT ROWID Tables", () => { let sql: SQL; beforeEach(async () => { sql = new SQL("sqlite://:memory:"); }); afterEach(async () => { await sql?.close(); }); test("WITHOUT ROWID table with composite primary key", async () => { await sql`CREATE TABLE sessions ( user_id INTEGER, device_id TEXT, token TEXT, created_at TEXT, PRIMARY KEY (user_id, device_id) ) WITHOUT ROWID`; await sql`INSERT INTO sessions VALUES (1, 'phone', 'token1', '2024-01-01'), (1, 'laptop', 'token2', '2024-01-01'), (2, 'phone', 'token3', '2024-01-01')`; const results = await sql`SELECT * FROM sessions WHERE user_id = 1`; expect(results).toHaveLength(2); try { await sql`INSERT INTO sessions VALUES (1, 'phone', 'token4', '2024-01-02')`; expect(true).toBe(false); } catch (err) { expect((err as Error).message).toContain("UNIQUE"); } }); }); describe("Concurrency and Locking", () => { test("concurrent reads work correctly", async () => { const dir = tempDirWithFiles("sqlite-concurrent-test", {}); const dbPath = path.join(dir, "concurrent.db"); const sql1 = new SQL(`sqlite://${dbPath}`); await sql1`CREATE TABLE test (id INTEGER PRIMARY KEY, value TEXT)`; for (let i = 1; i <= 100; i++) { await sql1`INSERT INTO test VALUES (${i}, ${"value" + i})`; } const sql2 = new SQL(`sqlite://${dbPath}`); const sql3 = new SQL(`sqlite://${dbPath}`); const [result1, result2, result3] = await Promise.all([ sql1`SELECT COUNT(*) as count FROM test`, sql2`SELECT COUNT(*) as count FROM test`, sql3`SELECT COUNT(*) as count FROM test`, ]); expect(result1[0].count).toBe(100); expect(result2[0].count).toBe(100); expect(result3[0].count).toBe(100); await sql1.close(); await sql2.close(); await sql3.close(); await rm(dir, { recursive: true }); }); test("write lock prevents concurrent writes", async () => { const dir = tempDirWithFiles("sqlite-write-lock-test", {}); const dbPath = path.join(dir, "writelock.db"); const sql = new SQL(`sqlite://${dbPath}`); await sql`CREATE TABLE counter (id INTEGER PRIMARY KEY, value INTEGER)`; await sql`INSERT INTO counter VALUES (1, 0)`; const updatePromise = sql.begin(async tx => { await tx`UPDATE counter SET value = value + 1 WHERE id = 1`; await new Promise(resolve => setTimeout(resolve, 50)); await tx`UPDATE counter SET value = value + 1 WHERE id = 1`; return "done"; }); const sql2 = new SQL(`sqlite://${dbPath}`); const startTime = Date.now(); await updatePromise; const duration = Date.now() - startTime; expect(duration).toBeGreaterThanOrEqual(40); const final = await sql`SELECT value FROM counter WHERE id = 1`; expect(final[0].value).toBe(2); await sql.close(); await sql2.close(); await rm(dir, { recursive: true }); }); test("busy timeout handling", async () => { const dir = tempDirWithFiles("sqlite-busy-test", {}); const dbPath = path.join(dir, "busy.db"); const sql1 = new SQL(`sqlite://${dbPath}`); await sql1`CREATE TABLE test (id INTEGER PRIMARY KEY, value TEXT)`; await sql1`PRAGMA busy_timeout = 100`; const sql2 = new SQL(`sqlite://${dbPath}`); await sql2`PRAGMA busy_timeout = 100`; const longTransaction = sql1.begin(async tx => { await tx`INSERT INTO test VALUES (1, 'test')`; await new Promise(resolve => setTimeout(resolve, 200)); return "done"; }); try { await sql2`INSERT INTO test VALUES (2, 'test2')`; expect(true).toBe(false); } catch (err) { expect(err).toBeInstanceOf(Error); } await longTransaction; await sql1.close(); await sql2.close(); await rm(dir, { recursive: true }); }); }); describe("Date and Time Functions", () => { let sql: SQL; beforeAll(async () => { sql = new SQL("sqlite://:memory:"); }); afterAll(async () => { await sql?.close(); }); test("date and time functions", async () => { await sql`CREATE TABLE timestamps ( id INTEGER PRIMARY KEY, created_at TEXT DEFAULT CURRENT_TIMESTAMP, date_only TEXT DEFAULT (DATE('now')), time_only TEXT DEFAULT (TIME('now')) )`; await sql`INSERT INTO timestamps (id) VALUES (1)`; const result = await sql`SELECT * FROM timestamps`; expect(result[0].created_at).toBeDefined(); expect(result[0].date_only).toMatch(/^\d{4}-\d{2}-\d{2}$/); expect(result[0].time_only).toMatch(/^\d{2}:\d{2}:\d{2}$/); }); test("date arithmetic", async () => { const results = await sql` SELECT DATE('2024-01-15', '+1 month') as next_month, DATE('2024-01-15', '-7 days') as last_week, DATE('2024-01-15', '+1 year') as next_year, julianday('2024-01-15') - julianday('2024-01-01') as days_diff `; expect(results[0].next_month).toBe("2024-02-15"); expect(results[0].last_week).toBe("2024-01-08"); expect(results[0].next_year).toBe("2025-01-15"); expect(results[0].days_diff).toBe(14); }); test("strftime formatting", async () => { const results = await sql` SELECT strftime('%Y-%m-%d', '2024-01-15 14:30:45') as date_only, strftime('%H:%M:%S', '2024-01-15 14:30:45') as time_only, strftime('%w', '2024-01-15') as day_of_week, strftime('%j', '2024-01-15') as day_of_year, strftime('%s', '2024-01-15 00:00:00') as unix_timestamp `; expect(results[0].date_only).toBe("2024-01-15"); expect(results[0].time_only).toBe("14:30:45"); expect(results[0].day_of_week).toBe("1"); expect(results[0].day_of_year).toBe("015"); expect(parseInt(results[0].unix_timestamp)).toBeGreaterThan(0); }); }); describe("Aggregate Functions and Grouping", () => { let sql: SQL; beforeAll(async () => { sql = new SQL("sqlite://:memory:"); await sql`CREATE TABLE sales_data ( id INTEGER PRIMARY KEY, region TEXT, product TEXT, quantity INTEGER, price REAL, sale_date TEXT )`; const salesData = [ ["North", "Widget", 10, 25.5, "2024-01-01"], ["North", "Widget", 15, 25.5, "2024-01-02"], ["North", "Gadget", 5, 75.0, "2024-01-01"], ["South", "Widget", 20, 25.5, "2024-01-01"], ["South", "Gadget", 8, 75.0, "2024-01-02"], ["East", "Widget", 12, 25.5, "2024-01-01"], ["East", "Gadget", 3, 75.0, "2024-01-01"], ["West", "Widget", 18, 25.5, "2024-01-02"], ]; for (let i = 0; i < salesData.length; i++) { const [region, product, quantity, price, date] = salesData[i]; await sql`INSERT INTO sales_data VALUES (${i + 1}, ${region}, ${product}, ${quantity}, ${price}, ${date})`; } }); afterAll(async () => { await sql?.close(); }); test("basic aggregate functions", async () => { const result = await sql` SELECT COUNT(*) as total_records, SUM(quantity) as total_quantity, AVG(price) as avg_price, MIN(quantity) as min_quantity, MAX(quantity) as max_quantity, GROUP_CONCAT(DISTINCT region) as all_regions FROM sales_data `; expect(result[0].total_records).toBe(8); expect(result[0].total_quantity).toBe(91); expect(result[0].avg_price).toBeCloseTo(44.0625, 2); // (5*25.5 + 3*75.0) / 8 expect(result[0].min_quantity).toBe(3); expect(result[0].max_quantity).toBe(20); expect(result[0].all_regions.split(",")).toHaveLength(4); }); test("GROUP BY with HAVING", async () => { const result = await sql` SELECT region, SUM(quantity * price) as total_sales, COUNT(*) as transaction_count FROM sales_data GROUP BY region HAVING SUM(quantity * price) > 500 ORDER BY total_sales DESC `; expect(result.length).toBeGreaterThan(0); result.forEach(row => { expect(row.total_sales).toBeGreaterThan(500); }); }); test("UNION ALL for subtotals (ROLLUP equivalent)", async () => { const result = await sql` SELECT region, product, SUM(quantity) as total_quantity FROM sales_data GROUP BY region, product UNION ALL SELECT region, NULL as product, SUM(quantity) as total_quantity FROM sales_data GROUP BY region UNION ALL SELECT NULL as region, NULL as product, SUM(quantity) as total_quantity FROM sales_data ORDER BY region NULLS LAST, product NULLS LAST `; const grandTotal = result.find(r => r.region === null && r.product === null); expect(grandTotal).toBeDefined(); expect(grandTotal.total_quantity).toBe(91); }); }); describe("STRICT Tables", () => { let sql: SQL; beforeEach(async () => { sql = new SQL("sqlite://:memory:"); }); afterEach(async () => { await sql?.close(); }); test("STRICT table type enforcement", async () => { await sql`CREATE TABLE strict_test ( id INTEGER PRIMARY KEY, int_col INTEGER, real_col REAL, text_col TEXT, blob_col BLOB, any_col ANY ) STRICT`; await sql`INSERT INTO strict_test VALUES (1, 42, 3.14, 'text', X'0102', 'anything')`; try { await sql`INSERT INTO strict_test VALUES (2, 'not an int', 3.14, 'text', X'0102', 'anything')`; expect(true).toBe(false); } catch (err) { expect(err).toBeInstanceOf(Error); } try { await sql`INSERT INTO strict_test VALUES (3, 42, 'not a real', 'text', X'0102', 'anything')`; expect(true).toBe(false); } catch (err) { expect(err).toBeInstanceOf(Error); } await sql`INSERT INTO strict_test VALUES (4, 42, 3.14, 'text', X'0102', 123)`; await sql`INSERT INTO strict_test VALUES (5, 42, 3.14, 'text', X'0102', X'ABCD')`; const results = await sql`SELECT * FROM strict_test ORDER BY id`; expect(results).toHaveLength(3); }); }); describe("Virtual Tables (besides FTS)", () => { let sql: SQL; beforeEach(async () => { sql = new SQL("sqlite://:memory:"); }); afterEach(async () => { await sql?.close(); }); test("json_each virtual table", async () => { const jsonArray = JSON.stringify([1, 2, 3, 4, 5]); const result = await sql` SELECT value FROM json_each(${jsonArray}) `; expect(result).toHaveLength(5); expect(result.map(r => r.value)).toEqual([1, 2, 3, 4, 5]); }); test("json_tree virtual table", async () => { const jsonObj = JSON.stringify({ name: "root", children: [ { name: "child1", value: 1 }, { name: "child2", value: 2 }, ], }); const result = await sql` SELECT key, value, type, path FROM json_tree(${jsonObj}) WHERE type != 'object' AND type != 'array' `; expect(result.length).toBeGreaterThan(0); const nameRow = result.find(r => r.key === "name" && r.value === "root"); expect(nameRow).toBeDefined(); }); }); describe("Recursive Queries and Complex CTEs", () => { let sql: SQL; beforeEach(async () => { sql = new SQL("sqlite://:memory:"); }); afterEach(async () => { await sql?.close(); }); test("factorial using recursive CTE", async () => { const result = await sql` WITH RECURSIVE factorial(n, fact) AS ( SELECT 1, 1 UNION ALL SELECT n + 1, fact * (n + 1) FROM factorial WHERE n < 10 ) SELECT n, fact FROM factorial `; expect(result).toHaveLength(10); expect(result[0].fact).toBe(1); expect(result[9].fact).toBe(3628800); }); test("Fibonacci sequence", async () => { const result = await sql` WITH RECURSIVE fib(n, a, b) AS ( SELECT 1, 0, 1 UNION ALL SELECT n + 1, b, a + b FROM fib WHERE n < 10 ) SELECT n, a as fibonacci FROM fib `; expect(result).toHaveLength(10); expect(result[0].fibonacci).toBe(0); expect(result[9].fibonacci).toBe(34); }); test("tree traversal with path", async () => { await sql`CREATE TABLE tree ( id INTEGER PRIMARY KEY, parent_id INTEGER, name TEXT )`; await sql`INSERT INTO tree VALUES (1, NULL, 'root'), (2, 1, 'branch1'), (3, 1, 'branch2'), (4, 2, 'leaf1'), (5, 2, 'leaf2'), (6, 3, 'leaf3')`; const result = await sql` WITH RECURSIVE tree_path AS ( SELECT id, parent_id, name, name as path, 0 as depth FROM tree WHERE parent_id IS NULL UNION ALL SELECT t.id, t.parent_id, t.name, tp.path || '/' || t.name as path, tp.depth + 1 as depth FROM tree t JOIN tree_path tp ON t.parent_id = tp.id ) SELECT * FROM tree_path ORDER BY path `; expect(result).toHaveLength(6); expect(result[0].path).toBe("root"); expect(result[result.length - 1].depth).toBe(2); }); }); describe("Mathematical and String Functions", () => { let sql: SQL; beforeAll(async () => { sql = new SQL("sqlite://:memory:"); }); afterAll(async () => { await sql?.close(); }); test("mathematical functions", async () => { const result = await sql` SELECT ABS(-42) as abs_val, ROUND(3.14159, 2) as rounded, MIN(1, 2, 3) as min_val, MAX(1, 2, 3) as max_val `; expect(result[0].abs_val).toBe(42); expect(result[0].rounded).toBe(3.14); expect(result[0].min_val).toBe(1); expect(result[0].max_val).toBe(3); }); test("string functions", async () => { const result = await sql` SELECT LENGTH('Hello') as str_length, UPPER('hello') as uppercase, LOWER('HELLO') as lowercase, TRIM(' hello ') as trimmed, LTRIM(' hello') as left_trimmed, RTRIM('hello ') as right_trimmed, SUBSTR('Hello World', 7, 5) as substring, REPLACE('Hello World', 'World', 'SQLite') as replaced, INSTR('Hello World', 'World') as position, PRINTF('%d-%02d-%02d', 2024, 1, 5) as formatted, HEX('ABC') as hex_val, CHAR(65, 66, 67) as char_val `; expect(result[0].str_length).toBe(5); expect(result[0].uppercase).toBe("HELLO"); expect(result[0].lowercase).toBe("hello"); expect(result[0].trimmed).toBe("hello"); expect(result[0].left_trimmed).toBe("hello"); expect(result[0].right_trimmed).toBe("hello"); expect(result[0].substring).toBe("World"); expect(result[0].replaced).toBe("Hello SQLite"); expect(result[0].position).toBe(7); expect(result[0].formatted).toBe("2024-01-05"); expect(result[0].hex_val).toBe("414243"); expect(result[0].char_val).toBe("ABC"); }); test("pattern matching with GLOB", async () => { await sql`CREATE TABLE patterns (id INTEGER, text TEXT)`; await sql`INSERT INTO patterns VALUES (1, 'hello'), (2, 'Hello'), (3, 'HELLO'), (4, 'hELLo'), (5, 'world')`; const globResult = await sql`SELECT * FROM patterns WHERE text GLOB 'h*'`; expect(globResult).toHaveLength(2); const likeResult = await sql`SELECT * FROM patterns WHERE text LIKE 'h%'`; expect(likeResult).toHaveLength(4); }); }); describe("Edge Cases for NULL handling", () => { let sql: SQL; beforeAll(async () => { sql = new SQL("sqlite://:memory:"); }); afterAll(async () => { await sql?.close(); }); test("NULL in arithmetic operations", async () => { const result = await sql` SELECT NULL + 5 as null_add, NULL * 10 as null_multiply, NULL || 'text' as null_concat, COALESCE(NULL, NULL, 'default') as coalesced, IFNULL(NULL, 'replacement') as if_null, NULLIF(5, 5) as null_if_equal, NULLIF(5, 3) as null_if_not_equal `; expect(result[0].null_add).toBeNull(); expect(result[0].null_multiply).toBeNull(); expect(result[0].null_concat).toBeNull(); // In SQLite, NULL || 'text' returns NULL expect(result[0].coalesced).toBe("default"); expect(result[0].if_null).toBe("replacement"); expect(result[0].null_if_equal).toBeNull(); expect(result[0].null_if_not_equal).toBe(5); }); test("NULL in comparisons", async () => { await sql`CREATE TABLE null_test (id INTEGER, value INTEGER)`; await sql`INSERT INTO null_test VALUES (1, 10), (2, NULL), (3, 20)`; const eq = await sql`SELECT * FROM null_test WHERE value = NULL`; expect(eq).toHaveLength(0); const isNull = await sql`SELECT * FROM null_test WHERE value IS NULL`; expect(isNull).toHaveLength(1); const notNull = await sql`SELECT * FROM null_test WHERE value IS NOT NULL`; expect(notNull).toHaveLength(2); const asc = await sql`SELECT * FROM null_test ORDER BY value ASC`; expect(asc[0].value).toBeNull(); const desc = await sql`SELECT * FROM null_test ORDER BY value DESC`; expect(desc[2].value).toBeNull(); }); test("NULL in aggregates", async () => { await sql`CREATE TABLE agg_null (id INTEGER, value INTEGER)`; await sql`INSERT INTO agg_null VALUES (1, 10), (2, NULL), (3, 20), (4, NULL), (5, 30)`; const result = await sql` SELECT COUNT(*) as count_all, COUNT(value) as count_values, SUM(value) as sum_values, AVG(value) as avg_values, MAX(value) as max_value, MIN(value) as min_value FROM agg_null `; expect(result[0].count_all).toBe(5); expect(result[0].count_values).toBe(3); expect(result[0].sum_values).toBe(60); expect(result[0].avg_values).toBe(20); expect(result[0].max_value).toBe(30); expect(result[0].min_value).toBe(10); }); }); describe("System Tables and Introspection", () => { let sql: SQL; beforeAll(async () => { sql = new SQL("sqlite://:memory:"); await sql`CREATE TABLE test_table ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, created_at TEXT DEFAULT CURRENT_TIMESTAMP )`; await sql`CREATE INDEX idx_name ON test_table(name)`; await sql`CREATE VIEW test_view AS SELECT id, name FROM test_table`; }); afterAll(async () => { await sql?.close(); }); test("sqlite_master table", async () => { const objects = await sql` SELECT type, name, sql FROM sqlite_master WHERE type IN ('table', 'index', 'view') ORDER BY type, name `; expect(objects.length).toBeGreaterThan(0); const table = objects.find(o => o.type === "table" && o.name === "test_table"); expect(table).toBeDefined(); expect(table.sql).toContain("CREATE TABLE"); const index = objects.find(o => o.type === "index" && o.name === "idx_name"); expect(index).toBeDefined(); const view = objects.find(o => o.type === "view" && o.name === "test_view"); expect(view).toBeDefined(); }); test("pragma table_info", async () => { const columns = await sql`PRAGMA table_info(test_table)`; expect(columns).toHaveLength(3); const idCol = columns.find(c => c.name === "id"); expect(idCol.pk).toBe(1); expect(idCol.type).toBe("INTEGER"); const nameCol = columns.find(c => c.name === "name"); expect(nameCol.notnull).toBe(1); expect(nameCol.type).toBe("TEXT"); const createdCol = columns.find(c => c.name === "created_at"); expect(createdCol.dflt_value).toBe("CURRENT_TIMESTAMP"); }); test("pragma index_list and index_info", async () => { const indexes = await sql`PRAGMA index_list(test_table)`; expect(indexes.length).toBeGreaterThan(0); const idx = indexes.find(i => i.name === "idx_name"); expect(idx).toBeDefined(); const indexInfo = await sql`PRAGMA index_info(idx_name)`; expect(indexInfo).toHaveLength(1); expect(indexInfo[0].name).toBe("name"); }); }); describe("Error Recovery and Database Integrity", () => { test("handles corrupted data gracefully", async () => { const dir = tempDirWithFiles("sqlite-corrupt-test", {}); const dbPath = path.join(dir, "test.db"); const sql = new SQL(`sqlite://${dbPath}`); await sql`CREATE TABLE test (id INTEGER PRIMARY KEY, data TEXT)`; await sql`INSERT INTO test VALUES (1, 'test')`; const integrityCheck = await sql`PRAGMA integrity_check`; expect(integrityCheck[0].integrity_check).toBe("ok"); await sql.close(); await rm(dir, { recursive: true }); }); test("foreign key cascade actions", async () => { const sql = new SQL("sqlite://:memory:"); await sql`PRAGMA foreign_keys = ON`; await sql`CREATE TABLE authors ( id INTEGER PRIMARY KEY, name TEXT )`; await sql`CREATE TABLE books ( id INTEGER PRIMARY KEY, title TEXT, author_id INTEGER, FOREIGN KEY (author_id) REFERENCES authors(id) ON DELETE CASCADE )`; await sql`INSERT INTO authors VALUES (1, 'Author 1'), (2, 'Author 2')`; await sql`INSERT INTO books VALUES (1, 'Book 1', 1), (2, 'Book 2', 1), (3, 'Book 3', 2)`; await sql`DELETE FROM authors WHERE id = 1`; const remainingBooks = await sql`SELECT * FROM books`; expect(remainingBooks).toHaveLength(1); expect(remainingBooks[0].author_id).toBe(2); await sql.close(); }); test("deferred foreign key constraints", async () => { const sql = new SQL("sqlite://:memory:"); await sql`PRAGMA foreign_keys = ON`; await sql`CREATE TABLE parent (id INTEGER PRIMARY KEY)`; await sql`CREATE TABLE child ( id INTEGER PRIMARY KEY, parent_id INTEGER, FOREIGN KEY (parent_id) REFERENCES parent(id) DEFERRABLE INITIALLY DEFERRED )`; await sql.begin(async tx => { await tx`INSERT INTO child VALUES (1, 1)`; await tx`INSERT INTO parent VALUES (1)`; }); const result = await sql`SELECT * FROM child`; expect(result).toHaveLength(1); await sql.close(); }); }); describe("Temp Tables and Attached Databases", () => { test("temporary tables", async () => { const sql = new SQL("sqlite://:memory:"); await sql`CREATE TEMP TABLE temp_data (id INTEGER, value TEXT)`; await sql`INSERT INTO temp_data VALUES (1, 'temp')`; const result = await sql`SELECT * FROM temp_data`; expect(result).toHaveLength(1); const tempTables = await sql`SELECT name FROM sqlite_temp_master WHERE type = 'table'`; expect(tempTables.some(t => t.name === "temp_data")).toBe(true); const mainTables = await sql`SELECT name FROM sqlite_master WHERE name = 'temp_data'`; expect(mainTables).toHaveLength(0); await sql.close(); }); test("cross-database queries with ATTACH", async () => { const dir = tempDirWithFiles("sqlite-attach-cross-test", {}); const mainPath = path.join(dir, "main.db"); const attachPath = path.join(dir, "attached.db"); const mainSql = new SQL(`sqlite://${mainPath}`); await mainSql`CREATE TABLE main_table (id INTEGER, data TEXT)`; await mainSql`INSERT INTO main_table VALUES (1, 'main data')`; const attachSql = new SQL(`sqlite://${attachPath}`); await attachSql`CREATE TABLE attached_table (id INTEGER, data TEXT)`; await attachSql`INSERT INTO attached_table VALUES (2, 'attached data')`; await attachSql.close(); await mainSql`ATTACH DATABASE ${attachPath} AS attached_db`; const crossQuery = await mainSql` SELECT m.data as main_data, a.data as attached_data FROM main_table m, attached_db.attached_table a WHERE m.id = 1 AND a.id = 2 `; expect(crossQuery).toHaveLength(1); expect(crossQuery[0].main_data).toBe("main data"); expect(crossQuery[0].attached_data).toBe("attached data"); await mainSql`DETACH DATABASE attached_db`; await mainSql.close(); await rm(dir, { recursive: true }); }); }); describe("Query Explain and Optimization", () => { let sql: SQL; beforeAll(async () => { sql = new SQL("sqlite://:memory:"); await sql`CREATE TABLE large_table ( id INTEGER PRIMARY KEY, category TEXT, value INTEGER, description TEXT )`; for (let i = 1; i <= 1000; i++) { await sql`INSERT INTO large_table VALUES ( ${i}, ${"category" + (i % 10)}, ${i * 10}, ${"description for item " + i} )`; } }); afterAll(async () => { await sql?.close(); }); test("EXPLAIN QUERY PLAN", async () => { const planWithoutIndex = await sql` EXPLAIN QUERY PLAN SELECT * FROM large_table WHERE category = 'category5' `; expect(planWithoutIndex[0]).toMatchObject({ detail: "SCAN large_table", id: expect.any(Number), parent: 0, }); await sql`CREATE INDEX idx_category ON large_table(category)`; const planWithIndex = await sql` EXPLAIN QUERY PLAN SELECT * FROM large_table WHERE category = 'category5' `; expect(planWithIndex[0]).toMatchObject({ detail: "SEARCH large_table USING INDEX idx_category (category=?)", id: expect.any(Number), parent: 0, }); }); }); describe("Query Normalization Fuzzing Tests", () => { let sql: SQL; beforeAll(async () => { sql = new SQL("sqlite://:memory:"); await sql`CREATE TABLE test_table (id INTEGER, name TEXT, value REAL)`; await sql`CREATE TABLE "weird-table" (col1 TEXT, "col-2" INTEGER)`; await sql`CREATE TABLE [bracket table] ([col 1] TEXT, [col 2] INTEGER)`; await sql`CREATE TABLE \`backtick\` (\`col\` TEXT)`; }); afterAll(async () => { await sql?.close(); }); test("handles CTEs with various syntax styles", async () => { const cte1 = await sql.unsafe(` WITH cte AS (SELECT 1 as n) SELECT * FROM cte `); expect(cte1[0].n).toBe(1); const cte2 = await sql.unsafe(` WITH cte1 AS (SELECT 1 as n), cte2 AS (SELECT 2 as n), cte3 AS (SELECT n * 2 as doubled FROM cte1) SELECT * FROM cte3 `); expect(cte2[0].doubled).toBe(2); const cte3 = await sql.unsafe(` WITH RECURSIVE cnt(x) AS ( SELECT 1 UNION ALL SELECT x+1 FROM cnt WHERE x<5 ) SELECT * FROM cnt `); expect(cte3).toHaveLength(5); const cte4 = await sql.unsafe(` WITH /* comment */ cte AS ( SELECT 1 as n -- inline comment ) SELECT * FROM cte `); expect(cte4[0].n).toBe(1); }); test("handles window functions with complex syntax", async () => { await sql`INSERT INTO test_table VALUES (1, 'a', 10.5), (2, 'b', 20.5), (3, 'a', 30.5)`; const win1 = await sql.unsafe(` SELECT name, value, ROW_NUMBER() OVER (ORDER BY value) as rn FROM test_table `); expect(win1).toHaveLength(3); const win2 = await sql.unsafe(` SELECT name, value, ROW_NUMBER() OVER w1 as rn, RANK() OVER w1 as rank, DENSE_RANK() OVER w1 as dense_rank, LAG(value, 1, 0) OVER (ORDER BY id) as prev_value, LEAD(value) OVER (ORDER BY id) as next_value, FIRST_VALUE(value) OVER w2 as first_val, LAST_VALUE(value) OVER w2 as last_val, NTH_VALUE(value, 2) OVER w2 as second_val FROM test_table WINDOW w1 AS (PARTITION BY name ORDER BY value DESC), w2 AS (ORDER BY id ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) `); expect(win2).toHaveLength(3); const win3 = await sql.unsafe(` SELECT value, SUM(value) OVER ( ORDER BY id ROWS BETWEEN 2 PRECEDING AND CURRENT ROW ) as rolling_sum, AVG(value) OVER ( ORDER BY id RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW ) as cumulative_avg FROM test_table `); expect(win3).toHaveLength(3); }); test("handles UPSERT with various conflict resolution strategies", async () => { await sql`CREATE TABLE upsert_test (id INTEGER PRIMARY KEY, value TEXT UNIQUE, count INTEGER DEFAULT 0)`; await sql` INSERT OR REPLACE INTO upsert_test (id, value) VALUES (1, 'test') `; await sql` INSERT OR IGNORE INTO upsert_test (id, value) VALUES (1, 'ignored') `; await sql` INSERT INTO upsert_test (id, value, count) VALUES (1, 'test', 1) ON CONFLICT(id) DO UPDATE SET count = excluded.count + upsert_test.count, value = excluded.value || ' updated' `; await sql` INSERT INTO upsert_test (id, value, count) VALUES (2, 'test', 5) ON CONFLICT(value) DO UPDATE SET count = excluded.count WHERE excluded.count > upsert_test.count `; try { await sql`INSERT OR ABORT INTO upsert_test (id) VALUES (1)`; } catch {} try { await sql`INSERT OR FAIL INTO upsert_test (id) VALUES (1)`; } catch {} }); test("handles complex JOIN syntax variations", async () => { const join1 = await sql.unsafe(` SELECT * FROM test_table NATURAL JOIN test_table t2 `); const join2 = await sql.unsafe(` SELECT * FROM test_table t1 JOIN test_table t2 USING (id) `); const join3 = await sql.unsafe(` SELECT * FROM test_table t1 LEFT JOIN test_table t2 ON t1.id = t2.id RIGHT OUTER JOIN test_table t3 ON t2.id = t3.id FULL OUTER JOIN test_table t4 ON t3.id = t4.id CROSS JOIN test_table t5 INNER JOIN test_table t6 ON 1=1 `); const join4 = await sql.unsafe(` SELECT * FROM test_table t1 JOIN test_table t2 ON ( t1.id = t2.id AND t1.name = t2.name OR t1.value > t2.value AND EXISTS (SELECT 1 FROM test_table WHERE id = t1.id) ) `); }); test("handles weird but valid identifier quoting", async () => { await sql` SELECT [bracket table].[col 1], "weird-table"."col-2", \`backtick\`.\`col\`, test_table.id FROM [bracket table], "weird-table", \`backtick\`, test_table `; await sql`CREATE TABLE "table""with""quotes" ("col""umn" TEXT)`; await sql`SELECT "col""umn" FROM "table""with""quotes"`; await sql`CREATE TABLE "测试表" ("列名" TEXT)`; await sql`SELECT "列名" FROM "测试表"`; await sql`CREATE TABLE "SELECT" ("FROM" TEXT, "WHERE" INTEGER)`; await sql`SELECT "FROM", "WHERE" FROM "SELECT"`; }); test("handles complex string literals and escaping", async () => { await sql`SELECT 'It''s a test' as str`; await sql`SELECT 'Hello' || ' ' || 'World' as greeting`; await sql`SELECT X'48656C6C6F' as hex_string`; await sql`SELECT x'0123456789ABCDEF' as blob_data`; try { await sql`SELECT 'Line 1\nLine 2\tTabbed' as escaped`; } catch {} await sql`SELECT '测试' as unicode_str`; }); test("handles PRAGMA statements with various formats", async () => { await sql`PRAGMA table_info(test_table)`; await sql`PRAGMA cache_size = 2000`; await sql`PRAGMA table_info('test_table')`; await sql` PRAGMA foreign_keys = ON; PRAGMA journal_mode = WAL; PRAGMA synchronous = NORMAL; `; await sql`PRAGMA main.table_info('test_table')`; }); test("handles VACUUM and other maintenance commands", async () => { await sql`VACUUM`; const tempDb = `/tmp/test_vacuum_${Date.now()}.db`; try { await sql`VACUUM INTO '${tempDb}'`; } catch {} await sql`ANALYZE`; await sql`ANALYZE test_table`; await sql`ANALYZE main.test_table`; try { await sql`REINDEX`; await sql`REINDEX test_table`; } catch {} }); test("handles triggers with complex syntax", async () => { await sql` CREATE TRIGGER IF NOT EXISTS my_trigger AFTER INSERT ON test_table BEGIN SELECT 1; END `; await sql` CREATE TRIGGER complex_trigger BEFORE UPDATE OF name, value ON test_table FOR EACH ROW WHEN NEW.value > OLD.value BEGIN SELECT NEW.value; SELECT OLD.value; UPDATE test_table SET value = NEW.value WHERE id != NEW.id; END `; await sql`CREATE VIEW test_view AS SELECT * FROM test_table`; await sql` CREATE TRIGGER view_trigger INSTEAD OF INSERT ON test_view BEGIN INSERT INTO test_table VALUES (NEW.id, NEW.name, NEW.value); END `; }); test("handles RETURNING clause variations", async () => { const res1 = await sql.unsafe(` INSERT INTO test_table (name, value) VALUES ('test', 100) RETURNING * `); expect(res1).toHaveLength(1); const res2 = await sql.unsafe(` UPDATE test_table SET value = value * 2 WHERE name = 'test' RETURNING id, value as new_value, value/2 as old_value `); const res3 = await sql.unsafe(` DELETE FROM test_table WHERE value > 1000 RETURNING id, name `); }); test("handles VALUES clause as table constructor", async () => { const vals1 = await sql.unsafe(` SELECT 1 as a, 'a' as b UNION ALL SELECT 2, 'b' UNION ALL SELECT 3, 'c' `); expect(vals1).toHaveLength(3); const vals2 = await sql.unsafe(` WITH t(num, letter) AS ( SELECT 1, 'x' UNION ALL SELECT 2, 'y' UNION ALL SELECT 3, 'z' ) SELECT * FROM t `); expect(vals2).toHaveLength(3); const vals3 = await sql.unsafe(` SELECT 1 + 1 as col1, UPPER('hello') as col2 UNION ALL SELECT 2 * 3, LOWER('WORLD') UNION ALL SELECT (SELECT COUNT(*) FROM test_table), 'count' `); expect(vals3).toHaveLength(3); }); test("handles complex CASE expressions", async () => { await sql` SELECT CASE name WHEN 'a' THEN 'Alpha' WHEN 'b' THEN 'Beta' ELSE 'Other' END as name_full FROM test_table `; await sql` SELECT CASE WHEN value < 10 AND name = 'a' THEN 'Low A' WHEN value BETWEEN 10 AND 20 THEN 'Medium' WHEN value > 20 OR name IN ('x', 'y', 'z') THEN 'High or Special' WHEN EXISTS (SELECT 1 FROM test_table t2 WHERE t2.id > test_table.id) THEN 'Has Greater' ELSE 'Default' END as category FROM test_table `; await sql` SELECT CASE WHEN value > 50 THEN CASE name WHEN 'a' THEN 'High A' ELSE 'High Other' END ELSE 'Low' END as nested_category FROM test_table `; }); test("handles complex subqueries and correlated subqueries", async () => { await sql` SELECT name, (SELECT COUNT(*) FROM test_table t2 WHERE t2.name = t1.name) as name_count, (SELECT MAX(value) FROM test_table t2 WHERE t2.id < t1.id) as max_before FROM test_table t1 `; await sql` SELECT * FROM ( SELECT * FROM test_table t1 WHERE value > (SELECT AVG(value) FROM test_table t2 WHERE t2.name = t1.name) ) subq `; await sql` SELECT * FROM test_table t1 WHERE EXISTS ( SELECT 1 FROM test_table t2 WHERE t2.id != t1.id AND t2.value > t1.value ) AND NOT EXISTS ( SELECT 1 FROM test_table t3 WHERE t3.name = t1.name AND t3.id < t1.id ) `; await sql` SELECT * FROM test_table WHERE id IN (SELECT id FROM test_table WHERE value > 10) AND name NOT IN (SELECT DISTINCT name FROM test_table WHERE value < 5) `; await sql` UPDATE test_table SET value = ( SELECT AVG(value) FROM test_table t2 WHERE t2.name = test_table.name ) WHERE id IN (SELECT id FROM test_table WHERE name = 'a') `; }); test("handles weird spacing, comments and formatting", async () => { await sql`SELECT*FROM test_table WHERE id=1 AND name='a'OR value>10`; await sql` SELECT id , name FROM test_table WHERE id = 1 `; await sql` /* start */ SELECT /* mid */ * /* comment */ FROM /* another */ test_table -- line comment WHERE id = 1 -- inline comment /* multi line comment */ AND name = 'test' `; await sql` SELECT id, -- comment 1 /* comment 2 */ name, value -- comment 3 /* comment 4 */ FROM test_table /* WHERE clause comment */ WHERE /* inline */ id /* another */ = /* more */ 1 `; }); test("handles special SQLite syntax features", async () => { try { await sql` SELECT * FROM test_table INDEXED BY sqlite_autoindex_test_table_1 WHERE id = 1 `; } catch {} await sql` SELECT * FROM test_table NOT INDEXED WHERE id = 1 `; await sql` SELECT * FROM test_table WHERE name GLOB 'a*' `; try { await sql` SELECT * FROM test_table WHERE name MATCH 'search query' `; } catch {} await sql` SELECT * FROM test_table WHERE (id, name) IN ((1, 'a'), (2, 'b')) `; await sql` SELECT * FROM test_table WHERE value IS NOT NULL AND name IS NOT 'test' `; }); test("handles table-valued functions", async () => { await sql` SELECT * FROM json_each('["a", "b", "c"]') `; await sql` SELECT * FROM json_tree('{"a": [1, 2], "b": {"c": 3}}') `; try { await sql` SELECT value FROM generate_series(1, 10, 2) `; } catch {} await sql` SELECT * FROM test_table JOIN json_each('["a", "b"]') ON test_table.name = json_each.value `; }); test("handles COLLATE clauses", async () => { await sql`CREATE TABLE collate_test (name TEXT COLLATE NOCASE)`; await sql` SELECT * FROM test_table WHERE name = 'A' COLLATE NOCASE `; await sql` SELECT * FROM test_table ORDER BY name COLLATE NOCASE DESC `; await sql` SELECT * FROM test_table WHERE name COLLATE BINARY = 'a' ORDER BY name COLLATE NOCASE, value COLLATE RTRIM `; }); test("handles date/time functions with complex formatting", async () => { await sql` SELECT datetime('now'), datetime('now', '+1 day', '-1 hour', '+30 minutes'), date('now', 'start of month', '+1 month', '-1 day'), time('12:34:56'), julianday('now'), strftime('%Y-%m-%d %H:%M:%S', 'now', 'localtime'), strftime('%s', 'now'), unixepoch('now') `; await sql` SELECT * FROM test_table WHERE datetime('now') > datetime('2023-01-01') `; }); test("handles savepoints and nested transactions", async () => { await sql`SAVEPOINT sp1`; await sql`INSERT INTO test_table VALUES (999, 'savepoint', 999)`; await sql`SAVEPOINT sp2`; await sql`UPDATE test_table SET value = 0 WHERE id = 999`; await sql`ROLLBACK TO sp2`; await sql`RELEASE sp1`; await sql` SAVEPOINT outer; SAVEPOINT inner; ROLLBACK TO inner; RELEASE outer; `; }); test("handles extremely nested queries", async () => { await sql` SELECT * FROM ( SELECT * FROM ( SELECT * FROM ( SELECT * FROM ( SELECT * FROM test_table ) l4 ) l3 ) l2 ) l1 `; await sql` SELECT CASE WHEN (value + (10 * (20 - (30 / (40 + (50 - 60)))))) > 0 THEN ((((1 + 2) * 3) - 4) / 5) ELSE (((((6))))) END as nested_calc FROM test_table `; await sql` SELECT CASE WHEN id = 1 THEN CASE WHEN value > 10 THEN CASE WHEN name = 'a' THEN 'A1>10' ELSE 'Other1>10' END ELSE 'Low1' END ELSE 'NotOne' END as super_nested FROM test_table `; }); test("handles FILTER clauses on aggregate functions", async () => { await sql` SELECT COUNT(*) FILTER (WHERE value > 10) as high_count, SUM(value) FILTER (WHERE name = 'a') as a_sum, AVG(value) FILTER (WHERE id < 5) as early_avg FROM test_table `; await sql` SELECT SUM(value) FILTER (WHERE name = 'a') OVER (ORDER BY id) as filtered_sum FROM test_table `; await sql` SELECT COUNT(*) FILTER (WHERE value > 10 AND name = 'a') as complex_filter, MAX(value) FILTER (WHERE id IN (1,2,3)) as id_filter FROM test_table GROUP BY name `; }); test("handles special numeric literals", async () => { await sql`SELECT 1.23e10, 4.56E-7, .5e2, 9.`; await sql`SELECT 0x1234, 0xDEADBEEF, 0xffffffff`; await sql`SELECT 1e308 * 10, 0.0 / 0.0`; await sql` SELECT 999999999999999999999999999999999999999, 0.000000000000000000000000000000000001 `; }); test("handles compound SELECT statements", async () => { expect( await sql` SELECT id, name FROM test_table UNION SELECT id + 100, 'union' FROM test_table `.execute(), ).toMatchInlineSnapshot(` [ { "id": null, "name": "test", }, { "id": null, "name": "union", }, { "id": 1, "name": "a", }, { "id": 2, "name": "b", }, { "id": 3, "name": "a", }, { "id": 101, "name": "union", }, { "id": 102, "name": "union", }, { "id": 103, "name": "union", }, { "id": 999, "name": "savepoint", }, { "id": 1099, "name": "union", }, ] `); expect( await sql` SELECT * FROM test_table UNION ALL SELECT * FROM test_table `.execute(), ).toMatchInlineSnapshot(` [ { "id": 1, "name": "a", "value": 20.5, }, { "id": 2, "name": "b", "value": 20.5, }, { "id": 3, "name": "a", "value": 20.5, }, { "id": null, "name": "test", "value": 200, }, { "id": 999, "name": "savepoint", "value": 999, }, { "id": 1, "name": "a", "value": 20.5, }, { "id": 2, "name": "b", "value": 20.5, }, { "id": 3, "name": "a", "value": 20.5, }, { "id": null, "name": "test", "value": 200, }, { "id": 999, "name": "savepoint", "value": 999, }, ] `); expect( await sql` SELECT name FROM test_table WHERE value > 10 INTERSECT SELECT name FROM test_table WHERE id < 5 `.execute(), ).toMatchInlineSnapshot(` [ { "name": "a", }, { "name": "b", }, ] `); expect( await sql` SELECT * FROM test_table EXCEPT SELECT * FROM test_table WHERE name = 'excluded' `.execute(), ).toMatchInlineSnapshot(` [ { "id": null, "name": "test", "value": 200, }, { "id": 1, "name": "a", "value": 20.5, }, { "id": 2, "name": "b", "value": 20.5, }, { "id": 3, "name": "a", "value": 20.5, }, { "id": 999, "name": "savepoint", "value": 999, }, ] `); expect( await sql` SELECT id FROM test_table WHERE value > 20 UNION SELECT id FROM test_table WHERE name = 'a' EXCEPT SELECT id FROM test_table WHERE id > 100 INTERSECT SELECT id FROM test_table WHERE value < 50 `.execute(), ).toMatchInlineSnapshot(` [ { "id": 1, }, { "id": 2, }, { "id": 3, }, ] `); expect( await sql` SELECT * FROM test_table WHERE value > 10 UNION ALL SELECT * FROM test_table WHERE value <= 10 ORDER BY value DESC LIMIT 5 `.execute(), ).toMatchInlineSnapshot(` [ { "id": 999, "name": "savepoint", "value": 999, }, { "id": null, "name": "test", "value": 200, }, { "id": 1, "name": "a", "value": 20.5, }, { "id": 2, "name": "b", "value": 20.5, }, { "id": 3, "name": "a", "value": 20.5, }, ] `); }); test("handles CREATE TABLE with all constraint types", async () => { await sql` CREATE TABLE IF NOT EXISTS complex_constraints ( id INTEGER PRIMARY KEY AUTOINCREMENT, email TEXT UNIQUE NOT NULL CHECK(email LIKE '%@%'), age INTEGER CHECK(age >= 0 AND age <= 150), status TEXT DEFAULT 'active' CHECK(status IN ('active', 'inactive', 'pending')), parent_id INTEGER REFERENCES test_table(id) ON DELETE CASCADE ON UPDATE RESTRICT, created_at TEXT DEFAULT CURRENT_TIMESTAMP, data JSON CHECK(json_valid(data)), UNIQUE(email, status), CHECK(age > 18 OR parent_id IS NOT NULL), FOREIGN KEY (parent_id) REFERENCES test_table(id) ) `; await sql` CREATE TABLE strict_table ( id INTEGER PRIMARY KEY, int_col INT, real_col REAL, text_col TEXT, blob_col BLOB, any_col ANY ) STRICT `; await sql` CREATE TABLE without_rowid_table ( id INTEGER PRIMARY KEY, value TEXT ) WITHOUT ROWID `; await sql` CREATE TABLE generated_cols ( radius REAL, area REAL GENERATED ALWAYS AS (3.14159 * radius * radius) STORED, circumference REAL GENERATED ALWAYS AS (2 * 3.14159 * radius) VIRTUAL ) `; }); test("handles exotic but valid SQL patterns", async () => { await sql`SELECT 'text with; semicolon' as str`; await sql` SELECT id as "SELECT", name as "FROM", value as "WHERE" FROM test_table `; await sql`SELECT * FROM test_table WHERE 1`; await sql`SELECT * FROM test_table WHERE 0`; await sql`SELECT * FROM test_table WHERE NULL`; await sql` SELECT * FROM test_table WHERE NOT NOT (value > 10) `; await sql` SELECT (((id))), ((name)), (((((value))))) FROM (((test_table))) WHERE ((((id = 1)))) `; await sql`SELECT 1`; await sql`SELECT 2;`; await sql.unsafe(`SELECT 3;;`); await sql.unsafe(`;SELECT 4`); await sql.unsafe(`;;SELECT 5;;`); await sql`CREATE TABLE weird_cols ("123" TEXT, "!" INTEGER, "@#$" REAL)`; await sql.unsafe(`SELECT "123", "!", "@#$" FROM weird_cols`); const longName = "a".repeat(50_000_000); await sql.unsafe(`CREATE TABLE "${longName}" (col TEXT)`); await sql.unsafe(`SELECT * FROM "${longName}"`); await sql.unsafe(`DROP TABLE "${longName}"`); }); describe("Result Modes", () => { test("values() mode returns arrays instead of objects", async () => { const dir = tempDirWithFiles("sqlite-values-mode", {}); const sql = new SQL(`sqlite://${dir}/test.db`); await sql`CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)`; await sql`INSERT INTO users (name, age) VALUES ('Alice', 30), ('Bob', 25), ('Charlie', 35)`; const objectResults = await sql`SELECT id, name, age FROM users ORDER BY id`; expect(objectResults).toHaveLength(3); expect(objectResults[0]).toEqual({ id: 1, name: "Alice", age: 30 }); expect(objectResults[1]).toEqual({ id: 2, name: "Bob", age: 25 }); expect(objectResults[2]).toEqual({ id: 3, name: "Charlie", age: 35 }); const valuesResults = await sql`SELECT id, name, age FROM users ORDER BY id`.values(); expect(valuesResults).toHaveLength(3); expect(valuesResults[0]).toEqual([1, "Alice", 30]); expect(valuesResults[1]).toEqual([2, "Bob", 25]); expect(valuesResults[2]).toEqual([3, "Charlie", 35]); await sql.close(); await rm(dir, { recursive: true }); }); test("raw() mode returns buffers for SQLite", async () => { const dir = tempDirWithFiles("sqlite-raw-mode", {}); const sql = new SQL(`sqlite://${dir}/test.db`); await sql`CREATE TABLE test (id INTEGER, name TEXT, data BLOB, score REAL)`; await sql`INSERT INTO test VALUES (42, 'hello', ${Buffer.from([1, 2, 3])}, 3.14)`; const result = await sql`SELECT * FROM test`.raw(); expect(result).toBeArray(); expect(result).toHaveLength(1); const row = result[0]; expect(row).toBeArray(); expect(row).toHaveLength(4); expect(row[0]).toBeInstanceOf(Uint8Array); expect(row[1]).toBeInstanceOf(Uint8Array); expect(row[2]).toBeInstanceOf(Uint8Array); expect(row[3]).toBeInstanceOf(Uint8Array); const idBuf = row[0] as Uint8Array; const idView = new DataView(idBuf.buffer, idBuf.byteOffset, idBuf.byteLength); expect(idView.getBigInt64(0, true)).toBe(42n); const nameBuf = row[1] as Uint8Array; expect(new TextDecoder().decode(nameBuf)).toBe("hello"); const dataBuf = row[2] as Uint8Array; expect(Array.from(dataBuf)).toEqual([1, 2, 3]); const scoreBuf = row[3] as Uint8Array; const scoreView = new DataView(scoreBuf.buffer, scoreBuf.byteOffset, scoreBuf.byteLength); expect(scoreView.getFloat64(0, true)).toBe(3.14); await sql`INSERT INTO test VALUES (NULL, NULL, NULL, NULL)`; const resultWithNull = await sql`SELECT * FROM test WHERE id IS NULL`.raw(); expect(resultWithNull).toHaveLength(1); const nullRow = resultWithNull[0]; expect(nullRow[0]).toBeNull(); expect(nullRow[1]).toBeNull(); expect(nullRow[2]).toBeNull(); expect(nullRow[3]).toBeNull(); await sql.close(); await rm(dir, { recursive: true }); }); test("values() mode works with PRAGMA commands", async () => { const dir = tempDirWithFiles("sqlite-values-pragma", {}); const sql = new SQL(`sqlite://${dir}/test.db`); const pragmaValues = await sql`PRAGMA table_info('sqlite_master')`.values(); expect(Array.isArray(pragmaValues)).toBe(true); if (pragmaValues.length > 0) { expect(Array.isArray(pragmaValues[0])).toBe(true); } expect(pragmaValues).toMatchSnapshot(); await sql.close(); await rm(dir, { recursive: true }); }); }); }); describe("Unicode & Encoding Fuzzing Tests", () => { let sql: SQL; beforeEach(async () => { sql = new SQL("sqlite://:memory:"); }); afterEach(async () => { await sql?.close(); }); test("handles extensive Unicode scripts and languages", async () => { await sql`CREATE TABLE unicode_fuzz (id INTEGER PRIMARY KEY, text_data TEXT, description TEXT)`; const unicodeTests = [ // Japanese (Hiragana, Katakana, Kanji) { text: "ひらがな", desc: "Hiragana" }, { text: "カタカナ", desc: "Katakana" }, { text: "漢字", desc: "Kanji" }, { text: "日本語の文章です。", desc: "Japanese sentence" }, { text: "。・゚゚・(>_<)・゚゚・。", desc: "Japanese emoticon" }, { text: "㊗️㊙️㊟", desc: "Circled ideographs" }, // Arabic (RTL) { text: "مرحبا بالعالم", desc: "Arabic hello world" }, { text: "السَّلَامُ عَلَيْكُمْ", desc: "Arabic with diacritics" }, { text: "١٢٣٤٥٦٧٨٩٠", desc: "Arabic-Indic digits" }, { text: "ﷺ", desc: "Arabic ligature" }, { text: "ﺍﺏﺕﺙﺝﺡﺥﺩﺫﺭﺯ", desc: "Arabic presentation forms" }, // Hebrew (RTL) { text: "שָׁלוֹם עוֹלָם", desc: "Hebrew with vowel points" }, { text: "עִבְרִית", desc: "Hebrew word" }, { text: "א״ב ג״ד", desc: "Hebrew with geresh" }, // Cyrillic { text: "Привет мир", desc: "Russian" }, { text: "Здравствуйте", desc: "Russian greeting" }, { text: "ЁЖИК", desc: "Russian caps with Ё" }, { text: "Ѳѳ Ѵѵ Ѱѱ", desc: "Old Cyrillic" }, // Greek { text: "Γειά σου κόσμε", desc: "Greek hello world" }, { text: "Ελληνικά", desc: "Greek word" }, { text: "Α Β Γ Δ Ε Ζ Η Θ", desc: "Greek alphabet" }, { text: "άέήίόύώ", desc: "Greek with tonos" }, // Thai { text: "สวัสดีชาวโลก", desc: "Thai hello world" }, { text: "ภาษาไทย", desc: "Thai language" }, { text: "๏๐๑๒๓๔๕๖๗๘๙", desc: "Thai digits and symbols" }, // Korean { text: "안녕하세요", desc: "Korean greeting" }, { text: "한글", desc: "Hangul" }, { text: "ㄱㄴㄷㄹㅁㅂㅅ", desc: "Korean Jamo" }, // Chinese { text: "你好世界", desc: "Chinese simplified" }, { text: "繁體中文", desc: "Traditional Chinese" }, { text: "㊀㊁㊂㊃㊄㊅", desc: "Circled Chinese" }, // Devanagari (Hindi) { text: "नमस्ते दुनिया", desc: "Hindi hello world" }, { text: "अआइईउऊऋॠ", desc: "Devanagari vowels" }, { text: "०१२३४५६७८९", desc: "Devanagari digits" }, // Tamil { text: "வணக்கம் உலகம்", desc: "Tamil hello world" }, { text: "தமிழ்", desc: "Tamil word" }, // Emoji sequences { text: "👨‍👩‍👧‍👦", desc: "Family emoji ZWJ sequence" }, { text: "👨🏻‍💻", desc: "Man technologist with skin tone" }, { text: "🏳️‍🌈", desc: "Rainbow flag" }, { text: "🧑‍🤝‍🧑", desc: "People holding hands" }, { text: "👁️‍🗨️", desc: "Eye in speech bubble" }, { text: "🏴󠁧󠁢󠁥󠁮󠁧󠁿", desc: "England flag" }, { text: "🏴󠁧󠁢󠁳󠁣󠁴󠁿", desc: "Scotland flag" }, { text: "🏴󠁧󠁢󠁷󠁬󠁳󠁿", desc: "Wales flag" }, // Mathematical symbols { text: "∀∃∅∈∉⊂⊃⊆⊇", desc: "Set theory symbols" }, { text: "∫∬∭∮∯∰", desc: "Integral symbols" }, { text: "√∛∜", desc: "Root symbols" }, { text: "𝕳𝖊𝖑𝖑𝖔", desc: "Mathematical bold Fraktur" }, { text: "𝓗𝓮𝓵𝓵𝓸", desc: "Mathematical bold script" }, { text: "𝒽ℯ𝓁𝓁ℴ", desc: "Mathematical italic" }, // Combining characters { text: "e\u0301", desc: "e with combining acute" }, { text: "n\u0303", desc: "n with combining tilde" }, { text: "a\u0300\u0301\u0302\u0303\u0304", desc: "a with multiple combining marks" }, { text: "Z̴̧̢̛͔̳̮̤̣̈́̊̄͒a̸̧̨̺̯̟̯̿̈́͊̕l̶̢̜̦̣̇̆̾g̸̨̣̲̈́͊̍̕ȏ̷̧̜̠̣̊", desc: "Zalgo text" }, // Zero-width characters { text: "test\u200Bword", desc: "Zero-width space" }, { text: "test\u200Cword", desc: "Zero-width non-joiner" }, { text: "test\u200Dword", desc: "Zero-width joiner" }, { text: "test\uFEFFword", desc: "Zero-width no-break space" }, // RTL/LTR mixing { text: "Hello שלום World", desc: "Mixed LTR/RTL" }, { text: "العربية English עברית", desc: "Multiple script directions" }, { text: "\u202Eevil text", desc: "RLO override" }, { text: "\u202Dforce LTR\u202C", desc: "LTR override with pop" }, // Special Unicode blocks { text: "♠♣♥♦", desc: "Card suits" }, { text: "☀☁☂☃☄★☆", desc: "Weather symbols" }, { text: "♈♉♊♋♌♍♎♏", desc: "Zodiac symbols" }, { text: "⚀⚁⚂⚃⚄⚅", desc: "Dice faces" }, { text: "❶❷❸❹❺❻❼❽❾❿", desc: "Circled numbers" }, // Box drawing { text: "┌─┬─┐│ ││ │├─┼─┤└─┴─┘", desc: "Box drawing characters" }, { text: "╔═╦═╗║ ║║ ║╠═╬═╣╚═╩═╝", desc: "Double box drawing" }, // Currency symbols { text: "$€£¥₹₽₩₨₪₫₱", desc: "Currency symbols" }, // Superscript/Subscript { text: "x²y³z⁴", desc: "Superscript" }, { text: "H₂O", desc: "Subscript" }, // Weird UTF-8 edge cases { text: "\uD800", desc: "High surrogate (invalid alone)" }, { text: "\uDFFF", desc: "Low surrogate (invalid alone)" }, { text: "\uFFFD", desc: "Replacement character" }, { text: "\uFFFE", desc: "Byte order mark inverse" }, { text: String.fromCodePoint(0x10ffff), desc: "Max valid Unicode" }, { text: String.fromCodePoint(0x1f4a9), desc: "Pile of poo emoji" }, // Various quote marks { text: `''‚""„`, desc: "Various quotes" }, { text: "«»‹›", desc: "Guillemets" }, { text: "「」『』", desc: "CJK quotes" }, // Control characters mixed with text { text: "hello\x00world", desc: "Null in middle" }, { text: "tab\there", desc: "Tab character" }, { text: "line\nbreak", desc: "Newline" }, { text: "carriage\rreturn", desc: "Carriage return" }, // Long repetitive Unicode { text: "🎉".repeat(100), desc: "100 party emojis" }, { text: "あ".repeat(500), desc: "500 Japanese characters" }, { text: "۝".repeat(200), desc: "200 Arabic symbols" }, // Mixed everything chaos { text: "Hello世界مرحبا🌍שלום мир🎉", desc: "Multiple scripts and emoji" }, { text: "a̐éö̲ūï̍œ̃", desc: "Latin with various diacritics" }, { text: "㊗️エンコーディング🎌テスト✨", desc: "Japanese with emoji" }, ]; // Insert all test cases for (let i = 0; i < unicodeTests.length; i++) { const { text, desc } = unicodeTests[i]; await sql`INSERT INTO unicode_fuzz VALUES (${i}, ${text}, ${desc})`; } // Verify all data was stored and retrieved correctly for (let i = 0; i < unicodeTests.length; i++) { const { text, desc } = unicodeTests[i]; const result = await sql`SELECT text_data, description FROM unicode_fuzz WHERE id = ${i}`; expect(result).toHaveLength(1); // SQLite's actual behavior with problematic Unicode: // - Lone surrogates (\uD800, \uDFFF) are dropped (become empty string) // - BOM inverse (\uFFFE) is dropped (becomes empty string) // - Null characters are preserved (not truncated) const droppedCharacters = [ "High surrogate (invalid alone)", "Low surrogate (invalid alone)", "Byte order mark inverse", ]; if (droppedCharacters.includes(desc)) { // SQLite drops these invalid UTF-8 sequences expect(result[0].text_data).toBe(""); } else { // All other characters should be preserved exactly, including null bytes expect(result[0].text_data).toBe(text); } expect(result[0].description).toBe(desc); } // Test searching with Unicode const arabicSearch = await sql`SELECT * FROM unicode_fuzz WHERE text_data LIKE ${"%مرحبا%"}`; expect(arabicSearch.length).toBeGreaterThan(0); const emojiSearch = await sql`SELECT * FROM unicode_fuzz WHERE text_data LIKE ${"%🎉%"}`; expect(emojiSearch.length).toBeGreaterThan(0); }); test("handles Unicode in column names and table names", async () => { // Table names with Unicode await sql`CREATE TABLE "日本語テーブル" (id INTEGER, value TEXT)`; await sql`INSERT INTO "日本語テーブル" VALUES (1, 'test')`; const result1 = await sql`SELECT * FROM "日本語テーブル"`; expect(result1).toHaveLength(1); // Column names with Unicode await sql`CREATE TABLE unicode_cols ("列名" TEXT, "عمود" TEXT, "στήλη" TEXT)`; await sql`INSERT INTO unicode_cols VALUES ('Japanese', 'Arabic', 'Greek')`; const result2 = await sql`SELECT * FROM unicode_cols`; expect(result2[0]["列名"]).toBe("Japanese"); expect(result2[0]["عمود"]).toBe("Arabic"); expect(result2[0]["στήλη"]).toBe("Greek"); }); test("handles Unicode in SQL functions", async () => { await sql`CREATE TABLE unicode_func_test (id INTEGER, text_data TEXT)`; const testCases = [ { text: "HELLO WORLD", expected_lower: "hello world" }, { text: "ЁЖИК", expected_lower: "ёжик" }, { text: "ΔΙΑΦΟΡΆ", expected_lower: "διαφορά" }, ]; for (let i = 0; i < testCases.length; i++) { const { text } = testCases[i]; await sql`INSERT INTO unicode_func_test VALUES (${i}, ${text})`; } // Test LENGTH with Unicode await sql`INSERT INTO unicode_func_test VALUES (100, ${"🎉🎊🎈"})`; const lengthResult = await sql`SELECT LENGTH(text_data) as len FROM unicode_func_test WHERE id = 100`; // Note: SQLite LENGTH returns byte count for UTF-8 expect(lengthResult[0].len).toBeGreaterThan(0); // Test SUBSTR with Unicode await sql`INSERT INTO unicode_func_test VALUES (101, ${"Hello世界"})`; const substrResult = await sql`SELECT SUBSTR(text_data, 6, 2) as sub FROM unicode_func_test WHERE id = 101`; expect(substrResult[0].sub).toBe("世界"); }); test("handles Unicode normalization edge cases", async () => { await sql`CREATE TABLE normalization_test (id INTEGER, text_data TEXT)`; // Different Unicode normalizations of "é" const normalizations = [ "\u00E9", // NFC: é (single character) "e\u0301", // NFD: e + combining acute "\u0065\u0301", // NFD explicit ]; for (let i = 0; i < normalizations.length; i++) { await sql`INSERT INTO normalization_test VALUES (${i}, ${normalizations[i]})`; const result = await sql`SELECT text_data FROM normalization_test WHERE id = ${i}`; expect(result[0].text_data).toBe(normalizations[i]); } }); test("handles binary data that looks like UTF-8", async () => { await sql`CREATE TABLE binary_test (id INTEGER, data BLOB)`; // Invalid UTF-8 sequences const invalidSequences = [ Buffer.from([0xff, 0xfe, 0xfd]), // Invalid UTF-8 start bytes Buffer.from([0xc0, 0x80]), // Overlong encoding Buffer.from([0xed, 0xa0, 0x80]), // UTF-16 surrogate Buffer.from([0xf4, 0x90, 0x80, 0x80]), // Code point > U+10FFFF Buffer.from([0xc2]), // Incomplete sequence Buffer.from([0xe0, 0x80, 0x80]), // Overlong 3-byte Buffer.from([0xf0, 0x80, 0x80, 0x80]), // Overlong 4-byte ]; for (let i = 0; i < invalidSequences.length; i++) { await sql`INSERT INTO binary_test VALUES (${i}, ${invalidSequences[i]})`; const result = await sql`SELECT data FROM binary_test WHERE id = ${i}`; expect(Buffer.from(result[0].data)).toEqual(invalidSequences[i]); } }); test("handles massive Unicode string operations", async () => { await sql`CREATE TABLE massive_unicode (id INTEGER, text_data TEXT)`; // Create a massive string with various Unicode const components = ["English", "日本語", "العربية", "עברית", "Ελληνικά", "🎉", "👨‍👩‍👧‍👦", "∫∂∇", "№", "™", "©", "®"]; const massiveString = components.map(c => c.repeat(100)).join(" "); await sql`INSERT INTO massive_unicode VALUES (1, ${massiveString})`; const result = await sql`SELECT text_data FROM massive_unicode WHERE id = 1`; expect(result[0].text_data).toBe(massiveString); // Test with LIKE on massive Unicode string const likeResult = await sql`SELECT id FROM massive_unicode WHERE text_data LIKE ${"%日本語%"}`; expect(likeResult).toHaveLength(1); }); test("handles Unicode in prepared statement parameters", async () => { await sql`CREATE TABLE param_test (id INTEGER, text_data TEXT)`; const unicodeParams = [ "🚀 Launch", "مرحبا parameters", "パラメータ", "\u0000embedded null", "tab\there", "new\nline", ]; // Test with direct parameters for (let i = 0; i < unicodeParams.length; i++) { const param = unicodeParams[i]; await sql`INSERT INTO param_test VALUES (${i}, ${param})`; } // Verify all parameters were handled correctly for (let i = 0; i < unicodeParams.length; i++) { const result = await sql`SELECT text_data FROM param_test WHERE id = ${i}`; expect(result[0].text_data).toBe(unicodeParams[i]); } // Test WHERE clause with Unicode parameter const whereResult = await sql`SELECT * FROM param_test WHERE text_data = ${"🚀 Launch"}`; expect(whereResult).toHaveLength(1); expect(whereResult[0].id).toBe(0); }); test("handles Unicode collation and sorting", async () => { await sql`CREATE TABLE collation_test (id INTEGER, text_data TEXT)`; const sortTestData = [ "zebra", "Zebra", "ZEBRA", "äpfel", "Äpfel", "апельсин", "Апельсин", "🍎", "🍊", "日本", "中国", "한국", ]; for (let i = 0; i < sortTestData.length; i++) { await sql`INSERT INTO collation_test VALUES (${i}, ${sortTestData[i]})`; } // Test ORDER BY with Unicode const ordered = await sql`SELECT text_data FROM collation_test ORDER BY text_data`; expect(ordered).toHaveLength(sortTestData.length); // Verify ordering happened (exact order depends on SQLite collation) expect(ordered[0].text_data).toBeDefined(); expect(ordered[ordered.length - 1].text_data).toBeDefined(); }); test("handles Unicode in JSON operations", async () => { await sql`CREATE TABLE json_unicode (id INTEGER, json_data TEXT)`; const jsonWithUnicode = { english: "Hello", japanese: "こんにちは", arabic: "مرحبا", emoji: "🎉🚀", special: "a\u0301\u0302\u0303", rtl: "Hello עברית World", }; const jsonString = JSON.stringify(jsonWithUnicode); await sql`INSERT INTO json_unicode VALUES (1, ${jsonString})`; const result = await sql`SELECT json_data FROM json_unicode WHERE id = 1`; const parsed = JSON.parse(result[0].json_data); expect(parsed.japanese).toBe("こんにちは"); expect(parsed.arabic).toBe("مرحبا"); expect(parsed.emoji).toBe("🎉🚀"); }); test("handles extreme edge cases and malformed sequences", async () => { await sql`CREATE TABLE edge_cases (id INTEGER, text_data TEXT, blob_data BLOB)`; const edgeCases = [ // Extremely long strings { text: "A".repeat(10000) + "🎉".repeat(1000) + "世".repeat(1000), desc: "Very long mixed" }, // Boundary values { text: String.fromCharCode(0), desc: "Null character" }, { text: String.fromCharCode(0xd7ff), desc: "Before surrogates" }, { text: String.fromCharCode(0xe000), desc: "After surrogates" }, { text: String.fromCharCode(0xfffd), desc: "Replacement char" }, // Mixed direction markers { text: "\u202A\u202B\u202C\u202D\u202E", desc: "All direction markers" }, // Variation selectors { text: "☃️", desc: "Snowman with variation selector" }, { text: "☃︎", desc: "Snowman text style" }, // Regional indicators (flags) { text: "🇺🇸🇯🇵🇬🇧🇫🇷🇩🇪", desc: "Multiple flags" }, // Skin tone modifiers { text: "👋🏻👋🏼👋🏽👋🏾👋🏿", desc: "Wave with all skin tones" }, // Zero width joiners in text { text: "पार्थ", desc: "Devanagari with ZWJ" }, // Invisible characters { text: "\u2060\u2061\u2062\u2063", desc: "Invisible math operators" }, { text: "\u2028\u2029", desc: "Line and paragraph separators" }, ]; for (let i = 0; i < edgeCases.length; i++) { const { text } = edgeCases[i]; await sql`INSERT INTO edge_cases VALUES (${i}, ${text}, ${Buffer.from(text)})`; const result = await sql`SELECT text_data, blob_data FROM edge_cases WHERE id = ${i}`; expect(result[0].text_data).toBe(text); expect(Buffer.from(result[0].blob_data).toString()).toBe(text); } }); });