Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions packages/cloudflare/test/utils/internalSqlQuery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,17 @@ describe('targetsCloudflareInternalTable', () => {
['cf_ai_ prefix', 'INSERT INTO cf_ai_chat_stream_chunks (id) VALUES (?)'],
['cf_mcp_ prefix', 'SELECT * FROM cf_mcp_agent_event'],
['schema version', 'SELECT version FROM cf_schema_version'],
// SQLite upsert forms used by the agents framework for state/schedule/MCP persistence
['INSERT OR REPLACE', 'INSERT OR REPLACE INTO cf_agents_state (id, state) VALUES (?, ?)'],
[
'INSERT OR REPLACE with column list',
`INSERT OR REPLACE INTO cf_agents_mcp_servers ( id, name, server_url, client_id, auth_url,
callback_url, server_options )
VALUES ( ?, ?, ?, ?, ?, ?, ? )`,
],
['INSERT OR IGNORE', 'INSERT OR IGNORE INTO cf_agents_sub_agents (class, name) VALUES (?, ?)'],
['REPLACE INTO', 'REPLACE INTO cf_agents_queues (id, payload) VALUES (?, ?)'],
['UPDATE OR REPLACE', 'UPDATE OR REPLACE cf_agents_state SET state = ? WHERE id = ?'],
])('returns true for %s on internal tables', (_label, query) => {
expect(targetsCloudflareInternalTable(summarize(query))).toBe(true);
});
Expand Down Expand Up @@ -55,6 +66,9 @@ describe('targetsCloudflareInternalTable', () => {
['CREATE TABLE', 'CREATE TABLE users (id TEXT PRIMARY KEY)'],
['table with cf in the middle', 'SELECT * FROM my_cf_table'],
['table starting with cfg', 'SELECT * FROM cfg_settings'],
['INSERT OR REPLACE', 'INSERT OR REPLACE INTO users (id, name) VALUES (?, ?)'],
['REPLACE INTO', 'REPLACE INTO sessions (id, token) VALUES (?, ?)'],
['UPDATE OR IGNORE', 'UPDATE OR IGNORE products SET price = ? WHERE id = ?'],
])('returns false for %s on user tables', (_label, query) => {
expect(targetsCloudflareInternalTable(summarize(query))).toBe(false);
});
Expand All @@ -69,6 +83,14 @@ describe('targetsCloudflareInternalTable', () => {
expect(targetsCloudflareInternalTable(summarize('SELECT * FROM cf_reports_daily'), [/^cf_reports_/])).toBe(false);
});

it('returns false for an allowlisted table targeted by an upsert', () => {
expect(
targetsCloudflareInternalTable(summarize('INSERT OR REPLACE INTO cf_my_table (id) VALUES (?)'), [
'cf_my_table',
]),
).toBe(false);
});

it('requires an exact match for string entries', () => {
// Substring matches must not opt a table back in, otherwise `cf_` would allowlist everything.
expect(targetsCloudflareInternalTable(summarize('SELECT * FROM cf_agents_state'), ['cf_agents'])).toBe(true);
Expand Down
14 changes: 12 additions & 2 deletions packages/core/src/utils/sql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,18 @@ const DDL_RE = new RegExp(
'i',
);

const INSERT_RE = new RegExp(`^\\s*(?<operation>INSERT)\\s+INTO\\s+(?<table>${TABLE_NAME})`, 'i');
const UPDATE_RE = new RegExp(`^\\s*(?<operation>UPDATE)\\s+(?<table>${TABLE_NAME})`, 'i');
// SQLite upserts insert an optional conflict clause between operation and INTO
// (`INSERT OR REPLACE INTO`, https://sqlite.org/lang_insert.html), with `REPLACE INTO` as the
// standalone shorthand. The clause is filler like INTO — stripping it keeps upserts on the same
// low-cardinality summary as plain inserts.
const INSERT_RE = new RegExp(
`^\\s*(?<operation>INSERT|REPLACE)(?:\\s+OR\\s+(?:ROLLBACK|ABORT|FAIL|IGNORE|REPLACE))?\\s+INTO\\s+(?<table>${TABLE_NAME})`,
'i',
);
const UPDATE_RE = new RegExp(
`^\\s*(?<operation>UPDATE)(?:\\s+OR\\s+(?:ROLLBACK|ABORT|FAIL|IGNORE|REPLACE))?\\s+(?<table>${TABLE_NAME})`,
'i',
);
const DELETE_RE = new RegExp(`^\\s*(?<operation>DELETE)\\s+FROM\\s+(?<table>${TABLE_NAME})`, 'i');

const SELECT_RE = /^\s*\(?\s*(?<operation>SELECT)\b/i;
Expand Down
27 changes: 27 additions & 0 deletions packages/core/test/lib/utils/sql.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,39 @@ describe('getSqlQuerySummary', () => {
'INSERT shipping_details SELECT orders',
);
});

it.each([
['INSERT OR REPLACE INTO users (id) VALUES (?)', 'INSERT users'],
['INSERT OR IGNORE INTO users (id) VALUES (?)', 'INSERT users'],
['INSERT OR ABORT INTO users (id) VALUES (?)', 'INSERT users'],
['INSERT OR FAIL INTO users (id) VALUES (?)', 'INSERT users'],
['INSERT OR ROLLBACK INTO users (id) VALUES (?)', 'INSERT users'],
['insert or replace into orders (id) values (?)', 'insert orders'],
])('strips the SQLite conflict clause: %j => %j', (input, expected) => {
expect(getSqlQuerySummary(input)).toBe(expected);
});

it.each([
['REPLACE INTO users (id) VALUES (?)', 'REPLACE users'],
['replace into orders (id) values (?)', 'replace orders'],
['REPLACE INTO shipping_details SELECT * FROM orders', 'REPLACE shipping_details SELECT orders'],
])('handles the REPLACE INTO shorthand: %j => %j', (input, expected) => {
expect(getSqlQuerySummary(input)).toBe(expected);
});

it('captures INSERT OR REPLACE...SELECT with both targets', () => {
expect(getSqlQuerySummary('INSERT OR REPLACE INTO shipping_details SELECT * FROM orders')).toBe(
'INSERT shipping_details SELECT orders',
);
});
});

describe('UPDATE', () => {
it.each([
['UPDATE users SET name = ? WHERE id = ?', 'UPDATE users'],
['update orders SET status = ? WHERE created_at < ?', 'update orders'],
['UPDATE OR REPLACE users SET name = ? WHERE id = ?', 'UPDATE users'],
['UPDATE OR IGNORE orders SET status = ?', 'UPDATE orders'],
])('%j => %j', (input, expected) => {
expect(getSqlQuerySummary(input)).toBe(expected);
});
Expand Down
Loading