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:
@@ -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 ')) {
|
||||
|
||||
Reference in New Issue
Block a user