Skip to content
Open
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
4 changes: 2 additions & 2 deletions src/ch-parser/pluginMacros.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ export const pluginMacros: PluginMacro[] = [
name: '$__adHocFilters',
isFunction: true,
documentation:
'Manually applies ad-hoc filters to a specific table. Useful for complex queries where automatic filter detection fails. Use in SETTINGS clause to specify the target table for ad-hoc filters',
example: "additional_table_filters={'table_name': 'column = \\'value\\' AND column2 = \\'value2\\'}",
'Manually applies ad-hoc filters to specific table(s). Useful for complex queries where automatic filter detection fails. Supports multiple tables by passing comma-separated table names. Use in SETTINGS clause to specify the target table(s) for ad-hoc filters',
example: "additional_table_filters={'table1': 'column = \\'value\\'', 'table2': 'column = \\'value\\''} (for multiple tables)",
},
];
43 changes: 43 additions & 0 deletions src/data/CHDatasource.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,49 @@ describe('ClickHouseDatasource', () => {
"SELECT * FROM complex_table settings additional_table_filters={'my_table': ' key = \\'val\\' '}"
);
});

it('should expand $__adHocFilters macro with multiple tables', async () => {
const query = {
rawSql: "SELECT * FROM complex_table settings $__adHocFilters('table1', 'table2')",
editorType: EditorType.SQL,
} as CHQuery;

const adHocFilters = [
{ key: 'key', operator: '=', value: 'val' },
{ key: 'keyNum', operator: '=', value: '123' },
];

const spyOnReplace = jest.spyOn(templateSrvMock, 'replace').mockImplementation((x) => x);
const spyOnGetVars = jest.spyOn(templateSrvMock, 'getVariables').mockImplementation(() => []);

const result = createInstance({}).applyTemplateVariables(query, {}, adHocFilters);

expect(spyOnReplace).toHaveBeenCalled();
expect(spyOnGetVars).toHaveBeenCalled();
expect(result.rawSql).toEqual(
"SELECT * FROM complex_table settings additional_table_filters={'table1': ' key = \\'val\\' AND keyNum = \\'123\\' ', 'table2': ' key = \\'val\\' AND keyNum = \\'123\\' '}"
);
});

it('should expand $__adHocFilters macro with multiple tables using double quotes', async () => {
const query = {
rawSql: 'SELECT * FROM complex_table settings $__adHocFilters("table1", "table2", "table3")',
editorType: EditorType.SQL,
} as CHQuery;

const adHocFilters = [{ key: 'key', operator: '=', value: 'val' }];

const spyOnReplace = jest.spyOn(templateSrvMock, 'replace').mockImplementation((x) => x);
const spyOnGetVars = jest.spyOn(templateSrvMock, 'getVariables').mockImplementation(() => []);

const result = createInstance({}).applyTemplateVariables(query, {}, adHocFilters);

expect(spyOnReplace).toHaveBeenCalled();
expect(spyOnGetVars).toHaveBeenCalled();
expect(result.rawSql).toEqual(
"SELECT * FROM complex_table settings additional_table_filters={'table1': ' key = \\'val\\' ', 'table2': ' key = \\'val\\' ', 'table3': ' key = \\'val\\' '}"
);
});
});

describe('Tag Keys', () => {
Expand Down
24 changes: 20 additions & 4 deletions src/data/CHDatasource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -306,15 +306,31 @@ export class Datasource
return rawQuery;
}

// Match $__adHocFilters('table_name') or $__adHocFilters("table_name")
const regex = /\$__adHocFilters\s*\(\s*['"](.+?)['"]\s*\)/g;
// Match $__adHocFilters('table_name') or $__adHocFilters("table_name") or multiple tables
const regex = /\$__adHocFilters\s*\(([^)]+)\)/g;

return rawQuery.replace(regex, (match, args) => {
// Extract all table names from comma-separated quoted strings
const tableNameRegex = /['"]([^'"]+)['"]/g;
const tableNames: string[] = [];
let tableMatch;

while ((tableMatch = tableNameRegex.exec(args)) !== null) {
tableNames.push(tableMatch[1]);
}

if (tableNames.length === 0) {
return match; // Return original if no valid table names found
}

return rawQuery.replace(regex, (match, tableName) => {
const filterStr = this.adHocFilter.buildFilterString(filters, useJSON);
if (filterStr === '') {
return 'additional_table_filters={}';
}
return `additional_table_filters={'${tableName}': '${filterStr}'}`;

// Build filter entries for all tables
const tableFilters = tableNames.map(tableName => `'${tableName}': '${filterStr}'`).join(', ');
return `additional_table_filters={${tableFilters}}`;
});
}

Expand Down
Loading