"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.renderFilterRuleSqlFromField = exports.renderFilterRuleSql = exports.renderTableCalculationFilterRuleSql = exports.renderBooleanFilterSql = exports.renderTimestampFilterSql = exports.renderDateFilterSql = exports.renderNumberFilterSql = exports.renderStringFilterSql = exports.castDateLiteral = exports.createBoundaryDateFormatter = exports.DEFAULT_FILTER_CASE_SENSITIVE = void 0;
const tslib_1 = require("tslib");
const moment_timezone_1 = tslib_1.__importDefault(require("moment-timezone"));
const dbt_1 = require("../types/dbt");
const errors_1 = require("../types/errors");
const field_1 = require("../types/field");
const filter_1 = require("../types/filter");
const assertUnreachable_1 = tslib_1.__importDefault(require("../utils/assertUnreachable"));
const booleanConverter_1 = require("../utils/booleanConverter");
const formatting_1 = require("../utils/formatting");
const item_1 = require("../utils/item");
const time_1 = require("../utils/time");
const timeFrames_1 = require("../utils/timeFrames");
exports.DEFAULT_FILTER_CASE_SENSITIVE = true;
/**
 * Formats computed Date boundaries for relative date operators (IN_THE_PAST, etc.)
 * by converting UTC back to the project timezone before formatting as YYYY-MM-DD.
 * Used only when timezone-aware DATE_TRUNC is enabled.
 */
const createBoundaryDateFormatter = (timezone) => (date) => (0, moment_timezone_1.default)(date).utc().tz(timezone).format('YYYY-MM-DD');
exports.createBoundaryDateFormatter = createBoundaryDateFormatter;
/**
 * Returns the default week start day for a given warehouse adapter.
 * This ensures JavaScript-side week boundary calculations match the warehouse.
 *
 * References:
 * - PostgreSQL: https://www.postgresql.org/docs/current/functions-datetime.html (ISO 8601 weeks start on Monday)
 * - Snowflake: https://docs.snowflake.com/en/sql-reference/functions-date-time (WEEK_START=0 defaults to Monday)
 * - Redshift: https://docs.aws.amazon.com/redshift/latest/dg/r_DATE_TRUNC.html (truncates week to Monday)
 * - Databricks: https://docs.databricks.com/aws/en/sql/language-manual/functions/date_trunc (WEEK truncates to Monday)
 * - Trino: https://trino.io/docs/current/functions/datetime.html (ISO 8601 weeks start on Monday)
 * - BigQuery: https://docs.cloud.google.com/bigquery/docs/reference/standard-sql/date_functions (WEEK is equivalent to WEEK(SUNDAY))
 * - ClickHouse: https://clickhouse.com/docs/sql-reference/functions/date-time-functions (toStartOfWeek default mode=0 is Sunday)
 */
const getDefaultStartOfWeek = (adapterType) => {
    switch (adapterType) {
        case dbt_1.SupportedDbtAdapter.BIGQUERY:
        case dbt_1.SupportedDbtAdapter.CLICKHOUSE:
            return timeFrames_1.WeekDay.SUNDAY;
        default:
            return timeFrames_1.WeekDay.MONDAY;
    }
};
// NOTE: This function requires a complete date as input.
// The Z format token appends the UTC offset (e.g. +00:00), ensuring the
// warehouse interprets the literal as UTC regardless of session timezone
// (which may be set via dataTimezone).
const formatTimestampAsUTC = (date) => (0, moment_timezone_1.default)(date).utc().format('YYYY-MM-DD HH:mm:ssZ');
// ClickHouse's date_time_input_format may be set to 'basic', which cannot
// parse timezone offsets like +00:00. BigQuery's DATETIME type also rejects
// timezone offsets. Both already interpret bare strings correctly as UTC.
const formatTimestampAsUTCNoOffset = (date) => (0, moment_timezone_1.default)(date).utc().format('YYYY-MM-DD HH:mm:ss');
/**
 * Cast a date/timestamp string to warehouse-specific SQL literal.
 * Trino/Athena require explicit timestamp casting; others work with bare strings.
 */
const castDateLiteral = (dateString, adapterType) => {
    switch (adapterType) {
        case dbt_1.SupportedDbtAdapter.TRINO:
        case dbt_1.SupportedDbtAdapter.ATHENA:
            return `CAST('${dateString}' AS timestamp)`;
        default:
            return `'${dateString}'`;
    }
};
exports.castDateLiteral = castDateLiteral;
const raiseInvalidFilterError = (type, filter) => {
    let targetString = '';
    if ((0, filter_1.isMetricFilterTarget)(filter.target)) {
        targetString = ` on "${filter.target.fieldRef}" field`;
    }
    else if ((0, filter_1.isFilterTarget)(filter.target)) {
        targetString = ` on "${filter.target.fieldId}" field`;
    }
    throw new errors_1.CompileError(`No function has been implemented to render SQL for the filter operator "${filter.operator}"${targetString} of type "${type}"`);
};
const renderStringFilterSql = (dimensionSql, filter, stringQuoteChar, caseSensitive = exports.DEFAULT_FILTER_CASE_SENSITIVE) => {
    const nonEmptyFilterValues = filter.values?.filter((v) => v !== '');
    // Apply UPPER() to both dimension and values when case insensitive (caseSensitive = false)
    const wrapDimension = (sql) => !caseSensitive ? `UPPER(${sql})` : sql;
    const wrapValue = (value) => !caseSensitive ? String(value).toUpperCase() : value;
    switch (filter.operator) {
        case filter_1.FilterOperator.EQUALS: {
            const inClause = filter.values && filter.values.length > 0
                ? `(${wrapDimension(dimensionSql)}) IN (${filter.values
                    .map((v) => `${stringQuoteChar}${wrapValue(v)}${stringQuoteChar}`)
                    .join(',')})`
                : undefined;
            if (filter.includeNull) {
                const nullClause = `(${dimensionSql}) IS NULL`;
                return inClause ? `(${inClause} OR ${nullClause})` : nullClause;
            }
            return inClause ?? 'true';
        }
        case filter_1.FilterOperator.NOT_EQUALS:
            return filter.values && filter.values.length > 0
                ? `((${wrapDimension(dimensionSql)}) NOT IN (${filter.values
                    .map((v) => `${stringQuoteChar}${wrapValue(v)}${stringQuoteChar}`)
                    .join(',')}) OR (${dimensionSql}) IS NULL)`
                : 'true';
        case filter_1.FilterOperator.INCLUDE:
            if (nonEmptyFilterValues && nonEmptyFilterValues.length > 0) {
                const includesQuery = nonEmptyFilterValues.map((v) => !caseSensitive
                    ? `UPPER(${dimensionSql}) LIKE UPPER('%${v}%')`
                    : `(${dimensionSql}) LIKE '%${v}%'`);
                if (includesQuery.length > 1)
                    return `(${includesQuery.join('\n  OR\n  ')})`;
                return includesQuery.join('\n  OR\n  ');
            }
            return 'true';
        case filter_1.FilterOperator.NOT_INCLUDE:
            if (nonEmptyFilterValues && nonEmptyFilterValues.length > 0) {
                const notIncludeQuery = nonEmptyFilterValues.map((v) => !caseSensitive
                    ? `UPPER(${dimensionSql}) NOT LIKE UPPER('%${v}%')`
                    : `(${dimensionSql}) NOT LIKE '%${v}%'`);
                return `(${notIncludeQuery.join('\n  AND\n  ')} OR (${dimensionSql}) IS NULL)`;
            }
            return 'true';
        case filter_1.FilterOperator.NULL:
            return `(${dimensionSql}) IS NULL`;
        case filter_1.FilterOperator.NOT_NULL:
            return `(${dimensionSql}) IS NOT NULL`;
        case filter_1.FilterOperator.STARTS_WITH:
            const startWithQuery = nonEmptyFilterValues?.map((v) => !caseSensitive
                ? `UPPER(${dimensionSql}) LIKE ${stringQuoteChar}${wrapValue(v)}%${stringQuoteChar}`
                : `(${dimensionSql}) LIKE ${stringQuoteChar}${v}%${stringQuoteChar}`);
            return startWithQuery?.join('\n  OR\n  ') || 'true';
        case filter_1.FilterOperator.ENDS_WITH:
            const endsWithQuery = nonEmptyFilterValues?.map((v) => !caseSensitive
                ? `UPPER(${dimensionSql}) LIKE ${stringQuoteChar}%${wrapValue(v)}${stringQuoteChar}`
                : `(${dimensionSql}) LIKE ${stringQuoteChar}%${v}${stringQuoteChar}`);
            return endsWithQuery?.join('\n  OR\n  ') || 'true';
        default:
            return raiseInvalidFilterError('string', filter);
    }
};
exports.renderStringFilterSql = renderStringFilterSql;
// Validate that all values are valid numbers
const validateAndSanitizeNumber = (value) => {
    const num = Number(value);
    if (Number.isNaN(num) || !Number.isFinite(num)) {
        throw new errors_1.CompileError(`Invalid number value in filter: "${value}". Expected a valid number.`);
    }
    return num;
};
const isValidNumberFilterValue = (values) => !!values && values.length > 0;
const renderNumberFilterSql = (dimensionSql, filter) => {
    switch (filter.operator) {
        case filter_1.FilterOperator.EQUALS:
            return isValidNumberFilterValue(filter.values)
                ? `(${dimensionSql}) IN (${filter.values
                    .map(validateAndSanitizeNumber)
                    .join(',')})`
                : 'true';
        case filter_1.FilterOperator.NOT_EQUALS:
            return !filter.values || filter.values.length === 0
                ? 'true'
                : `((${dimensionSql}) NOT IN (${filter.values
                    .map(validateAndSanitizeNumber)
                    .join(',')}) OR (${dimensionSql}) IS NULL)`;
        case filter_1.FilterOperator.NULL:
            return `(${dimensionSql}) IS NULL`;
        case filter_1.FilterOperator.NOT_NULL:
            return `(${dimensionSql}) IS NOT NULL`;
        case filter_1.FilterOperator.GREATER_THAN:
            return isValidNumberFilterValue(filter.values)
                ? `(${dimensionSql}) > (${validateAndSanitizeNumber(filter.values[0])})`
                : 'true';
        case filter_1.FilterOperator.GREATER_THAN_OR_EQUAL:
            return isValidNumberFilterValue(filter.values)
                ? `(${dimensionSql}) >= (${validateAndSanitizeNumber(filter.values[0])})`
                : 'true';
        case filter_1.FilterOperator.LESS_THAN:
            return isValidNumberFilterValue(filter.values)
                ? `(${dimensionSql}) < (${validateAndSanitizeNumber(filter.values[0])})`
                : 'true';
        case filter_1.FilterOperator.LESS_THAN_OR_EQUAL:
            return isValidNumberFilterValue(filter.values)
                ? `(${dimensionSql}) <= (${validateAndSanitizeNumber(filter.values[0])})`
                : 'true';
        case filter_1.FilterOperator.IN_BETWEEN:
            return !isValidNumberFilterValue(filter.values) ||
                filter.values.length < 2
                ? 'true'
                : `(${dimensionSql}) >= (${validateAndSanitizeNumber(filter.values[0])}) AND (${dimensionSql}) <= (${validateAndSanitizeNumber(filter.values[1])})`;
        case filter_1.FilterOperator.NOT_IN_BETWEEN:
            return !isValidNumberFilterValue(filter.values) ||
                filter.values.length < 2
                ? 'true'
                : `(${dimensionSql}) < (${validateAndSanitizeNumber(filter.values[0])}) OR (${dimensionSql}) > (${validateAndSanitizeNumber(filter.values[1])})`;
        default:
            return raiseInvalidFilterError('number', filter);
    }
};
exports.renderNumberFilterSql = renderNumberFilterSql;
/**
 * Shared filter SQL for date and timestamp dimensions.
 * literalFormatter handles user-provided values; boundaryFormatter handles computed boundaries.
 */
const renderDateOrTimestampFilterSql = ({ dimensionSql, filter, adapterType, timezone, literalFormatter, boundaryFormatter, startOfWeek, baseDimensionSql, useTimezoneAwareDateTrunc, sourceTimezone, }) => {
    // When startOfWeek is not explicitly configured, use the warehouse's default
    // to ensure JS-side week boundaries match the warehouse's DATE_TRUNC behavior.
    const effectiveStartOfWeek = startOfWeek ?? getDefaultStartOfWeek(adapterType);
    // Mirror the dim-side `resolveTimezoneWrap` short-circuit: when target tz
    // equals source tz, dropping the literal's TIMESTAMP cast keeps both sides
    // symmetric (a custom dim SQL emitting DATETIME can otherwise mismatch a
    // TIMESTAMP boundary on BigQuery).
    const wrapTimezoneLiteral = !!useTimezoneAwareDateTrunc &&
        !(0, timeFrames_1.isTimezoneRoundTripNoOp)(adapterType, timezone, sourceTimezone);
    const castValue = (value) => {
        if (wrapTimezoneLiteral) {
            // Column is a timestamptz (round-trip through project TZ). Tag
            // the literal in project TZ so coercion aligns with the column.
            const { toUTC } = timeFrames_1.dateTruncTimezoneConversions[adapterType];
            const naive = (() => {
                switch (adapterType) {
                    case dbt_1.SupportedDbtAdapter.TRINO:
                    case dbt_1.SupportedDbtAdapter.ATHENA:
                        return `CAST('${value}' AS timestamp)`;
                    case dbt_1.SupportedDbtAdapter.SNOWFLAKE:
                        return `'${value}'::timestamp_ntz`;
                    case dbt_1.SupportedDbtAdapter.DATABRICKS:
                        return `'${value}'`;
                    case dbt_1.SupportedDbtAdapter.BIGQUERY:
                        // No `::` cast; TIMESTAMP(s, tz) yields the UTC instant.
                        return `TIMESTAMP('${value}', '${timezone}')`;
                    case dbt_1.SupportedDbtAdapter.CLICKHOUSE:
                        return `toDateTime('${value}', '${timezone}')`;
                    default:
                        return `'${value}'::timestamp`;
                }
            })();
            return toUTC(naive, timezone);
        }
        switch (adapterType) {
            case dbt_1.SupportedDbtAdapter.TRINO:
            case dbt_1.SupportedDbtAdapter.ATHENA: {
                return `CAST('${value}' AS timestamp)`;
            }
            default:
                return `('${value}')`;
        }
    };
    const settings = (0, filter_1.isDateFilterRule)(filter) ? filter.settings : undefined;
    // Multi-value equals/notEquals match any value, like string/number filters
    const castValues = (values) => values.map((value) => castValue(literalFormatter(value))).join(',');
    switch (filter.operator) {
        case filter_1.FilterOperator.EQUALS:
            return filter.values && filter.values.length > 1
                ? `(${dimensionSql}) IN (${castValues(filter.values)})`
                : `(${dimensionSql}) = ${castValue(literalFormatter(filter.values?.[0]))}`;
        case filter_1.FilterOperator.NOT_EQUALS:
            return filter.values && filter.values.length > 1
                ? `((${dimensionSql}) NOT IN (${castValues(filter.values)}) OR (${dimensionSql}) IS NULL)`
                : `((${dimensionSql}) != ${castValue(literalFormatter(filter.values?.[0]))} OR (${dimensionSql}) IS NULL)`;
        case filter_1.FilterOperator.NULL:
            return `(${dimensionSql}) IS NULL`;
        case filter_1.FilterOperator.NOT_NULL:
            return `(${dimensionSql}) IS NOT NULL`;
        case filter_1.FilterOperator.GREATER_THAN:
            return `(${dimensionSql}) > ${castValue(literalFormatter(filter.values?.[0]))}`;
        case filter_1.FilterOperator.GREATER_THAN_OR_EQUAL:
            return `(${dimensionSql}) >= ${castValue(literalFormatter(filter.values?.[0]))}`;
        case filter_1.FilterOperator.LESS_THAN:
            return `(${dimensionSql}) < ${castValue(literalFormatter(filter.values?.[0]))}`;
        case filter_1.FilterOperator.LESS_THAN_OR_EQUAL:
            return `(${dimensionSql}) <= ${castValue(literalFormatter(filter.values?.[0]))}`;
        case filter_1.FilterOperator.NOT_IN_THE_PAST:
        case filter_1.FilterOperator.IN_THE_PAST: {
            const unitOfTime = settings?.unitOfTime || filter_1.UnitOfTime.days;
            const completed = !!settings?.completed;
            const not = filter.operator === filter_1.FilterOperator.NOT_IN_THE_PAST
                ? 'NOT '
                : '';
            if (completed) {
                const completedDate = (0, time_1.getMomentDateWithCustomStartOfWeek)(effectiveStartOfWeek)
                    .tz(timezone)
                    .startOf(unitOfTime)
                    .utc()
                    .toDate();
                const untilDate = boundaryFormatter((0, time_1.getMomentDateWithCustomStartOfWeek)(effectiveStartOfWeek)
                    .tz(timezone)
                    .startOf(unitOfTime)
                    .utc()
                    .toDate());
                return `${not}((${dimensionSql}) >= ${castValue(boundaryFormatter((0, time_1.getMomentDateWithCustomStartOfWeek)(effectiveStartOfWeek, completedDate)
                    .tz(timezone)
                    .subtract(filter.values?.[0], unitOfTime)
                    .utc()
                    .toDate()))} AND (${dimensionSql}) < ${castValue(untilDate)})`;
            }
            const untilDate = boundaryFormatter((0, time_1.getMomentDateWithCustomStartOfWeek)(effectiveStartOfWeek)
                .tz(timezone)
                .utc()
                .toDate());
            return `${not}((${dimensionSql}) >= ${castValue(boundaryFormatter((0, time_1.getMomentDateWithCustomStartOfWeek)(effectiveStartOfWeek)
                .tz(timezone)
                .subtract(filter.values?.[0], unitOfTime)
                .utc()
                .toDate()))} AND (${dimensionSql}) <= ${castValue(untilDate)})`;
        }
        case filter_1.FilterOperator.IN_THE_NEXT: {
            const unitOfTime = settings?.unitOfTime || filter_1.UnitOfTime.days;
            const completed = !!settings?.completed;
            if (completed) {
                const fromDate = (0, time_1.getMomentDateWithCustomStartOfWeek)(effectiveStartOfWeek)
                    .tz(timezone)
                    .add(1, unitOfTime)
                    .startOf(unitOfTime)
                    .utc()
                    .toDate();
                const toDate = boundaryFormatter((0, time_1.getMomentDateWithCustomStartOfWeek)(effectiveStartOfWeek, fromDate)
                    .tz(timezone)
                    .add(filter.values?.[0], unitOfTime)
                    .utc()
                    .toDate());
                return `((${dimensionSql}) >= ${castValue(boundaryFormatter(fromDate))} AND (${dimensionSql}) < ${castValue(toDate)})`;
            }
            const fromDate = boundaryFormatter((0, time_1.getMomentDateWithCustomStartOfWeek)(effectiveStartOfWeek)
                .tz(timezone)
                .utc()
                .toDate());
            const toDate = boundaryFormatter((0, time_1.getMomentDateWithCustomStartOfWeek)(effectiveStartOfWeek)
                .tz(timezone)
                .add(filter.values?.[0], unitOfTime)
                .utc()
                .toDate());
            return `((${dimensionSql}) >= ${castValue(fromDate)} AND (${dimensionSql}) <= ${castValue(toDate)})`;
        }
        case filter_1.FilterOperator.IN_THE_CURRENT: {
            const unitOfTime = settings?.unitOfTime || filter_1.UnitOfTime.days;
            const fromDate = boundaryFormatter((0, time_1.getMomentDateWithCustomStartOfWeek)(effectiveStartOfWeek)
                .tz(timezone)
                .startOf(unitOfTime)
                .utc()
                .toDate());
            const untilDate = boundaryFormatter((0, time_1.getMomentDateWithCustomStartOfWeek)(effectiveStartOfWeek)
                .tz(timezone)
                .endOf(unitOfTime)
                .utc()
                .toDate());
            const castedFromDate = castValue(fromDate);
            const castedUntilDate = castValue(untilDate);
            return `((${dimensionSql}) >= ${castedFromDate} AND (${dimensionSql}) <= ${castedUntilDate})`;
        }
        case filter_1.FilterOperator.NOT_IN_THE_CURRENT: {
            const unitOfTime = settings?.unitOfTime || filter_1.UnitOfTime.days;
            const fromDate = boundaryFormatter((0, time_1.getMomentDateWithCustomStartOfWeek)(effectiveStartOfWeek)
                .tz(timezone)
                .startOf(unitOfTime)
                .utc()
                .toDate());
            const untilDate = boundaryFormatter((0, time_1.getMomentDateWithCustomStartOfWeek)(effectiveStartOfWeek)
                .tz(timezone)
                .endOf(unitOfTime)
                .utc()
                .toDate());
            const castedFromDate = castValue(fromDate);
            const castedUntilDate = castValue(untilDate);
            return `(NOT ((${dimensionSql}) >= ${castedFromDate} AND (${dimensionSql}) <= ${castedUntilDate}))`;
        }
        case filter_1.FilterOperator.IN_PERIOD_TO_DATE: {
            const today = (0, time_1.getMomentDateWithCustomStartOfWeek)(effectiveStartOfWeek).tz(timezone);
            const periodUnit = settings?.unitOfTime;
            // Use the raw base dimension SQL for date extraction when available
            // (e.g., when filtering on a DATE_TRUNC'd dimension like order_date_year)
            const extractSql = baseDimensionSql || dimensionSql;
            switch (periodUnit) {
                case filter_1.UnitOfTime.years: {
                    const dayOfYear = today.dayOfYear(); // 1-366
                    switch (adapterType) {
                        case dbt_1.SupportedDbtAdapter.BIGQUERY:
                            return `(EXTRACT(DAYOFYEAR FROM ${extractSql}) <= ${dayOfYear})`;
                        case dbt_1.SupportedDbtAdapter.CLICKHOUSE:
                            return `(toDayOfYear(${extractSql}) <= ${dayOfYear})`;
                        default:
                            return `(EXTRACT(DOY FROM ${extractSql}) <= ${dayOfYear})`;
                    }
                }
                case filter_1.UnitOfTime.months: {
                    const dayOfMonth = today.date(); // 1-31
                    switch (adapterType) {
                        case dbt_1.SupportedDbtAdapter.CLICKHOUSE:
                            return `(toDayOfMonth(${extractSql}) <= ${dayOfMonth})`;
                        default:
                            return `(EXTRACT(DAY FROM ${extractSql}) <= ${dayOfMonth})`;
                    }
                }
                case filter_1.UnitOfTime.quarters: {
                    const quarterStart = today.clone().startOf('quarter');
                    const dayInQuarter = today.diff(quarterStart, 'days');
                    switch (adapterType) {
                        case dbt_1.SupportedDbtAdapter.BIGQUERY:
                            return `(DATE_DIFF(${extractSql}, DATE_TRUNC(${extractSql}, QUARTER), DAY) <= ${dayInQuarter})`;
                        case dbt_1.SupportedDbtAdapter.CLICKHOUSE:
                            return `(dateDiff('day', toStartOfQuarter(${extractSql}), ${extractSql}) <= ${dayInQuarter})`;
                        case dbt_1.SupportedDbtAdapter.TRINO:
                        case dbt_1.SupportedDbtAdapter.ATHENA:
                            return `(DATE_DIFF('day', DATE_TRUNC('quarter', ${extractSql}), ${extractSql}) <= ${dayInQuarter})`;
                        default:
                            return `(EXTRACT(DAY FROM ${extractSql} - DATE_TRUNC('QUARTER', ${extractSql})) <= ${dayInQuarter})`;
                    }
                }
                case filter_1.UnitOfTime.weeks: {
                    const weekStart = today.clone().startOf('week');
                    const dayInWeek = today.diff(weekStart, 'days');
                    switch (adapterType) {
                        case dbt_1.SupportedDbtAdapter.BIGQUERY:
                            return `(DATE_DIFF(${extractSql}, DATE_TRUNC(${extractSql}, WEEK(${effectiveStartOfWeek === timeFrames_1.WeekDay.SUNDAY ? 'SUNDAY' : 'MONDAY'})), DAY) <= ${dayInWeek})`;
                        case dbt_1.SupportedDbtAdapter.CLICKHOUSE:
                            return `(dateDiff('day', toStartOfWeek(${extractSql}, ${effectiveStartOfWeek === timeFrames_1.WeekDay.SUNDAY ? '0' : '1'}), ${extractSql}) <= ${dayInWeek})`;
                        case dbt_1.SupportedDbtAdapter.TRINO:
                        case dbt_1.SupportedDbtAdapter.ATHENA:
                            return `(DATE_DIFF('day', DATE_TRUNC('week', ${extractSql}), ${extractSql}) <= ${dayInWeek})`;
                        default:
                            return `(EXTRACT(DAY FROM ${extractSql} - DATE_TRUNC('WEEK', ${extractSql})) <= ${dayInWeek})`;
                    }
                }
                default:
                    throw new errors_1.CompileError(`Period to date filter requires a unitOfTime setting (weeks, months, quarters, or years)`);
            }
        }
        case filter_1.FilterOperator.IN_BETWEEN: {
            const startDate = literalFormatter(filter.values?.[0]);
            const endDate = literalFormatter(filter.values?.[1]);
            return `((${dimensionSql}) >= ${castValue(startDate)} AND (${dimensionSql}) <= ${castValue(endDate)})`;
        }
        default:
            return raiseInvalidFilterError('date', filter);
    }
};
/**
 * Renders filter SQL for DATE-type dimensions.
 *
 * When no explicit boundaryDateFormatter is provided, both boundary
 * computation and formatting default to UTC.  This matches the warehouse's
 * UTC-based DATE_TRUNC and removes any dependency on the server's local
 * timezone.  Callers that enable timezone-aware DATE_TRUNC should pass
 * createBoundaryDateFormatter(timezone) so both sides use the project
 * timezone instead.
 */
const renderDateFilterSql = ({ dimensionSql, filter, adapterType, timezone, boundaryDateFormatter, startOfWeek, baseDimensionSql, useTimezoneAwareDateTrunc, sourceTimezone, }) => {
    const effectiveTimezone = boundaryDateFormatter ? timezone : 'UTC';
    const effectiveFormatter = boundaryDateFormatter ?? (0, exports.createBoundaryDateFormatter)('UTC');
    return renderDateOrTimestampFilterSql({
        dimensionSql,
        filter,
        adapterType,
        timezone: effectiveTimezone,
        literalFormatter: formatting_1.formatDate,
        boundaryFormatter: effectiveFormatter,
        startOfWeek,
        baseDimensionSql,
        useTimezoneAwareDateTrunc,
        sourceTimezone,
    });
};
exports.renderDateFilterSql = renderDateFilterSql;
/** Renders filter SQL for TIMESTAMP-type dimensions. Both literals and boundaries use the same UTC formatter. */
const renderTimestampFilterSql = ({ dimensionSql, filter, adapterType, timezone, timestampFormatter, startOfWeek, baseDimensionSql, useTimezoneAwareDateTrunc, sourceTimezone, }) => renderDateOrTimestampFilterSql({
    dimensionSql,
    filter,
    adapterType,
    timezone,
    literalFormatter: timestampFormatter,
    boundaryFormatter: timestampFormatter,
    startOfWeek,
    baseDimensionSql,
    useTimezoneAwareDateTrunc,
    sourceTimezone,
});
exports.renderTimestampFilterSql = renderTimestampFilterSql;
const renderBooleanFilterSql = (dimensionSql, filter) => {
    switch (filter.operator) {
        case 'equals':
            return `(${dimensionSql}) = ${(0, booleanConverter_1.convertToBooleanValue)(filter.values?.[0])}`;
        case 'notEquals':
            return `((${dimensionSql}) != ${(0, booleanConverter_1.convertToBooleanValue)(filter.values?.[0])} OR (${dimensionSql}) IS NULL)`;
        case 'isNull':
            return `(${dimensionSql}) IS NULL`;
        case 'notNull':
            return `(${dimensionSql}) IS NOT NULL`;
        default:
            return raiseInvalidFilterError('boolean', filter);
    }
};
exports.renderBooleanFilterSql = renderBooleanFilterSql;
const escapeStringValuesOnFilterRule = (filterRule, escapeString) => ({
    ...filterRule,
    values: filterRule.values?.map((v) => typeof v === 'string'
        ? escapeString(v) // escape the string quote char
        : v),
});
const renderTableCalculationFilterRuleSql = (filterRule, field, fieldQuoteChar, stringQuoteChar, escapeString, adapterType, startOfWeek, timezone = 'UTC') => {
    if (!field)
        return '1=1';
    const fieldSql = `${fieldQuoteChar}${(0, item_1.getItemId)(field)}${fieldQuoteChar}`;
    const escapedFilterRule = escapeStringValuesOnFilterRule(filterRule, escapeString);
    // First we default to field.type
    // otherwise, we check the custom format for backwards compatibility
    switch (field.type) {
        case field_1.TableCalculationType.STRING:
            return (0, exports.renderStringFilterSql)(fieldSql, escapedFilterRule, stringQuoteChar, true);
        case field_1.TableCalculationType.DATE:
        case field_1.TableCalculationType.TIMESTAMP:
            return (0, exports.renderDateFilterSql)({
                dimensionSql: fieldSql,
                filter: escapedFilterRule,
                adapterType,
                timezone,
                startOfWeek,
            });
        case field_1.TableCalculationType.NUMBER:
            return (0, exports.renderNumberFilterSql)(fieldSql, escapedFilterRule);
        case field_1.TableCalculationType.BOOLEAN:
            return (0, exports.renderBooleanFilterSql)(fieldSql, escapedFilterRule);
        default:
        // Do nothing here. This will try with format.type for backwards compatibility
    }
    switch (field.format?.type) {
        case field_1.CustomFormatType.PERCENT:
        case field_1.CustomFormatType.CURRENCY:
        case field_1.CustomFormatType.NUMBER: {
            return (0, exports.renderNumberFilterSql)(fieldSql, escapedFilterRule);
        }
        default:
            return (0, exports.renderStringFilterSql)(fieldSql, escapedFilterRule, stringQuoteChar, true);
    }
};
exports.renderTableCalculationFilterRuleSql = renderTableCalculationFilterRuleSql;
const renderFilterRuleSql = (filterRule, fieldType, fieldSql, stringQuoteChar, escapeString, startOfWeek, adapterType, timezone = 'UTC', caseSensitive = exports.DEFAULT_FILTER_CASE_SENSITIVE, baseDimensionSql, useTimezoneAwareDateTrunc, baseTimeIntervalDimensionType, sourceTimezone) => {
    if (filterRule.disabled) {
        return `1=1`; // When filter is disabled, we want to return all rows
    }
    const escapedFilterRule = escapeStringValuesOnFilterRule(filterRule, escapeString);
    switch (fieldType) {
        case field_1.DimensionType.STRING:
        case field_1.MetricType.STRING: {
            return (0, exports.renderStringFilterSql)(fieldSql, escapedFilterRule, stringQuoteChar, caseSensitive);
        }
        case field_1.DimensionType.NUMBER:
        case field_1.MetricType.NUMBER:
        case field_1.MetricType.PERCENTILE:
        case field_1.MetricType.MEDIAN:
        case field_1.MetricType.AVERAGE:
        case field_1.MetricType.COUNT:
        case field_1.MetricType.COUNT_DISTINCT:
        case field_1.MetricType.SUM:
        case field_1.MetricType.SUM_DISTINCT:
        case field_1.MetricType.AVERAGE_DISTINCT:
        case field_1.MetricType.MIN:
        case field_1.MetricType.MAX:
        case field_1.MetricType.PERCENT_OF_PREVIOUS:
        case field_1.MetricType.PERCENT_OF_TOTAL:
        case field_1.MetricType.RUNNING_TOTAL: {
            return (0, exports.renderNumberFilterSql)(fieldSql, escapedFilterRule);
        }
        case field_1.DimensionType.DATE:
        case field_1.MetricType.DATE: {
            return (0, exports.renderDateFilterSql)({
                dimensionSql: fieldSql,
                filter: escapedFilterRule,
                adapterType,
                timezone,
                boundaryDateFormatter: useTimezoneAwareDateTrunc
                    ? (0, exports.createBoundaryDateFormatter)(timezone)
                    : undefined,
                startOfWeek,
                baseDimensionSql,
                // GLITCH-452: day-or-coarser dims now compile to a real DATE
                // (CAST(... AS DATE)), so the literal stays a bare date —
                // wrapping it as a timestamptz would re-introduce the tz drift
                // the cast removes (and break BigQuery's DATE vs TIMESTAMP check).
                useTimezoneAwareDateTrunc: false,
                sourceTimezone,
            });
        }
        case field_1.DimensionType.TIMESTAMP:
        case field_1.MetricType.TIMESTAMP: {
            return (0, exports.renderTimestampFilterSql)({
                dimensionSql: fieldSql,
                filter: escapedFilterRule,
                adapterType,
                timezone,
                timestampFormatter: adapterType === dbt_1.SupportedDbtAdapter.CLICKHOUSE ||
                    adapterType === dbt_1.SupportedDbtAdapter.BIGQUERY
                    ? formatTimestampAsUTCNoOffset
                    : formatTimestampAsUTC,
                startOfWeek,
                baseDimensionSql,
            });
        }
        case field_1.DimensionType.BOOLEAN:
        case field_1.MetricType.BOOLEAN: {
            return (0, exports.renderBooleanFilterSql)(fieldSql, escapedFilterRule);
        }
        default: {
            return (0, assertUnreachable_1.default)(fieldType, `No function implemented to render sql for filter group type ${fieldType}`);
        }
    }
};
exports.renderFilterRuleSql = renderFilterRuleSql;
// To be used for filters with a field that is a dimension or metric
const renderFilterRuleSqlFromField = (filterRule, field, fieldQuoteChar, stringQuoteChar, escapeString, startOfWeek, adapterType, timezone = 'UTC', exploreCaseSensitive = exports.DEFAULT_FILTER_CASE_SENSITIVE, baseDimensionSql, useTimezoneAwareDateTrunc, sourceTimezone) => {
    const fieldType = (0, field_1.isCompiledCustomSqlDimension)(field)
        ? field.dimensionType
        : field.type;
    const fieldSql = (0, field_1.isMetric)(field)
        ? `${fieldQuoteChar}${(0, item_1.getItemId)(field)}${fieldQuoteChar}`
        : field.compiledSql;
    // Determine if this filter should be case sensitive
    // Priority: filter-rule-level override > field-level setting > explore-level setting > default
    let caseSensitive;
    if (filterRule.caseSensitive !== undefined) {
        caseSensitive = filterRule.caseSensitive;
    }
    else if ((0, field_1.isMetric)(field)) {
        caseSensitive = exports.DEFAULT_FILTER_CASE_SENSITIVE;
    }
    else if ('caseSensitive' in field && field.caseSensitive !== undefined) {
        caseSensitive = field.caseSensitive;
    }
    else {
        caseSensitive = exploreCaseSensitive;
    }
    const baseTimeIntervalDimensionType = !(0, field_1.isCompiledCustomSqlDimension)(field) && !(0, field_1.isMetric)(field)
        ? field.timeIntervalBaseDimensionType
        : undefined;
    return (0, exports.renderFilterRuleSql)(filterRule, fieldType, fieldSql, stringQuoteChar, escapeString, startOfWeek, adapterType, timezone, caseSensitive, baseDimensionSql, useTimezoneAwareDateTrunc, baseTimeIntervalDimensionType, sourceTimezone);
};
exports.renderFilterRuleSqlFromField = renderFilterRuleSqlFromField;
//# sourceMappingURL=filtersCompiler.js.map