From 9b2be7ccbc22f9acb1b4335d8b48aa85da50237f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 3 Aug 2026 05:08:25 +0000 Subject: [PATCH 1/2] fix(sql-editor): count all Server Beam SQL toward MAX_SQL Plain sql`` under Beam no longer bypasses the cap. Bridge calls are queued so Promise.all cannot race the counter. Adds createBeamSqlCap helpers and coverage for sequential, mixed, and concurrent limits. Co-authored-by: huy.phan9 --- .../src/backend/api/code-cell-execute.test.ts | 86 ++++++++++++++++++- apps/web/src/backend/api/code-cell-execute.ts | 44 +++++++--- apps/web/src/shared/server-beam.test.ts | 23 +++++ apps/web/src/shared/server-beam.ts | 26 ++++++ docs/plans/server-beam.md | 2 +- 5 files changed, 166 insertions(+), 15 deletions(-) diff --git a/apps/web/src/backend/api/code-cell-execute.test.ts b/apps/web/src/backend/api/code-cell-execute.test.ts index a989fdf..ae9eeae 100644 --- a/apps/web/src/backend/api/code-cell-execute.test.ts +++ b/apps/web/src/backend/api/code-cell-execute.test.ts @@ -285,6 +285,90 @@ describe('code cell SQL bridge', () => { enforceBeamSqlOnCap: true, }); expect(result.ok).toBe(false); - if (!result.ok) expect(result.error).toMatch(new RegExp(`at most ${MAX_SQL} sql\\.on`, 'i')); + if (!result.ok) { + expect(result.error).toMatch(new RegExp(`at most ${MAX_SQL} SQL bridge calls`, 'i')); + } + }, 60_000); + + it('counts plain sql`` toward the Beam cap (not only sql.on)', async () => { + const calls: string[] = []; + const runQuery = async (text: string) => { + calls.push(text); + return [{ n: 1 }]; + }; + // Mix: MAX_SQL - 1 via plain sql, then one sql.on succeeds, then one more fails. + const body = + `for (let i = 0; i < ${MAX_SQL - 1}; i++) { await sql\`SELECT 1\`; }\n` + + `await sql.on('source')\`SELECT 2\`;\n` + + `try { await sql\`SELECT 3\`; return [{ ok: true }]; }\n` + + `catch (e) { return [{ ok: false, msg: String(e.message) }]; }`; + const result = await runCell(body, { + dialect: 'sqlite', + allowWrites: false, + runQuery, + beamDialects: { source: 'sqlite' }, + defaultBeamAlias: 'source', + enforceBeamSqlOnCap: true, + }); + if (!result.ok) throw new Error(result.error); + expect(result.rows[0]![0]).toBe(false); + expect(String(result.rows[0]![1])).toMatch(/at most .* SQL bridge calls/i); + // Cap rejects before the last runQuery — MAX_SQL successful bridge calls. + expect(calls.length).toBe(MAX_SQL); + }, 60_000); + + it('enforces the Beam cap under Promise.all concurrency', async () => { + let inFlight = 0; + let maxInFlight = 0; + const runQuery = async () => { + inFlight += 1; + maxInFlight = Math.max(maxInFlight, inFlight); + await new Promise((r) => setTimeout(r, 5)); + inFlight -= 1; + return [{ n: 1 }]; + }; + const body = + `const jobs = [];\n` + + `for (let i = 0; i < ${MAX_SQL + 5}; i++) {\n` + + ` jobs.push(sql.on('source')\`SELECT \${i} AS n\`);\n` + + `}\n` + + `const settled = await Promise.allSettled(jobs);\n` + + `const rejected = settled.filter((s) => s.status === 'rejected').length;\n` + + `const fulfilled = settled.filter((s) => s.status === 'fulfilled').length;\n` + + `return [{ fulfilled, rejected }];`; + const result = await runCell( + body, + { + dialect: 'sqlite', + allowWrites: false, + runQuery, + beamDialects: { source: 'sqlite' }, + defaultBeamAlias: 'source', + enforceBeamSqlOnCap: true, + }, + 60_000 + ); + if (!result.ok) throw new Error(result.error); + expect(result.rows[0]![0]).toBe(MAX_SQL); + expect(result.rows[0]![1]).toBe(5); + // Serialized Beam bridge: at most one query in flight at a time. + expect(maxInFlight).toBe(1); + }, 90_000); + + it('allows exactly MAX_SQL sequential Beam bridge calls', async () => { + const runQuery = async () => [{ n: 1 }]; + const body = + `for (let i = 0; i < ${MAX_SQL}; i++) { await sql.on('source')\`SELECT 1\`; }\n` + + `return [{ ok: true, n: ${MAX_SQL} }];`; + const result = await runCell(body, { + dialect: 'sqlite', + allowWrites: false, + runQuery, + beamDialects: { source: 'sqlite' }, + defaultBeamAlias: 'source', + enforceBeamSqlOnCap: true, + }); + if (!result.ok) throw new Error(result.error); + expect(result.rows).toEqual([[true, MAX_SQL]]); }, 60_000); }); diff --git a/apps/web/src/backend/api/code-cell-execute.ts b/apps/web/src/backend/api/code-cell-execute.ts index d356ab0..683fe1a 100644 --- a/apps/web/src/backend/api/code-cell-execute.ts +++ b/apps/web/src/backend/api/code-cell-execute.ts @@ -22,7 +22,7 @@ export type CellQueryRunner = ( alias?: string ) => Promise[]>; import { clampMaxRows } from './sql-execute'; -import { MAX_SQL } from '../../shared/server-beam'; +import { createBeamSqlCap } from '../../shared/server-beam'; export const MAX_CODE_CELL_LENGTH = 100_000; export const DEFAULT_CODE_CELL_TIMEOUT_MS = 10_000; @@ -135,7 +135,7 @@ function runInWorkerThread(args: { beamDialects?: Record; /** Server Beam: default alias for plain `sql`…``. */ defaultBeamAlias?: string; - /** When true, enforce max `sql.on()` calls per Execute. */ + /** When true, enforce max SQL bridge calls per Execute (all `sql` / `sql.on`). */ enforceBeamSqlOnCap?: boolean; }): Promise { return new Promise((resolve) => { @@ -144,6 +144,9 @@ function runInWorkerThread(args: { let timer: ReturnType | undefined; /** Bridged queries in flight; the cell clock is paused while > 0. */ let inFlight = 0; + const beamSqlCap = args.enforceBeamSqlOnCap ? createBeamSqlCap() : null; + /** Serialize Beam bridge work so cap + query stay ordered under Promise.all. */ + let beamBridgeTail: Promise = Promise.resolve(); const startTimer = () => { timer = setTimeout(() => { @@ -215,8 +218,6 @@ function runInWorkerThread(args: { startTimer(); - let sqlOnCount = 0; - const answerQuery = async (req: CellQueryRequest) => { pauseClock(); const reply = (res: CellQueryResponse) => { @@ -226,18 +227,35 @@ function runInWorkerThread(args: { /* worker already gone */ } }; - try { - if (!args.runQuery) throw new Error('This cell has no connection — select a credential first'); - if (args.enforceBeamSqlOnCap && req.viaOn) { - sqlOnCount += 1; - if (sqlOnCount > MAX_SQL) { - throw new Error( - `Server Beam allows at most ${MAX_SQL} sql.on() calls per editor Execute` - ); - } + + const run = async () => { + if (!args.runQuery) { + throw new Error('This cell has no connection — select a credential first'); } + // Every bridged call counts (plain `sql` and `sql.on`) when Beam is on. + beamSqlCap?.take(); const rows = await args.runQuery(req.text, req.params, req.alias); reply({ type: 'cell-query-result', id: req.id, ok: true, rows, rowCount: rows.length }); + }; + + try { + if (beamSqlCap) { + // Queue Beam bridge calls so concurrent cell Promise.all cannot + // interleave take()+query in surprising ways. + const prev = beamBridgeTail; + let release!: () => void; + beamBridgeTail = new Promise((r) => { + release = r; + }); + try { + await prev; + await run(); + } finally { + release(); + } + } else { + await run(); + } } catch (error: unknown) { reply({ type: 'cell-query-result', id: req.id, ok: false, error: errorMessage(error) }); } finally { diff --git a/apps/web/src/shared/server-beam.test.ts b/apps/web/src/shared/server-beam.test.ts index c01f2a4..3387ebc 100644 --- a/apps/web/src/shared/server-beam.test.ts +++ b/apps/web/src/shared/server-beam.test.ts @@ -3,6 +3,7 @@ import { MAX_SERVERS, MAX_SQL, beamAliasesForCount, + createBeamSqlCap, normalizeBeamAlias, parseBeamEndpoints, usesServerBeam, @@ -14,6 +15,28 @@ describe('server-beam', () => { expect(MAX_SQL).toBe(20); }); + it('createBeamSqlCap counts every take and rejects past max', () => { + const cap = createBeamSqlCap(3); + cap.take(); + cap.take(); + cap.take(); + expect(cap.count).toBe(3); + expect(() => cap.take()).toThrow(/at most 3 SQL bridge calls/i); + expect(cap.count).toBe(4); + }); + + it('createBeamSqlCap take() stays correct under interleaved microtasks', async () => { + const cap = createBeamSqlCap(5); + const tasks = Array.from({ length: 8 }, async () => { + await Promise.resolve(); + cap.take(); + }); + const results = await Promise.allSettled(tasks); + const rejected = results.filter((r) => r.status === 'rejected'); + expect(rejected.length).toBe(3); + expect(cap.count).toBe(8); + }); + it('detects sql.on usage', () => { expect(usesServerBeam('await sql.on("source")`SELECT 1`')).toBe(true); expect(usesServerBeam('await sql.on(\'target\')`SELECT 1`')).toBe(true); diff --git a/apps/web/src/shared/server-beam.ts b/apps/web/src/shared/server-beam.ts index a2c6257..97e49d5 100644 --- a/apps/web/src/shared/server-beam.ts +++ b/apps/web/src/shared/server-beam.ts @@ -13,6 +13,32 @@ export type BeamEndpointRef = { password?: string; }; +/** + * Per-Execute counter for Server Beam SQL bridge calls. + * Counts every bridged query (`sql`…`` and `sql.on`…``), not only `.on()`. + * `take()` is synchronous so concurrent `answerQuery` handlers (after an + * `await`) cannot slip past {@link MAX_SQL} on a single-threaded event loop. + */ +export function createBeamSqlCap(max = MAX_SQL): { + take: () => void; + readonly count: number; +} { + let count = 0; + return { + take() { + count += 1; + if (count > max) { + throw new Error( + `Server Beam allows at most ${max} SQL bridge calls per editor Execute` + ); + } + }, + get count() { + return count; + }, + }; +} + const ALIAS_RE = /^[a-zA-Z_][a-zA-Z0-9_]{0,63}$/; export function normalizeBeamAlias(raw: unknown): string | null { diff --git a/docs/plans/server-beam.md b/docs/plans/server-beam.md index a26c24e..17232d3 100644 --- a/docs/plans/server-beam.md +++ b/docs/plans/server-beam.md @@ -20,7 +20,7 @@ credentials are the Beam endpoints; clients choose source and target servers | Rule | Value | |------|--------| | Beam servers selectable per editor Execute | **up to 2** (source + target) | -| `sql.on()` calls per editor Execute | **up to 20** (`MAX_SQL`) | +| SQL bridge calls per editor Execute | **up to 20** (`MAX_SQL`) — every `sql` / `sql.on` | | Async / Promises in Node cells | **required** (already supported by code-cell runtime) | | Password / decrypt | Server-only via existing `resolveRef` / connection store | From d014432ae1dc3d4cbdf01302f99a327514570fdc Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 3 Aug 2026 05:08:34 +0000 Subject: [PATCH 2/2] docs: clarify Server Beam samples use SQL bridge cap Co-authored-by: huy.phan9 --- apps/web/src/frontend/lib/sqlEditorSamples.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/src/frontend/lib/sqlEditorSamples.ts b/apps/web/src/frontend/lib/sqlEditorSamples.ts index 04a1c3f..0dfba7b 100644 --- a/apps/web/src/frontend/lib/sqlEditorSamples.ts +++ b/apps/web/src/frontend/lib/sqlEditorSamples.ts @@ -524,7 +524,7 @@ return [ title: '★ Sample · Server Beam copy rows source → target', sql: `-- Server Beam — WRITES on target. Check TWO Destinations (source, then target). -- Turn Safe mode OFF. Creates fox_beam_demo on both sides, copies reshaped rows. --- Caps: 2 servers, up to 20 sql.on() calls per Execute. +-- Caps: 2 servers, up to 20 SQL bridge calls (sql / sql.on) per Execute. -- @node await sql.on('source')\`DROP TABLE IF EXISTS fox_beam_demo\`; @@ -570,7 +570,7 @@ return await sql.on('target')\`SELECT id, email, domain FROM fox_beam_demo ORDER title: '★ Sample · Server Beam chunked pull → push', sql: `-- Server Beam — WRITES. Two Destinations (source, then target). Safe mode OFF. -- Pulls from source in chunks and inserts into target (async / await). --- Stays within the sql.on() cap per Execute (2 setup + 2×(pull+push) + 1 verify). +-- Stays within the SQL bridge cap per Execute (2 setup + 2×(pull+push) + 1 verify). -- @node await sql.on('source')\`DROP TABLE IF EXISTS fox_beam_bulk\`;