SQL Expressions: Fix alerts with sql expressions that have a cte (#114852)

Fix for #114377 - fix alerts with sql expressions that have a cte
This commit is contained in:
Sarah Zinger
2025-12-05 10:14:02 -05:00
committed by GitHub
parent bf042afa98
commit 7cd10aa49e
2 changed files with 95 additions and 4 deletions
@@ -293,6 +293,61 @@ SELECT * FROM table1`)
expect(parseRefsFromSqlExpression('SELECT * FROM\ntable1')).toEqual(['table1']);
});
});
describe('CTE (Common Table Expression) handling', () => {
it('should exclude single CTE name from results', () => {
const query = 'WITH my_cte AS (SELECT * FROM table1) SELECT * FROM my_cte';
expect(parseRefsFromSqlExpression(query)).toEqual(['table1']);
});
it('should exclude multiple CTE names from results', () => {
const query = `
WITH cte1 AS (SELECT * FROM table1),
cte2 AS (SELECT * FROM table2)
SELECT * FROM cte1 JOIN cte2 ON cte1.id = cte2.id
`;
expect(parseRefsFromSqlExpression(query)).toEqual(['table1', 'table2']);
});
it('should handle CTEs with external table references in main query', () => {
const query = `
WITH summary AS (SELECT id, count FROM table1)
SELECT * FROM summary JOIN table2 ON summary.id = table2.id
`;
expect(parseRefsFromSqlExpression(query)).toEqual(['table1', 'table2']);
});
it('should handle CTE names case-insensitively', () => {
const query = 'WITH MyCte AS (SELECT * FROM table1) SELECT * FROM mycte';
expect(parseRefsFromSqlExpression(query)).toEqual(['table1']);
});
it('should handle RECURSIVE CTEs', () => {
const query = `
WITH RECURSIVE cte AS (
SELECT * FROM table1
UNION ALL
SELECT * FROM cte WHERE depth < 10
)
SELECT * FROM cte
`;
expect(parseRefsFromSqlExpression(query)).toEqual(['table1']);
});
it('should handle queries without CTEs normally', () => {
const query = 'SELECT * FROM table1 JOIN table2 ON table1.id = table2.id';
expect(parseRefsFromSqlExpression(query)).toEqual(['table1', 'table2']);
});
it('should handle CTE that references another CTE', () => {
const query = `
WITH cte1 AS (SELECT * FROM table1),
cte2 AS (SELECT * FROM cte1)
SELECT * FROM cte2
`;
expect(parseRefsFromSqlExpression(query)).toEqual(['table1']);
});
});
});
describe('fingerprints', () => {
@@ -132,10 +132,15 @@ export function parseRefsFromSqlExpression(input: string): string[] {
.replace(/\s+/g, ' ')
// Remove any potential multi line comments
.replace(/\/\*[\s\S]*?\*\//g, '');
// Extract CTE names to exclude them from table references
const cteNames = parseCteNames(query);
const tableMatches = [];
// Extract tables after FROM - case insensitive with /i flag
const fromRegex = /from\s+([^;]*?)(?:\s+(?:join|where|group|having|order|limit)|\s*$)/gi;
// Terminate on: SQL keywords, closing paren (for CTEs/subqueries), or end of string
const fromRegex = /from\s+([^;)]*?)(?:\s+(?:join|where|group|having|order|limit|on|select)|\)|$)/gi;
for (const match of query.matchAll(fromRegex)) {
const fromClause = match[1].trim();
@@ -153,13 +158,44 @@ export function parseRefsFromSqlExpression(input: string): string[] {
tableMatches.push(cleanTableName(match[1]));
}
return compact(uniq(tableMatches));
// Filter out CTE names - they're local definitions, not external references
const externalRefs = tableMatches.filter((table) => !cteNames.has(table.toLowerCase()));
return compact(uniq(externalRefs));
}
/**
* Parse CTE (Common Table Expression) names from a SQL query.
* CTEs are defined with: WITH cte_name AS (...), another_cte AS (...)
*/
function parseCteNames(query: string): Set<string> {
const cteNames = new Set<string>();
// Match the WITH clause - handles both regular and RECURSIVE CTEs
const withMatch = query.match(/^\s*with\s+(?:recursive\s+)?(.*?)(?:\s+select\s)/i);
if (!withMatch) {
return cteNames;
}
const withClause = withMatch[1];
// Match CTE names - they appear before "AS" keyword followed by opening paren
// This handles: cte_name AS (, "quoted_name" AS (
const cteNameRegex = /([a-zA-Z0-9_]+|"[^"]+"|'[^']+')\s+as\s*\(/gi;
for (const match of withClause.matchAll(cteNameRegex)) {
const cteName = match[1].replace(/['"]/g, '').toLowerCase();
cteNames.add(cteName);
}
return cteNames;
}
// Helper function to clean table names
function cleanTableName(tableName: string): string {
// Remove quotes
let name = tableName.replace(/['"]/g, '');
// Remove quotes and parentheses
let name = tableName.replace(/['"()]/g, '');
// Remove alias if present (both "AS alias" and "alias" forms)
if (name.includes(' as ')) {