mirror of
https://github.com/apache/superset.git
synced 2026-04-08 10:55:20 +00:00
1094 lines
29 KiB
TypeScript
1094 lines
29 KiB
TypeScript
/**
|
|
* Licensed to the Apache Software Foundation (ASF) under one
|
|
* or more contributor license agreements. See the NOTICE file
|
|
* distributed with this work for additional information
|
|
* regarding copyright ownership. The ASF licenses this file
|
|
* to you under the Apache License, Version 2.0 (the
|
|
* "License"); you may not use this file except in compliance
|
|
* with the License. You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing,
|
|
* software distributed under the License is distributed on an
|
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
* KIND, either express or implied. See the License for the
|
|
* specific language governing permissions and limitations
|
|
* under the License.
|
|
*/
|
|
import { AdhocColumn, QueryMode, VizType } from '@superset-ui/core';
|
|
import buildQuery from '../src/buildQuery';
|
|
import { TableChartFormData } from '../src/types';
|
|
|
|
const basicFormData: TableChartFormData = {
|
|
viz_type: VizType.Table,
|
|
datasource: '11__table',
|
|
query_mode: QueryMode.Aggregate,
|
|
groupby: ['state'],
|
|
metrics: ['count'],
|
|
};
|
|
|
|
const createAdhocColumn = (
|
|
sqlExpression: string,
|
|
label: string,
|
|
): AdhocColumn => ({
|
|
sqlExpression,
|
|
label,
|
|
expressionType: 'SQL',
|
|
});
|
|
|
|
describe('plugin-chart-ag-grid-table', () => {
|
|
describe('buildQuery - sort mapping for server pagination', () => {
|
|
test('should map string column colId to backend identifier', () => {
|
|
const query = buildQuery(
|
|
{
|
|
...basicFormData,
|
|
server_pagination: true,
|
|
},
|
|
{
|
|
ownState: {
|
|
sortBy: [{ key: 'state', desc: false }],
|
|
},
|
|
},
|
|
).queries[0];
|
|
|
|
expect(query.orderby).toEqual([['state', true]]);
|
|
});
|
|
|
|
test('should map AdhocColumn colId by sqlExpression', () => {
|
|
const adhocColumn = createAdhocColumn('degree_type', 'Highest Degree');
|
|
|
|
const query = buildQuery(
|
|
{
|
|
...basicFormData,
|
|
server_pagination: true,
|
|
groupby: [adhocColumn],
|
|
},
|
|
{
|
|
ownState: {
|
|
sortBy: [{ key: 'degree_type', desc: false }],
|
|
},
|
|
},
|
|
).queries[0];
|
|
|
|
expect(query.orderby).toEqual([['degree_type', true]]);
|
|
});
|
|
|
|
test('should map AdhocColumn colId by label', () => {
|
|
const adhocColumn = createAdhocColumn('degree_type', 'Highest Degree');
|
|
|
|
const query = buildQuery(
|
|
{
|
|
...basicFormData,
|
|
server_pagination: true,
|
|
groupby: [adhocColumn],
|
|
},
|
|
{
|
|
ownState: {
|
|
sortBy: [{ key: 'Highest Degree', desc: false }],
|
|
},
|
|
},
|
|
).queries[0];
|
|
|
|
expect(query.orderby).toEqual([['degree_type', true]]);
|
|
});
|
|
|
|
test('should map string metric colId to backend identifier', () => {
|
|
const query = buildQuery(
|
|
{
|
|
...basicFormData,
|
|
server_pagination: true,
|
|
metrics: ['SUM(revenue)'],
|
|
},
|
|
{
|
|
ownState: {
|
|
sortBy: [{ key: 'SUM(revenue)', desc: true }],
|
|
},
|
|
},
|
|
).queries[0];
|
|
|
|
expect(query.orderby).toEqual([['SUM(revenue)', false]]);
|
|
});
|
|
|
|
test('should map percent metric with % prefix', () => {
|
|
const query = buildQuery(
|
|
{
|
|
...basicFormData,
|
|
server_pagination: true,
|
|
metrics: ['revenue'],
|
|
percent_metrics: ['revenue'],
|
|
},
|
|
{
|
|
ownState: {
|
|
sortBy: [{ key: '%revenue', desc: false }],
|
|
},
|
|
},
|
|
).queries[0];
|
|
|
|
expect(query.orderby).toEqual([['revenue', true]]);
|
|
});
|
|
|
|
test('should handle desc sort direction correctly', () => {
|
|
const query = buildQuery(
|
|
{
|
|
...basicFormData,
|
|
server_pagination: true,
|
|
},
|
|
{
|
|
ownState: {
|
|
sortBy: [{ key: 'state', desc: true }],
|
|
},
|
|
},
|
|
).queries[0];
|
|
|
|
expect(query.orderby).toEqual([['state', false]]);
|
|
});
|
|
});
|
|
|
|
describe('buildQuery - CSV export with sortModel', () => {
|
|
test('should use sortModel for download queries', () => {
|
|
const query = buildQuery(
|
|
{
|
|
...basicFormData,
|
|
result_format: 'csv',
|
|
},
|
|
{
|
|
ownState: {
|
|
sortModel: [{ colId: 'state', sort: 'asc' }],
|
|
sortBy: [{ key: 'other', desc: false }],
|
|
},
|
|
},
|
|
).queries[0];
|
|
|
|
expect(query.orderby).toEqual([
|
|
['state', true],
|
|
['count', false],
|
|
]);
|
|
});
|
|
|
|
test('should map sortModel with desc direction', () => {
|
|
const query = buildQuery(
|
|
{
|
|
...basicFormData,
|
|
result_format: 'csv',
|
|
},
|
|
{
|
|
ownState: {
|
|
sortModel: [{ colId: 'state', sort: 'desc' }],
|
|
},
|
|
},
|
|
).queries[0];
|
|
|
|
expect(query.orderby?.[0]).toEqual(['state', false]);
|
|
});
|
|
|
|
test('should handle multi-column sort from sortModel', () => {
|
|
const query = buildQuery(
|
|
{
|
|
...basicFormData,
|
|
groupby: ['state', 'city'],
|
|
result_format: 'csv',
|
|
},
|
|
{
|
|
ownState: {
|
|
sortModel: [
|
|
{ colId: 'state', sort: 'asc', sortIndex: 0 },
|
|
{ colId: 'city', sort: 'desc', sortIndex: 1 },
|
|
],
|
|
},
|
|
},
|
|
).queries[0];
|
|
|
|
expect(query.orderby).toEqual([
|
|
['state', true],
|
|
['city', false],
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('buildQuery - stable sort tie-breaker', () => {
|
|
test('should add default orderby as tie-breaker for single-column CSV export', () => {
|
|
const query = buildQuery(
|
|
{
|
|
...basicFormData,
|
|
result_format: 'csv',
|
|
metrics: ['count'],
|
|
},
|
|
{
|
|
ownState: {
|
|
sortModel: [{ colId: 'state', sort: 'asc' }],
|
|
},
|
|
},
|
|
).queries[0];
|
|
|
|
expect(query.orderby).toEqual([
|
|
['state', true],
|
|
['count', false],
|
|
]);
|
|
});
|
|
|
|
test('should not add tie-breaker if primary sort matches default orderby', () => {
|
|
const query = buildQuery(
|
|
{
|
|
...basicFormData,
|
|
result_format: 'csv',
|
|
metrics: ['count'],
|
|
},
|
|
{
|
|
ownState: {
|
|
sortModel: [{ colId: 'count', sort: 'desc' }],
|
|
},
|
|
},
|
|
).queries[0];
|
|
|
|
expect(query.orderby).toEqual([['count', false]]);
|
|
});
|
|
|
|
test('should not add tie-breaker for multi-column sorts', () => {
|
|
const query = buildQuery(
|
|
{
|
|
...basicFormData,
|
|
groupby: ['state', 'city'],
|
|
result_format: 'csv',
|
|
},
|
|
{
|
|
ownState: {
|
|
sortModel: [
|
|
{ colId: 'state', sort: 'asc' },
|
|
{ colId: 'city', sort: 'desc' },
|
|
],
|
|
},
|
|
},
|
|
).queries[0];
|
|
|
|
expect(query.orderby).toEqual([
|
|
['state', true],
|
|
['city', false],
|
|
]);
|
|
});
|
|
|
|
test('should not add tie-breaker for non-download queries with server pagination', () => {
|
|
const query = buildQuery(
|
|
{
|
|
...basicFormData,
|
|
server_pagination: true,
|
|
},
|
|
{
|
|
ownState: {
|
|
sortBy: [{ key: 'state', desc: false }],
|
|
},
|
|
},
|
|
).queries[0];
|
|
|
|
expect(query.orderby).toEqual([['state', true]]);
|
|
});
|
|
});
|
|
|
|
describe('buildQuery - filter handling for CSV export', () => {
|
|
test('should apply AG Grid filters for download queries', () => {
|
|
const query = buildQuery(
|
|
{
|
|
...basicFormData,
|
|
result_format: 'csv',
|
|
},
|
|
{
|
|
ownState: {
|
|
filters: [
|
|
{
|
|
col: 'state',
|
|
op: 'IN',
|
|
val: ['CA', 'NY'],
|
|
},
|
|
],
|
|
},
|
|
},
|
|
).queries[0];
|
|
|
|
expect(query.filters).toContainEqual({
|
|
col: 'state',
|
|
op: 'IN',
|
|
val: ['CA', 'NY'],
|
|
});
|
|
});
|
|
|
|
test('should append AG Grid filters to existing filters', () => {
|
|
const query = buildQuery(
|
|
{
|
|
...basicFormData,
|
|
result_format: 'csv',
|
|
adhoc_filters: [
|
|
{
|
|
expressionType: 'SIMPLE',
|
|
subject: 'country',
|
|
operator: '==',
|
|
comparator: 'USA',
|
|
clause: 'WHERE',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
ownState: {
|
|
filters: [
|
|
{
|
|
col: 'state',
|
|
op: 'IN',
|
|
val: ['CA', 'NY'],
|
|
},
|
|
],
|
|
},
|
|
},
|
|
).queries[0];
|
|
|
|
expect(query.filters?.length).toBeGreaterThan(1);
|
|
expect(query.filters).toContainEqual({
|
|
col: 'state',
|
|
op: 'IN',
|
|
val: ['CA', 'NY'],
|
|
});
|
|
});
|
|
|
|
test('should not apply filters for non-download queries', () => {
|
|
const query = buildQuery(basicFormData, {
|
|
ownState: {
|
|
filters: [
|
|
{
|
|
col: 'state',
|
|
op: 'IN',
|
|
val: ['CA', 'NY'],
|
|
},
|
|
],
|
|
},
|
|
}).queries[0];
|
|
|
|
expect(query.filters).not.toContainEqual({
|
|
col: 'state',
|
|
op: 'IN',
|
|
val: ['CA', 'NY'],
|
|
});
|
|
});
|
|
|
|
test('should handle empty filters array', () => {
|
|
const query = buildQuery(
|
|
{
|
|
...basicFormData,
|
|
result_format: 'csv',
|
|
},
|
|
{
|
|
ownState: {
|
|
filters: [],
|
|
},
|
|
},
|
|
).queries[0];
|
|
|
|
expect(query.filters).toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe('buildQuery - column reordering for CSV export', () => {
|
|
test('should reorder columns based on columnOrder', () => {
|
|
const query = buildQuery(
|
|
{
|
|
...basicFormData,
|
|
groupby: ['state', 'city', 'country'],
|
|
result_format: 'csv',
|
|
},
|
|
{
|
|
ownState: {
|
|
columnOrder: ['city', 'country', 'state', 'count'],
|
|
},
|
|
},
|
|
).queries[0];
|
|
|
|
expect(query.columns).toEqual(['city', 'country', 'state']);
|
|
});
|
|
|
|
test('should reorder metrics based on columnOrder', () => {
|
|
const query = buildQuery(
|
|
{
|
|
...basicFormData,
|
|
metrics: ['count', 'revenue', 'profit'],
|
|
result_format: 'csv',
|
|
},
|
|
{
|
|
ownState: {
|
|
columnOrder: ['state', 'profit', 'count', 'revenue'],
|
|
},
|
|
},
|
|
).queries[0];
|
|
|
|
expect(query.metrics).toEqual(['profit', 'count', 'revenue']);
|
|
});
|
|
|
|
test('should preserve unmatched columns at the end', () => {
|
|
const query = buildQuery(
|
|
{
|
|
...basicFormData,
|
|
groupby: ['state', 'city', 'country'],
|
|
result_format: 'csv',
|
|
},
|
|
{
|
|
ownState: {
|
|
columnOrder: ['city'],
|
|
},
|
|
},
|
|
).queries[0];
|
|
|
|
expect(query.columns?.[0]).toEqual('city');
|
|
expect(query.columns).toContain('state');
|
|
expect(query.columns).toContain('country');
|
|
});
|
|
|
|
test('should match AdhocColumn by sqlExpression in columnOrder', () => {
|
|
const adhocColumn = createAdhocColumn('degree_type', 'Highest Degree');
|
|
|
|
const query = buildQuery(
|
|
{
|
|
...basicFormData,
|
|
groupby: ['state', adhocColumn],
|
|
result_format: 'csv',
|
|
},
|
|
{
|
|
ownState: {
|
|
columnOrder: ['degree_type', 'state'],
|
|
},
|
|
},
|
|
).queries[0];
|
|
|
|
expect(query.columns?.[0]).toMatchObject({
|
|
sqlExpression: 'degree_type',
|
|
});
|
|
});
|
|
|
|
test('should not reorder for non-download queries', () => {
|
|
const query = buildQuery(
|
|
{
|
|
...basicFormData,
|
|
groupby: ['state', 'city'],
|
|
},
|
|
{
|
|
ownState: {
|
|
columnOrder: ['city', 'state'],
|
|
},
|
|
},
|
|
).queries[0];
|
|
|
|
expect(query.columns).toEqual(['state', 'city']);
|
|
});
|
|
});
|
|
|
|
describe('buildQuery - AG Grid server-side filters', () => {
|
|
describe('Simple filters', () => {
|
|
test('should apply agGridSimpleFilters to query.filters', () => {
|
|
const query = buildQuery(
|
|
{
|
|
...basicFormData,
|
|
server_pagination: true,
|
|
},
|
|
{
|
|
ownState: {
|
|
agGridSimpleFilters: [
|
|
{ col: 'state', op: '==', val: 'CA' },
|
|
{ col: 'city', op: 'ILIKE', val: '%San%' },
|
|
],
|
|
},
|
|
},
|
|
).queries[0];
|
|
|
|
expect(query.filters).toContainEqual({
|
|
col: 'state',
|
|
op: '==',
|
|
val: 'CA',
|
|
});
|
|
expect(query.filters).toContainEqual({
|
|
col: 'city',
|
|
op: 'ILIKE',
|
|
val: '%San%',
|
|
});
|
|
});
|
|
|
|
test('should append simple filters to existing filters', () => {
|
|
const query = buildQuery(
|
|
{
|
|
...basicFormData,
|
|
server_pagination: true,
|
|
adhoc_filters: [
|
|
{
|
|
expressionType: 'SIMPLE',
|
|
subject: 'country',
|
|
operator: '==',
|
|
comparator: 'USA',
|
|
clause: 'WHERE',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
ownState: {
|
|
agGridSimpleFilters: [{ col: 'state', op: '==', val: 'CA' }],
|
|
},
|
|
},
|
|
).queries[0];
|
|
|
|
expect(query.filters?.length).toBeGreaterThan(1);
|
|
expect(query.filters).toContainEqual({
|
|
col: 'state',
|
|
op: '==',
|
|
val: 'CA',
|
|
});
|
|
});
|
|
|
|
test('should handle empty agGridSimpleFilters array', () => {
|
|
const query = buildQuery(
|
|
{
|
|
...basicFormData,
|
|
server_pagination: true,
|
|
},
|
|
{
|
|
ownState: {
|
|
agGridSimpleFilters: [],
|
|
},
|
|
},
|
|
).queries[0];
|
|
|
|
expect(query.filters).toBeDefined();
|
|
});
|
|
|
|
test('should not apply simple filters when server pagination is disabled', () => {
|
|
const query = buildQuery(basicFormData, {
|
|
ownState: {
|
|
agGridSimpleFilters: [{ col: 'state', op: '==', val: 'CA' }],
|
|
},
|
|
}).queries[0];
|
|
|
|
expect(query.filters).not.toContainEqual({
|
|
col: 'state',
|
|
op: '==',
|
|
val: 'CA',
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Complex WHERE clause', () => {
|
|
test('should apply agGridComplexWhere to query.extras.where', () => {
|
|
const query = buildQuery(
|
|
{
|
|
...basicFormData,
|
|
server_pagination: true,
|
|
},
|
|
{
|
|
ownState: {
|
|
agGridComplexWhere: '(age > 18 AND age < 65)',
|
|
},
|
|
},
|
|
).queries[0];
|
|
|
|
expect(query.extras?.where).toBe('(age > 18 AND age < 65)');
|
|
});
|
|
|
|
test('should combine with existing WHERE clause using AND', () => {
|
|
const query = buildQuery(
|
|
{
|
|
...basicFormData,
|
|
server_pagination: true,
|
|
adhoc_filters: [
|
|
{
|
|
expressionType: 'SQL',
|
|
clause: 'WHERE',
|
|
sqlExpression: 'country = "USA"',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
ownState: {
|
|
agGridComplexWhere: '(age > 18 AND age < 65)',
|
|
},
|
|
},
|
|
).queries[0];
|
|
|
|
expect(query.extras?.where).toContain('country = "USA"');
|
|
expect(query.extras?.where).toContain('(age > 18 AND age < 65)');
|
|
expect(query.extras?.where).toContain(' AND ');
|
|
});
|
|
|
|
test('should handle empty agGridComplexWhere', () => {
|
|
const query = buildQuery(
|
|
{
|
|
...basicFormData,
|
|
server_pagination: true,
|
|
},
|
|
{
|
|
ownState: {
|
|
agGridComplexWhere: '',
|
|
},
|
|
},
|
|
).queries[0];
|
|
|
|
// Empty string should not set extras.where (undefined or empty string both acceptable)
|
|
expect(query.extras?.where || undefined).toBeUndefined();
|
|
});
|
|
|
|
test('should not apply WHERE clause when server pagination is disabled', () => {
|
|
const query = buildQuery(basicFormData, {
|
|
ownState: {
|
|
agGridComplexWhere: '(age > 18)',
|
|
},
|
|
}).queries[0];
|
|
|
|
// When server_pagination is disabled, AG Grid filters should not be applied
|
|
expect(query.extras?.where || undefined).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('HAVING clause', () => {
|
|
test('should apply agGridHavingClause to query.extras.having', () => {
|
|
const query = buildQuery(
|
|
{
|
|
...basicFormData,
|
|
server_pagination: true,
|
|
metrics: ['SUM(revenue)'],
|
|
},
|
|
{
|
|
ownState: {
|
|
agGridHavingClause: 'SUM(revenue) > 1000',
|
|
},
|
|
},
|
|
).queries[0];
|
|
|
|
expect(query.extras?.having).toBe('SUM(revenue) > 1000');
|
|
});
|
|
|
|
test('should combine with existing HAVING clause using AND', () => {
|
|
const query = buildQuery(
|
|
{
|
|
...basicFormData,
|
|
server_pagination: true,
|
|
metrics: ['SUM(revenue)', 'COUNT(*)'],
|
|
adhoc_filters: [
|
|
{
|
|
expressionType: 'SQL',
|
|
clause: 'HAVING',
|
|
sqlExpression: 'COUNT(*) > 10',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
ownState: {
|
|
agGridHavingClause: 'SUM(revenue) > 1000',
|
|
},
|
|
},
|
|
).queries[0];
|
|
|
|
expect(query.extras?.having).toContain('COUNT(*) > 10');
|
|
expect(query.extras?.having).toContain('SUM(revenue) > 1000');
|
|
expect(query.extras?.having).toContain(' AND ');
|
|
});
|
|
|
|
test('should handle metric filters correctly', () => {
|
|
const query = buildQuery(
|
|
{
|
|
...basicFormData,
|
|
server_pagination: true,
|
|
metrics: ['AVG(score)', 'MAX(points)'],
|
|
},
|
|
{
|
|
ownState: {
|
|
agGridHavingClause: '(AVG(score) >= 90 AND MAX(points) < 100)',
|
|
},
|
|
},
|
|
).queries[0];
|
|
|
|
expect(query.extras?.having).toBe(
|
|
'(AVG(score) >= 90 AND MAX(points) < 100)',
|
|
);
|
|
});
|
|
|
|
test('should not apply HAVING clause when server pagination is disabled', () => {
|
|
const query = buildQuery(basicFormData, {
|
|
ownState: {
|
|
agGridHavingClause: 'SUM(revenue) > 1000',
|
|
},
|
|
}).queries[0];
|
|
|
|
// When server_pagination is disabled, AG Grid filters should not be applied
|
|
expect(query.extras?.having || undefined).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('Totals query handling', () => {
|
|
test('should exclude AG Grid WHERE filters from totals query', () => {
|
|
const { queries } = buildQuery(
|
|
{
|
|
...basicFormData,
|
|
server_pagination: true,
|
|
show_totals: true,
|
|
query_mode: QueryMode.Aggregate,
|
|
},
|
|
{
|
|
ownState: {
|
|
agGridComplexWhere: 'age > 18',
|
|
},
|
|
},
|
|
);
|
|
|
|
const mainQuery = queries[0];
|
|
const totalsQuery = queries[2]; // queries[1] is rowcount, queries[2] is totals
|
|
|
|
expect(mainQuery.extras?.where).toBe('age > 18');
|
|
expect(totalsQuery.extras?.where).toBeUndefined();
|
|
});
|
|
|
|
test('should preserve non-AG Grid WHERE clauses in totals', () => {
|
|
const { queries } = buildQuery(
|
|
{
|
|
...basicFormData,
|
|
server_pagination: true,
|
|
show_totals: true,
|
|
query_mode: QueryMode.Aggregate,
|
|
adhoc_filters: [
|
|
{
|
|
expressionType: 'SQL',
|
|
clause: 'WHERE',
|
|
sqlExpression: 'country = "USA"',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
ownState: {
|
|
agGridComplexWhere: 'age > 18',
|
|
},
|
|
},
|
|
);
|
|
|
|
const mainQuery = queries[0];
|
|
const totalsQuery = queries[2]; // queries[1] is rowcount, queries[2] is totals
|
|
|
|
expect(mainQuery.extras?.where).toContain('country = "USA"');
|
|
expect(mainQuery.extras?.where).toContain('age > 18');
|
|
expect(totalsQuery.extras?.where).toContain('country = "USA"');
|
|
expect(totalsQuery.extras?.where).not.toContain('age > 18');
|
|
});
|
|
|
|
test('should handle totals when AG Grid WHERE is only clause', () => {
|
|
const { queries } = buildQuery(
|
|
{
|
|
...basicFormData,
|
|
server_pagination: true,
|
|
show_totals: true,
|
|
query_mode: QueryMode.Aggregate,
|
|
},
|
|
{
|
|
ownState: {
|
|
agGridComplexWhere: 'status = "active"',
|
|
},
|
|
},
|
|
);
|
|
|
|
const totalsQuery = queries[2]; // queries[1] is rowcount, queries[2] is totals
|
|
|
|
expect(totalsQuery.extras?.where).toBeUndefined();
|
|
});
|
|
|
|
test('should handle totals with empty WHERE clause after removal', () => {
|
|
const { queries } = buildQuery(
|
|
{
|
|
...basicFormData,
|
|
server_pagination: true,
|
|
show_totals: true,
|
|
query_mode: QueryMode.Aggregate,
|
|
adhoc_filters: [
|
|
{
|
|
expressionType: 'SQL',
|
|
clause: 'WHERE',
|
|
sqlExpression: 'country = "USA"',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
ownState: {
|
|
agGridComplexWhere: 'country = "USA"',
|
|
},
|
|
},
|
|
);
|
|
|
|
const totalsQuery = queries[2]; // queries[1] is rowcount, queries[2] is totals
|
|
|
|
// After removing AG Grid WHERE, totals should still have the adhoc filter
|
|
expect(totalsQuery.extras).toBeDefined();
|
|
});
|
|
|
|
test('should not modify totals query when no AG Grid filters applied', () => {
|
|
const { queries } = buildQuery(
|
|
{
|
|
...basicFormData,
|
|
server_pagination: true,
|
|
show_totals: true,
|
|
query_mode: QueryMode.Aggregate,
|
|
},
|
|
{
|
|
ownState: {},
|
|
},
|
|
);
|
|
|
|
const totalsQuery = queries[2]; // queries[1] is rowcount, queries[2] is totals
|
|
|
|
expect(totalsQuery.columns).toEqual([]);
|
|
expect(totalsQuery.row_limit).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('Integration - all filter types together', () => {
|
|
test('should apply simple, WHERE, and HAVING filters simultaneously', () => {
|
|
const query = buildQuery(
|
|
{
|
|
...basicFormData,
|
|
server_pagination: true,
|
|
metrics: ['SUM(revenue)', 'COUNT(*)'],
|
|
},
|
|
{
|
|
ownState: {
|
|
agGridSimpleFilters: [{ col: 'state', op: '==', val: 'CA' }],
|
|
agGridComplexWhere: '(age > 18 AND age < 65)',
|
|
agGridHavingClause: 'SUM(revenue) > 1000',
|
|
},
|
|
},
|
|
).queries[0];
|
|
|
|
expect(query.filters).toContainEqual({
|
|
col: 'state',
|
|
op: '==',
|
|
val: 'CA',
|
|
});
|
|
expect(query.extras?.where).toBe('(age > 18 AND age < 65)');
|
|
expect(query.extras?.having).toBe('SUM(revenue) > 1000');
|
|
});
|
|
|
|
test('should combine AG Grid filters with adhoc filters', () => {
|
|
const query = buildQuery(
|
|
{
|
|
...basicFormData,
|
|
server_pagination: true,
|
|
adhoc_filters: [
|
|
{
|
|
expressionType: 'SIMPLE',
|
|
subject: 'country',
|
|
operator: '==',
|
|
comparator: 'USA',
|
|
clause: 'WHERE',
|
|
},
|
|
{
|
|
expressionType: 'SQL',
|
|
clause: 'WHERE',
|
|
sqlExpression: 'region = "West"',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
ownState: {
|
|
agGridSimpleFilters: [
|
|
{ col: 'state', op: 'IN', val: ['CA', 'OR', 'WA'] },
|
|
],
|
|
agGridComplexWhere: "status = 'active'",
|
|
},
|
|
},
|
|
).queries[0];
|
|
|
|
expect(query.filters).toContainEqual({
|
|
col: 'state',
|
|
op: 'IN',
|
|
val: ['CA', 'OR', 'WA'],
|
|
});
|
|
expect(query.extras?.where).toContain('region = "West"');
|
|
expect(query.extras?.where).toContain("status = 'active'");
|
|
});
|
|
|
|
test('should reset currentPage to 0 when filtering', () => {
|
|
const query = buildQuery(
|
|
{
|
|
...basicFormData,
|
|
server_pagination: true,
|
|
},
|
|
{
|
|
ownState: {
|
|
currentPage: 5,
|
|
agGridSimpleFilters: [{ col: 'state', op: '==', val: 'CA' }],
|
|
},
|
|
},
|
|
).queries[0];
|
|
|
|
// The query itself doesn't have page info, but ownState should be updated
|
|
expect(query.filters).toContainEqual({
|
|
col: 'state',
|
|
op: '==',
|
|
val: 'CA',
|
|
});
|
|
});
|
|
|
|
test('should include filter metadata in ownState', () => {
|
|
const query = buildQuery(
|
|
{
|
|
...basicFormData,
|
|
server_pagination: true,
|
|
},
|
|
{
|
|
ownState: {
|
|
agGridFilterModel: {
|
|
state: { filterType: 'text', type: 'equals', filter: 'CA' },
|
|
},
|
|
lastFilteredColumn: 'state',
|
|
lastFilteredInputPosition: 'first',
|
|
},
|
|
},
|
|
).queries[0];
|
|
|
|
// Query should be generated with the filter model metadata
|
|
expect(query).toBeDefined();
|
|
});
|
|
|
|
test('should handle complex real-world scenario', () => {
|
|
const { queries } = buildQuery(
|
|
{
|
|
...basicFormData,
|
|
server_pagination: true,
|
|
show_totals: true,
|
|
query_mode: QueryMode.Aggregate,
|
|
groupby: ['state', 'city'],
|
|
metrics: ['SUM(revenue)', 'AVG(score)', 'COUNT(*)'],
|
|
adhoc_filters: [
|
|
{
|
|
expressionType: 'SIMPLE',
|
|
subject: 'country',
|
|
operator: '==',
|
|
comparator: 'USA',
|
|
clause: 'WHERE',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
ownState: {
|
|
agGridSimpleFilters: [
|
|
{ col: 'state', op: 'IN', val: ['CA', 'NY', 'TX'] },
|
|
],
|
|
agGridComplexWhere:
|
|
'(population > 100000 AND growth_rate > 0.05)',
|
|
agGridHavingClause:
|
|
'(SUM(revenue) > 1000000 AND AVG(score) >= 4.5)',
|
|
currentPage: 0,
|
|
pageSize: 50,
|
|
},
|
|
},
|
|
);
|
|
|
|
const mainQuery = queries[0];
|
|
const totalsQuery = queries[2]; // queries[1] is rowcount, queries[2] is totals
|
|
|
|
// Main query should have all filters
|
|
expect(mainQuery.filters).toContainEqual({
|
|
col: 'state',
|
|
op: 'IN',
|
|
val: ['CA', 'NY', 'TX'],
|
|
});
|
|
expect(mainQuery.extras?.where).toContain('population > 100000');
|
|
expect(mainQuery.extras?.having).toContain('SUM(revenue) > 1000000');
|
|
|
|
// Totals query should exclude AG Grid WHERE (since it's the only WHERE clause, it should be undefined)
|
|
expect(totalsQuery.extras?.where).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('Edge cases', () => {
|
|
test('should handle null ownState gracefully', () => {
|
|
const query = buildQuery(
|
|
{
|
|
...basicFormData,
|
|
server_pagination: true,
|
|
},
|
|
{},
|
|
).queries[0];
|
|
|
|
expect(query).toBeDefined();
|
|
});
|
|
|
|
test('should handle ownState without filter properties', () => {
|
|
const query = buildQuery(
|
|
{
|
|
...basicFormData,
|
|
server_pagination: true,
|
|
},
|
|
{
|
|
ownState: {
|
|
currentPage: 0,
|
|
pageSize: 20,
|
|
},
|
|
},
|
|
).queries[0];
|
|
|
|
expect(query).toBeDefined();
|
|
});
|
|
|
|
test('should handle filters with special SQL characters', () => {
|
|
const query = buildQuery(
|
|
{
|
|
...basicFormData,
|
|
server_pagination: true,
|
|
},
|
|
{
|
|
ownState: {
|
|
agGridSimpleFilters: [{ col: 'name', op: '==', val: "O'Brien" }],
|
|
},
|
|
},
|
|
).queries[0];
|
|
|
|
expect(query.filters).toContainEqual({
|
|
col: 'name',
|
|
op: '==',
|
|
val: "O'Brien",
|
|
});
|
|
});
|
|
|
|
test('should handle very long filter clauses', () => {
|
|
const longWhereClause = Array(50)
|
|
.fill(0)
|
|
.map((_, i) => `field${i} > ${i}`)
|
|
.join(' AND ');
|
|
|
|
const query = buildQuery(
|
|
{
|
|
...basicFormData,
|
|
server_pagination: true,
|
|
},
|
|
{
|
|
ownState: {
|
|
agGridComplexWhere: longWhereClause,
|
|
},
|
|
},
|
|
).queries[0];
|
|
|
|
expect(query.extras?.where).toBe(longWhereClause);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('buildQuery - metrics handling in different query modes', () => {
|
|
test('should not include metrics in raw records mode', () => {
|
|
const query = buildQuery({
|
|
viz_type: VizType.Table,
|
|
datasource: '11__table',
|
|
query_mode: QueryMode.Raw,
|
|
all_columns: ['state', 'city'],
|
|
}).queries[0];
|
|
|
|
expect(query.metrics).toBeUndefined();
|
|
});
|
|
|
|
test('should set metrics to empty array in aggregate mode when no metrics specified', () => {
|
|
const query = buildQuery({
|
|
viz_type: VizType.Table,
|
|
datasource: '11__table',
|
|
query_mode: QueryMode.Aggregate,
|
|
groupby: ['state'],
|
|
}).queries[0];
|
|
|
|
expect(query.metrics).toEqual([]);
|
|
});
|
|
});
|
|
});
|