From b48b5e8c135ca64e9c32a08f5b595d3a13461428 Mon Sep 17 00:00:00 2001 From: Osama Ansar Date: Thu, 3 Sep 2026 16:21:02 +0500 Subject: [PATCH] fix(sql-cache): measure the 5kb stored-SQL cap in UTF-8 bytes --- .../src/__tests__/sql_cache.test.ts | 62 +++++++++++++++++++ packages/libsql-client/src/sql_cache.ts | 4 +- 2 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 packages/libsql-client/src/__tests__/sql_cache.test.ts diff --git a/packages/libsql-client/src/__tests__/sql_cache.test.ts b/packages/libsql-client/src/__tests__/sql_cache.test.ts new file mode 100644 index 00000000..35567d80 --- /dev/null +++ b/packages/libsql-client/src/__tests__/sql_cache.test.ts @@ -0,0 +1,62 @@ +import { expect } from "@jest/globals"; +import type { Sql, SqlOwner } from "@libsql/hrana-client"; +import { Stmt } from "@libsql/hrana-client"; + +import { SqlCache } from "../sql_cache.js"; + +class FakeSqlOwner implements SqlOwner { + readonly stored: string[] = []; + + storeSql(sql: string): Sql { + this.stored.push(sql); + return { _sqlId: this.stored.length } as unknown as Sql; + } + + _closeSql(_sqlId: number): void {} +} + +function stmt(sql: string): Stmt { + return new Stmt(sql); +} + +describe("SqlCache.apply() stored-SQL size limit", () => { + test("caches a small statement", () => { + const owner = new FakeSqlOwner(); + const cache = new SqlCache(owner, 100); + const s = stmt("SELECT 1"); + + cache.apply([s]); + + expect(owner.stored).toEqual(["SELECT 1"]); + expect(typeof s.sql).not.toBe("string"); + }); + + test("does not cache a statement that reaches the 5kb cap in UTF-8 bytes while staying under it in UTF-16 code units", () => { + const owner = new FakeSqlOwner(); + const cache = new SqlCache(owner, 100); + // "借" is 1 UTF-16 code unit but 3 UTF-8 bytes: under the cap by length, over it by bytes. + const sql = `SELECT '${"借".repeat(2000)}'`; + expect(sql.length).toBeLessThan(5000); + expect(new TextEncoder().encode(sql).length).toBeGreaterThanOrEqual( + 5000, + ); + const s = stmt(sql); + + cache.apply([s]); + + expect(owner.stored).toEqual([]); + expect(s.sql).toBe(sql); + }); + + test("does not cache a statement over the cap in UTF-16 code units", () => { + const owner = new FakeSqlOwner(); + const cache = new SqlCache(owner, 100); + const sql = "x".repeat(5000); + const s = stmt(sql); + + cache.apply([s]); + + expect(owner.stored).toEqual([]); + expect(s.sql).toBe(sql); + }); +}); diff --git a/packages/libsql-client/src/sql_cache.ts b/packages/libsql-client/src/sql_cache.ts index a59a1a01..a7f813be 100644 --- a/packages/libsql-client/src/sql_cache.ts +++ b/packages/libsql-client/src/sql_cache.ts @@ -31,9 +31,9 @@ export class SqlCache { } const sqlText = hranaStmt.sql; - // Stored SQL cannot exceed 5kb. + // Stored SQL cannot exceed 5kb, which the server measures in UTF-8 bytes. // https://github.com/tursodatabase/libsql/blob/e9d637e051685f92b0da43849507b5ef4232fbeb/libsql-server/src/hrana/http/request.rs#L10 - if (sqlText.length >= 5000) { + if (new TextEncoder().encode(sqlText).length >= 5000) { continue; }