import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { lintAnalyticsEngineSql } from '../supabase/functions/_backend/utils/analyticsEngineSqlLint.ts' import { collectAnalyticsEngineSqlFixtures } from './helpers/collectAnalyticsEngineSqlFixtures.ts' describe('analytics engine sql lint rules', () => { it.concurrent('flags COUNT(*)', () => { expect(lintAnalyticsEngineSql('SELECT COUNT(*) AS total FROM device_info')).toEqual([ expect.objectContaining({ rule: 'no-count-star' }), ]) }) it.concurrent('flags CASE WHEN inside argMax', () => { expect(lintAnalyticsEngineSql( 'SELECT argMax(blob9, CASE WHEN blob9 != \'\' THEN timestamp ELSE toDateTime(\'1970-01-01 00:00:00\') END) FROM device_info', )).toEqual([ expect.objectContaining({ rule: 'no-case-in-argmax' }), ]) }) it.concurrent.each([ ['no-join', 'SELECT 1 FROM bandwidth_usage LEFT JOIN device_usage ON bandwidth_usage.blob1 = device_usage.blob1'], ['no-multiif', 'SELECT multiIf(blob4 != \'\', blob4, 1, \'ios\') FROM device_usage'], ['no-to-string', 'SELECT toString(toDate(timestamp)) AS date FROM app_log'], ['no-concat', 'SELECT concat(index1, \':\', blob1) FROM device_usage'], ])('flags %s', (rule, sql) => { expect(lintAnalyticsEngineSql(sql).map(issue => issue.rule)).toContain(rule) }) it.concurrent('flags ORDER BY on a source column that is only projected under an alias', () => { expect(lintAnalyticsEngineSql( 'SELECT blob2 AS action, timestamp AS created_at FROM app_log ORDER BY created_at ASC, blob2 ASC LIMIT 10', ).map(issue => issue.rule)).toContain('no-order-by-aliased-source-column') expect(lintAnalyticsEngineSql( 'SELECT blob2 AS action, timestamp AS created_at FROM app_log ORDER BY timestamp ASC', ).map(issue => issue.rule)).toContain('no-order-by-aliased-source-column') expect(lintAnalyticsEngineSql( 'SELECT blob2 AS action, timestamp AS created_at FROM app_log ORDER BY created_at ASC, action ASC LIMIT 10', ).map(issue => issue.rule)).not.toContain('no-order-by-aliased-source-column') expect(lintAnalyticsEngineSql( 'SELECT blob2, count() AS total FROM app_log GROUP BY blob2 ORDER BY blob2', ).map(issue => issue.rule)).not.toContain('no-order-by-aliased-source-column') expect(lintAnalyticsEngineSql( 'SELECT blob2 AS action FROM app_log ORDER BY action -- blob2', ).map(issue => issue.rule)).not.toContain('no-order-by-aliased-source-column') expect(lintAnalyticsEngineSql( 'SELECT blob2 AS action FROM app_log ORDER BY action /* blob2 */ LIMIT 1', ).map(issue => issue.rule)).not.toContain('no-order-by-aliased-source-column') expect(lintAnalyticsEngineSql( 'SELECT blob2 AS action FROM app_log WHERE blob3 != \'ORDER BY blob2\' ORDER BY action', ).map(issue => issue.rule)).not.toContain('no-order-by-aliased-source-column') }) it.concurrent('accepts supported COUNT() and COUNT(DISTINCT) forms', () => { expect(lintAnalyticsEngineSql('SELECT COUNT() AS total FROM device_info')).toEqual([]) expect(lintAnalyticsEngineSql('SELECT COUNT(DISTINCT blob1) AS total FROM device_info')).toEqual([]) }) it.concurrent('flags untyped NULL IF() branches', () => { expect(lintAnalyticsEngineSql( 'SELECT min(if(blob2 = \'download_complete\' AND double1 > 0, double1, NULL)) FROM app_log', ).map(issue => issue.rule)).toContain('no-if-untyped-null') expect(lintAnalyticsEngineSql( 'SELECT min(if(blob2 IN (\'download_complete\', \'download_zip_complete\') AND double1 > 0, double1, NULL)) FROM app_log', ).map(issue => issue.rule)).toContain('no-if-untyped-null') expect(lintAnalyticsEngineSql( 'SELECT min(if(blob2 = \'download_complete\', NULL, double1)) FROM app_log', ).map(issue => issue.rule)).toContain('no-if-untyped-null') expect(lintAnalyticsEngineSql( 'SELECT min(if(blob2 = \'download_complete\', double1, null)) FROM app_log', ).map(issue => issue.rule)).toContain('no-if-untyped-null') expect(lintAnalyticsEngineSql( 'SELECT coalesce(double1, NULL) FROM app_log', ).map(issue => issue.rule)).not.toContain('no-if-untyped-null') expect(lintAnalyticsEngineSql( 'SELECT sum(if(blob2 = \'set\', 1, 0)) AS succeeded FROM app_log WHERE blob2 IN (\'set\', NULL)', ).map(issue => issue.rule)).not.toContain('no-if-untyped-null') expect(lintAnalyticsEngineSql( 'SELECT if(blob2 IN (\'set\', NULL), 1, 0) FROM app_log', ).map(issue => issue.rule)).not.toContain('no-if-untyped-null') expect(lintAnalyticsEngineSql( 'SELECT if(1, \'x)\', NULL) FROM app_log', ).map(issue => issue.rule)).toContain('no-if-untyped-null') expect(lintAnalyticsEngineSql( 'SELECT \'if(1, NULL, 2)\' FROM app_log', ).map(issue => issue.rule)).not.toContain('no-if-untyped-null') expect(lintAnalyticsEngineSql( 'SELECT 1 FROM app_log -- if(1, NULL, 0)', ).map(issue => issue.rule)).not.toContain('no-if-untyped-null') expect(lintAnalyticsEngineSql( 'SELECT 1 FROM app_log /* if(1, NULL, 0) */', ).map(issue => issue.rule)).not.toContain('no-if-untyped-null') expect(lintAnalyticsEngineSql( 'SELECT if(1, NULL, 0) FROM app_log -- if(1, NULL, 0)', ).map(issue => issue.rule)).toContain('no-if-untyped-null') expect(lintAnalyticsEngineSql( 'SELECT if(1, NULL /* ) */, 0) FROM app_log', ).map(issue => issue.rule)).toContain('no-if-untyped-null') expect(lintAnalyticsEngineSql( 'SELECT if(1 /* , */, NULL) FROM app_log', ).map(issue => issue.rule)).toContain('no-if-untyped-null') expect(lintAnalyticsEngineSql( 'SELECT if(1, /* NULL */, 0) FROM app_log', ).map(issue => issue.rule)).not.toContain('no-if-untyped-null') expect(lintAnalyticsEngineSql( 'SELECT if(1, NULL -- )\n, 0) FROM app_log', ).map(issue => issue.rule)).toContain('no-if-untyped-null') }) it.concurrent('flags avgIf/sumIf/countIf because they expand to IF(..., NULL)', () => { expect(lintAnalyticsEngineSql( 'SELECT avgIf(double1, blob2 IN (\'download_complete\', \'download_zip_complete\') AND double1 > 0) FROM app_log', ).map(issue => issue.rule)).toContain('no-conditional-agg-if') expect(lintAnalyticsEngineSql( 'SELECT sumIf(double1, double1 > 0) FROM app_log', ).map(issue => issue.rule)).toContain('no-conditional-agg-if') expect(lintAnalyticsEngineSql( 'SELECT countIf(blob2 = \'download_complete\') FROM app_log', ).map(issue => issue.rule)).toContain('no-conditional-agg-if') expect(lintAnalyticsEngineSql( 'SELECT max(if(blob2 = \'download_complete\' AND double1 > 0, double1, 0.0)) FROM app_log', ).map(issue => issue.rule)).not.toContain('no-conditional-agg-if') expect(lintAnalyticsEngineSql( 'SELECT \'avgIf(\' FROM app_log', ).map(issue => issue.rule)).not.toContain('no-conditional-agg-if') expect(lintAnalyticsEngineSql( 'SELECT \'sumIf(\' FROM app_log', ).map(issue => issue.rule)).not.toContain('no-conditional-agg-if') expect(lintAnalyticsEngineSql( 'SELECT \'countIf(\' FROM app_log', ).map(issue => issue.rule)).not.toContain('no-conditional-agg-if') expect(lintAnalyticsEngineSql( 'SELECT 1 FROM app_log -- avgIf(double1, 1)', ).map(issue => issue.rule)).not.toContain('no-conditional-agg-if') expect(lintAnalyticsEngineSql( 'SELECT 1 FROM app_log /* sumIf(double1, 1) */', ).map(issue => issue.rule)).not.toContain('no-conditional-agg-if') expect(lintAnalyticsEngineSql( 'SELECT avgIf(double1, 1) FROM app_log -- countIf(1)', ).map(issue => issue.rule)).toContain('no-conditional-agg-if') expect(lintAnalyticsEngineSql( 'SELECT \'it\\\'s avgIf(\' FROM app_log', ).map(issue => issue.rule)).not.toContain('no-conditional-agg-if') expect(lintAnalyticsEngineSql( 'SELECT \'foo\\\\\', avgIf(double1, 1) FROM app_log', ).map(issue => issue.rule)).toContain('no-conditional-agg-if') }) }) describe('analytics engine sql fixtures', () => { beforeEach(() => { vi.unstubAllGlobals() }) afterEach(() => { vi.unstubAllGlobals() }) it('generates lint-clean queries for every Analytics Engine read path', async () => { const fixtures = await collectAnalyticsEngineSqlFixtures() expect(fixtures.length).toBeGreaterThan(30) const offenders = fixtures.flatMap((fixture) => { const issues = lintAnalyticsEngineSql(fixture.query) return issues.map(issue => ({ fixture: fixture.name, issue })) }) expect(offenders, offenders.map(entry => `${entry.fixture}: ${entry.issue.rule}`).join('\n')).toEqual([]) }) })