diff --git a/ee/clickhouse/queries/test/__snapshots__/test_lifecycle.ambr b/ee/clickhouse/queries/test/__snapshots__/test_lifecycle.ambr index 29eb93b4ae929..5acb951590251 100644 --- a/ee/clickhouse/queries/test/__snapshots__/test_lifecycle.ambr +++ b/ee/clickhouse/queries/test/__snapshots__/test_lifecycle.ambr @@ -352,7 +352,7 @@ AND event = '$pageview' AND timestamp >= toDateTime(dateTrunc('day', toDateTime('2021-04-28 00:00:00', 'UTC'))) - INTERVAL 1 day AND timestamp < toDateTime(dateTrunc('day', toDateTime('2021-05-05 23:59:59', 'UTC'))) + INTERVAL 1 day - AND (and(like(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(properties, '$current_url'), ''), 'null'), '^"|"$', ''), '%example%'), 1)) + AND (and(ifNull(like(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(properties, '$current_url'), ''), 'null'), '^"|"$', ''), '%example%'), 0), 1)) GROUP BY pdi.person_id) GROUP BY start_of_period, status) @@ -426,7 +426,7 @@ AND event = '$pageview' AND timestamp >= toDateTime(dateTrunc('day', toDateTime('2021-04-28 00:00:00', 'UTC'))) - INTERVAL 1 day AND timestamp < toDateTime(dateTrunc('day', toDateTime('2021-05-05 23:59:59', 'UTC'))) + INTERVAL 1 day - AND (and(like(nullIf(nullIf(events.`mat_$current_url`, ''), 'null'), '%example%'), 1)) + AND (and(ifNull(like(nullIf(nullIf(events.`mat_$current_url`, ''), 'null'), '%example%'), 0), 1)) GROUP BY pdi.person_id) GROUP BY start_of_period, status) @@ -501,7 +501,7 @@ AND event = '$pageview' AND timestamp >= toDateTime(dateTrunc('day', toDateTime('2021-04-28 00:00:00', 'UTC'))) - INTERVAL 1 day AND timestamp < toDateTime(dateTrunc('day', toDateTime('2021-05-05 23:59:59', 'UTC'))) + INTERVAL 1 day - AND (like(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(person_properties, 'email'), ''), 'null'), '^"|"$', ''), '%test.com')) + AND (ifNull(like(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(person_properties, 'email'), ''), 'null'), '^"|"$', ''), '%test.com'), 0)) GROUP BY pdi.person_id) GROUP BY start_of_period, status) @@ -576,7 +576,7 @@ AND event = '$pageview' AND timestamp >= toDateTime(dateTrunc('day', toDateTime('2021-04-28 00:00:00', 'UTC'))) - INTERVAL 1 day AND timestamp < toDateTime(dateTrunc('day', toDateTime('2021-05-05 23:59:59', 'UTC'))) + INTERVAL 1 day - AND (like(nullIf(nullIf(mat_pp_email, ''), 'null'), '%test.com')) + AND (ifNull(like(nullIf(nullIf(mat_pp_email, ''), 'null'), '%test.com'), 0)) GROUP BY pdi.person_id) GROUP BY start_of_period, status) diff --git a/ee/clickhouse/views/test/__snapshots__/test_clickhouse_experiments.ambr b/ee/clickhouse/views/test/__snapshots__/test_clickhouse_experiments.ambr index d185a7a063790..09cddbe7f6756 100644 --- a/ee/clickhouse/views/test/__snapshots__/test_clickhouse_experiments.ambr +++ b/ee/clickhouse/views/test/__snapshots__/test_clickhouse_experiments.ambr @@ -1613,8 +1613,7 @@ AND toTimeZone(timestamp, 'UTC') >= toDateTime('2020-01-01 00:00:00', 'UTC') AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-06 00:00:00', 'UTC') AND ((has(['control', 'test'], replaceRegexpAll(JSONExtractRaw(e.properties, '$feature/a-b-test'), '^"|"$', ''))) - AND (ifNull(ilike(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(properties, 'hogql'), ''), 'null'), '^"|"$', ''), 'true'), isNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(properties, 'hogql'), ''), 'null'), '^"|"$', '')) - and isNull('true')))) + AND (ifNull(ilike(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(properties, 'hogql'), ''), 'null'), '^"|"$', ''), 'true'), 0))) GROUP BY value ORDER BY count DESC, value DESC LIMIT 25 @@ -1655,8 +1654,7 @@ WHERE e.team_id = 2 AND event = '$pageview' AND ((has(['control', 'test'], replaceRegexpAll(JSONExtractRaw(e.properties, '$feature/a-b-test'), '^"|"$', ''))) - AND (ifNull(ilike(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(properties, 'hogql'), ''), 'null'), '^"|"$', ''), 'true'), isNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(properties, 'hogql'), ''), 'null'), '^"|"$', '')) - and isNull('true')))) + AND (ifNull(ilike(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(properties, 'hogql'), ''), 'null'), '^"|"$', ''), 'true'), 0))) AND toTimeZone(timestamp, 'UTC') >= toDateTime('2020-01-01 00:00:00', 'UTC') AND toTimeZone(timestamp, 'UTC') <= toDateTime('2020-01-06 00:00:00', 'UTC') AND replaceRegexpAll(JSONExtractRaw(properties, '$feature/a-b-test'), '^"|"$', '') in (['test', 'control']) diff --git a/posthog/api/test/__snapshots__/test_insight.ambr b/posthog/api/test/__snapshots__/test_insight.ambr index a5d6fadd37533..0f8b2b5457332 100644 --- a/posthog/api/test/__snapshots__/test_insight.ambr +++ b/posthog/api/test/__snapshots__/test_insight.ambr @@ -170,7 +170,7 @@ AND toTimeZone(timestamp, 'UTC') >= toDateTime('2012-01-08 00:00:00', 'UTC') AND toTimeZone(timestamp, 'UTC') <= toDateTime('2012-01-15 23:59:59', 'UTC') AND ((and(ifNull(less(toInt64OrNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(properties, 'int_value'), ''), 'null'), '^"|"$', '')), 10), 0), 1)) - AND (like(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(person_properties, 'fish'), ''), 'null'), '^"|"$', ''), '%fish%'))) + AND (ifNull(like(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(person_properties, 'fish'), ''), 'null'), '^"|"$', ''), '%fish%'), 0))) AND (step_0 = 1 OR step_1 = 1) )) WHERE step_0 = 1 )) @@ -215,11 +215,11 @@ person.person_props as person_props , if(event = 'user signed up' AND (and(ifNull(less(toInt64OrNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(properties, 'int_value'), ''), 'null'), '^"|"$', '')), 10), 0), 1) - AND like(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(person_properties, 'fish'), ''), 'null'), '^"|"$', ''), '%fish%')), 1, 0) as step_0, + AND ifNull(like(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(person_properties, 'fish'), ''), 'null'), '^"|"$', ''), '%fish%'), 0)), 1, 0) as step_0, if(step_0 = 1, timestamp, null) as latest_0, if(event = 'user did things' AND (and(ifNull(less(toInt64OrNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(properties, 'int_value'), ''), 'null'), '^"|"$', '')), 10), 0), 1) - AND like(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(person_properties, 'fish'), ''), 'null'), '^"|"$', ''), '%fish%')), 1, 0) as step_1, + AND ifNull(like(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(person_properties, 'fish'), ''), 'null'), '^"|"$', ''), '%fish%'), 0)), 1, 0) as step_1, if(step_1 = 1, timestamp, null) as latest_1 FROM events e INNER JOIN @@ -438,7 +438,7 @@ AND toTimeZone(timestamp, 'UTC') >= toDateTime(toStartOfDay(toDateTime('2012-01-08 00:00:00', 'UTC')), 'UTC') AND toTimeZone(timestamp, 'UTC') <= toDateTime('2012-01-15 23:59:59', 'UTC') AND ((and(ifNull(greater(toInt64OrNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(properties, 'int_value'), ''), 'null'), '^"|"$', '')), 10), 0), 1)) - AND (like(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(person_properties, 'fish'), ''), 'null'), '^"|"$', ''), '%fish%'))) + AND (ifNull(like(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(person_properties, 'fish'), ''), 'null'), '^"|"$', ''), '%fish%'), 0))) GROUP BY date) GROUP BY day_start ORDER BY day_start) @@ -506,7 +506,7 @@ AND toTimeZone(timestamp, 'UTC') >= toDateTime(toStartOfDay(toDateTime('2012-01-08 00:00:00', 'UTC')), 'UTC') AND toTimeZone(timestamp, 'UTC') <= toDateTime('2012-01-15 23:59:59', 'UTC') AND ((and(ifNull(greater(toInt64OrNull(nullIf(nullIf(events.mat_int_value, ''), 'null')), 10), 0), 1)) - AND (like(nullIf(nullIf(mat_pp_fish, ''), 'null'), '%fish%'))) + AND (ifNull(like(nullIf(nullIf(mat_pp_fish, ''), 'null'), '%fish%'), 0))) GROUP BY date) GROUP BY day_start ORDER BY day_start) @@ -548,7 +548,7 @@ AND toTimeZone(timestamp, 'UTC') >= toDateTime(toStartOfDay(toDateTime('2012-01-08 00:00:00', 'UTC')), 'UTC') AND toTimeZone(timestamp, 'UTC') <= toDateTime('2012-01-15 23:59:59', 'UTC') AND (and(ifNull(less(toInt64OrNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(properties, 'int_value'), ''), 'null'), '^"|"$', '')), 10), 0), 1) - AND like(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(person_properties, 'fish'), ''), 'null'), '^"|"$', ''), '%fish%')) + AND ifNull(like(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(person_properties, 'fish'), ''), 'null'), '^"|"$', ''), '%fish%'), 0)) GROUP BY date) GROUP BY day_start ORDER BY day_start) @@ -590,7 +590,7 @@ AND toTimeZone(timestamp, 'UTC') >= toDateTime(toStartOfDay(toDateTime('2012-01-08 00:00:00', 'UTC')), 'UTC') AND toTimeZone(timestamp, 'UTC') <= toDateTime('2012-01-15 23:59:59', 'UTC') AND (and(ifNull(less(toInt64OrNull(nullIf(nullIf(events.mat_int_value, ''), 'null')), 10), 0), 1) - AND like(nullIf(nullIf(mat_pp_fish, ''), 'null'), '%fish%')) + AND ifNull(like(nullIf(nullIf(mat_pp_fish, ''), 'null'), '%fish%'), 0)) GROUP BY date) GROUP BY day_start ORDER BY day_start) diff --git a/posthog/api/test/__snapshots__/test_query.ambr b/posthog/api/test/__snapshots__/test_query.ambr index 05501e8c5ac45..d467a08e15b7a 100644 --- a/posthog/api/test/__snapshots__/test_query.ambr +++ b/posthog/api/test/__snapshots__/test_query.ambr @@ -41,8 +41,7 @@ 'a%sd', concat(ifNull(toString(events.event), ''), ' ', ifNull(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, 'key'), ''), 'null'), '^"|"$', '')), '')) FROM events - WHERE and(equals(events.team_id, 2), ifNull(ilike(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, 'path'), ''), 'null'), '^"|"$', ''), '%/%'), isNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, 'path'), ''), 'null'), '^"|"$', '')) - and isNull('%/%')), less(toTimeZone(events.timestamp, 'UTC'), toDateTime64('2020-01-10 12:14:05.000000', 6, 'UTC')), greater(toTimeZone(events.timestamp, 'UTC'), toDateTime64('2020-01-09 12:14:00.000000', 6, 'UTC'))) + WHERE and(equals(events.team_id, 2), ifNull(ilike(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, 'path'), ''), 'null'), '^"|"$', ''), '%/%'), 0), less(toTimeZone(events.timestamp, 'UTC'), toDateTime64('2020-01-10 12:14:05.000000', 6, 'UTC')), greater(toTimeZone(events.timestamp, 'UTC'), toDateTime64('2020-01-09 12:14:00.000000', 6, 'UTC'))) ORDER BY events.event ASC LIMIT 101 OFFSET 0 SETTINGS readonly=2, @@ -93,8 +92,7 @@ 'a%sd', concat(ifNull(toString(events.event), ''), ' ', ifNull(toString(nullIf(nullIf(events.mat_key, ''), 'null')), '')) FROM events - WHERE and(equals(events.team_id, 2), ifNull(ilike(nullIf(nullIf(events.mat_path, ''), 'null'), '%/%'), isNull(nullIf(nullIf(events.mat_path, ''), 'null')) - and isNull('%/%')), less(toTimeZone(events.timestamp, 'UTC'), toDateTime64('2020-01-10 12:14:05.000000', 6, 'UTC')), greater(toTimeZone(events.timestamp, 'UTC'), toDateTime64('2020-01-09 12:14:00.000000', 6, 'UTC'))) + WHERE and(equals(events.team_id, 2), ifNull(ilike(nullIf(nullIf(events.mat_path, ''), 'null'), '%/%'), 0), less(toTimeZone(events.timestamp, 'UTC'), toDateTime64('2020-01-10 12:14:05.000000', 6, 'UTC')), greater(toTimeZone(events.timestamp, 'UTC'), toDateTime64('2020-01-09 12:14:00.000000', 6, 'UTC'))) ORDER BY events.event ASC LIMIT 101 OFFSET 0 SETTINGS readonly=2, @@ -533,7 +531,7 @@ SELECT count(), events.event FROM events - WHERE and(equals(events.team_id, 2), or(equals(events.event, 'sign up'), like(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, 'key'), ''), 'null'), '^"|"$', ''), '%val2')), less(toTimeZone(events.timestamp, 'UTC'), toDateTime64('2020-01-10 12:14:05.000000', 6, 'UTC')), greater(toTimeZone(events.timestamp, 'UTC'), toDateTime64('2020-01-09 12:14:00.000000', 6, 'UTC'))) + WHERE and(equals(events.team_id, 2), or(equals(events.event, 'sign up'), ifNull(like(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, 'key'), ''), 'null'), '^"|"$', ''), '%val2'), 0)), less(toTimeZone(events.timestamp, 'UTC'), toDateTime64('2020-01-10 12:14:05.000000', 6, 'UTC')), greater(toTimeZone(events.timestamp, 'UTC'), toDateTime64('2020-01-09 12:14:00.000000', 6, 'UTC'))) GROUP BY events.event ORDER BY count() DESC, events.event ASC LIMIT 101 diff --git a/posthog/hogql/database/database.py b/posthog/hogql/database/database.py index 59580ccc6d8e7..c45b4ca4caa8b 100644 --- a/posthog/hogql/database/database.py +++ b/posthog/hogql/database/database.py @@ -90,7 +90,7 @@ class Database(BaseModel): _timezone: Optional[str] _week_start_day: Optional[WeekStartDay] - def __init__(self, timezone: Optional[str], week_start_day: Optional[WeekStartDay]): + def __init__(self, timezone: Optional[str] = None, week_start_day: Optional[WeekStartDay] = None): super().__init__() try: self._timezone = str(ZoneInfo(timezone)) if timezone else None diff --git a/posthog/hogql/database/schema/persons.py b/posthog/hogql/database/schema/persons.py index 1a1d79123436d..f823f1ce3c9f4 100644 --- a/posthog/hogql/database/schema/persons.py +++ b/posthog/hogql/database/schema/persons.py @@ -53,7 +53,7 @@ def select_from_persons_table(requested_fields: Dict[str, List[str]], modifiers: SELECT id, max(version) as version FROM raw_persons GROUP BY id - HAVING ifNull(equals(argMax(raw_persons.is_deleted, raw_persons.version), 0), 0) + HAVING equals(argMax(raw_persons.is_deleted, raw_persons.version), 0) ) """ ) diff --git a/posthog/hogql/functions/mapping.py b/posthog/hogql/functions/mapping.py index 8d8fca037f21a..018ddc23b49b4 100644 --- a/posthog/hogql/functions/mapping.py +++ b/posthog/hogql/functions/mapping.py @@ -48,6 +48,21 @@ class HogQLFunctionMeta: """Whether the function is timezone-aware. This means the project timezone will be appended as the last arg.""" +HOGQL_COMPARISON_MAPPING: Dict[str, ast.CompareOperationOp] = { + "equals": ast.CompareOperationOp.Eq, + "notEquals": ast.CompareOperationOp.NotEq, + "less": ast.CompareOperationOp.Lt, + "greater": ast.CompareOperationOp.Gt, + "lessOrEquals": ast.CompareOperationOp.LtEq, + "greaterOrEquals": ast.CompareOperationOp.GtEq, + "like": ast.CompareOperationOp.Like, + "ilike": ast.CompareOperationOp.ILike, + "notLike": ast.CompareOperationOp.NotLike, + "notILike": ast.CompareOperationOp.NotILike, + "in": ast.CompareOperationOp.In, + "notIn": ast.CompareOperationOp.NotIn, +} + HOGQL_CLICKHOUSE_FUNCTIONS: Dict[str, HogQLFunctionMeta] = { # arithmetic "plus": HogQLFunctionMeta("plus", 2, 2), diff --git a/posthog/hogql/printer.py b/posthog/hogql/printer.py index 3bb0139b4b6f1..5e5a076b9e55f 100644 --- a/posthog/hogql/printer.py +++ b/posthog/hogql/printer.py @@ -29,7 +29,7 @@ escape_hogql_identifier, escape_hogql_string, ) -from posthog.hogql.functions.mapping import ALL_EXPOSED_FUNCTION_NAMES, validate_function_args +from posthog.hogql.functions.mapping import ALL_EXPOSED_FUNCTION_NAMES, validate_function_args, HOGQL_COMPARISON_MAPPING from posthog.hogql.resolver import ResolverException, resolve_types from posthog.hogql.resolver_utils import lookup_field_by_name from posthog.hogql.transforms.in_cohort import resolve_in_cohorts @@ -556,7 +556,11 @@ def visit_compare_operation(self, node: ast.CompareOperation): return op # Special optimization for "Eq" operator - if node.op == ast.CompareOperationOp.Eq: + if ( + node.op == ast.CompareOperationOp.Eq + or node.op == ast.CompareOperationOp.Like + or node.op == ast.CompareOperationOp.ILike + ): if isinstance(node.right, ast.Constant): if node.right.value is None: return f"isNull({left})" @@ -568,7 +572,11 @@ def visit_compare_operation(self, node: ast.CompareOperation): return f"ifNull({op}, isNull({left}) and isNull({right}))" # Worse case performance, but accurate # Special optimization for "NotEq" operator - if node.op == ast.CompareOperationOp.NotEq: + if ( + node.op == ast.CompareOperationOp.NotEq + or node.op == ast.CompareOperationOp.NotLike + or node.op == ast.CompareOperationOp.NotILike + ): if isinstance(node.right, ast.Constant): if node.right.value is None: return f"isNotNull({left})" @@ -655,7 +663,19 @@ def visit_field(self, node: ast.Field): raise HogQLException(f"Unknown Type, can not print {type(node.type).__name__}") def visit_call(self, node: ast.Call): - if node.name in HOGQL_AGGREGATIONS: + if node.name in HOGQL_COMPARISON_MAPPING: + op = HOGQL_COMPARISON_MAPPING[node.name] + if len(node.args) != 2: + raise HogQLException(f"Comparison '{node.name}' requires exactly two arguments") + # We do "cleverer" logic with nullable types in visit_compare_operation + return self.visit_compare_operation( + ast.CompareOperation( + left=node.args[0], + right=node.args[1], + op=op, + ) + ) + elif node.name in HOGQL_AGGREGATIONS: func_meta = HOGQL_AGGREGATIONS[node.name] validate_function_args( diff --git a/posthog/hogql/test/test_printer.py b/posthog/hogql/test/test_printer.py index 75f182a618f54..53f31749c85d2 100644 --- a/posthog/hogql/test/test_printer.py +++ b/posthog/hogql/test/test_printer.py @@ -7,7 +7,7 @@ from posthog.hogql.constants import HogQLQuerySettings, HogQLGlobalSettings from posthog.hogql.context import HogQLContext from posthog.hogql.database.database import Database -from posthog.hogql.database.models import DateDatabaseField +from posthog.hogql.database.models import DateDatabaseField, StringDatabaseField from posthog.hogql.errors import HogQLException from posthog.hogql.hogql import translate_hogql from posthog.hogql.parser import parse_select @@ -964,10 +964,36 @@ def test_functions_expecting_datetime_arg(self): ) def test_field_nullable_equals(self): - generated_sql_statements = self._select( - "SELECT min_first_timestamp = toStartOfMonth(now()), now() = now(), 1 = now(), now() = 1, 1 = 1, click_count = 1, 1 = click_count, click_count = keypress_count, click_count = null, null = click_count FROM session_replay_events" + generated_sql_statements1 = self._select( + "SELECT " + "min_first_timestamp = toStartOfMonth(now()), " + "now() = now(), " + "1 = now(), " + "now() = 1, " + "1 = 1, " + "click_count = 1, " + "1 = click_count, " + "click_count = keypress_count, " + "click_count = null, " + "null = click_count " + "FROM session_replay_events" + ) + generated_sql_statements2 = self._select( + "SELECT " + "equals(min_first_timestamp, toStartOfMonth(now())), " + "equals(now(), now()), " + "equals(1, now()), " + "equals(now(), 1), " + "equals(1, 1), " + "equals(click_count, 1), " + "equals(1, click_count), " + "equals(click_count, keypress_count), " + "equals(click_count, null), " + "equals(null, click_count) " + "FROM session_replay_events" ) - assert generated_sql_statements == ( + assert generated_sql_statements1 == generated_sql_statements2 + assert generated_sql_statements1 == ( f"SELECT " # min_first_timestamp = toStartOfMonth(now()) # (the return of toStartOfMonth() is treated as "potentially nullable" since we yet have full typing support) @@ -996,12 +1022,18 @@ def test_field_nullable_equals(self): ) def test_field_nullable_not_equals(self): - generated_sql = self._select( + generated_sql1 = self._select( "SELECT min_first_timestamp != toStartOfMonth(now()), now() != now(), 1 != now(), now() != 1, 1 != 1, " "click_count != 1, 1 != click_count, click_count != keypress_count, click_count != null, null != click_count " "FROM session_replay_events" ) - assert generated_sql == ( + generated_sql2 = self._select( + "SELECT notEquals(min_first_timestamp, toStartOfMonth(now())), notEquals(now(), now()), notEquals(1, now()), notEquals(now(), 1), notEquals(1, 1), " + "notEquals(click_count, 1), notEquals(1, click_count), notEquals(click_count, keypress_count), notEquals(click_count, null), notEquals(null, click_count) " + "FROM session_replay_events" + ) + assert generated_sql1 == generated_sql2 + assert generated_sql1 == ( f"SELECT " # min_first_timestamp = toStartOfMonth(now()) # (the return of toStartOfMonth() is treated as "potentially nullable" since we yet have full typing support) @@ -1029,6 +1061,98 @@ def test_field_nullable_not_equals(self): f"FROM (SELECT session_replay_events.min_first_timestamp AS min_first_timestamp, sum(session_replay_events.click_count) AS click_count, sum(session_replay_events.keypress_count) AS keypress_count FROM session_replay_events WHERE equals(session_replay_events.team_id, {self.team.pk}) GROUP BY session_replay_events.min_first_timestamp) AS session_replay_events LIMIT 10000" ) + def test_field_nullable_like(self): + context = HogQLContext(team_id=self.team.pk, enable_select_queries=True, database=Database()) + context.database.events.fields["nullable_field"] = StringDatabaseField(name="nullable_field", nullable=True) # type: ignore + generated_sql_statements1 = self._select( + "SELECT " + "nullable_field like 'a', " + "nullable_field like null, " + "null like nullable_field, " + "null like 'a', " + "'a' like nullable_field, " + "'a' like null " + "FROM events", + context, + ) + + context = HogQLContext(team_id=self.team.pk, enable_select_queries=True, database=Database()) + context.database.events.fields["nullable_field"] = StringDatabaseField(name="nullable_field", nullable=True) # type: ignore + generated_sql_statements2 = self._select( + "SELECT " + "like(nullable_field, 'a'), " + "like(nullable_field, null), " + "like(null, nullable_field), " + "like(null, 'a'), " + "like('a', nullable_field), " + "like('a', null) " + "FROM events", + context, + ) + assert generated_sql_statements1 == generated_sql_statements2 + assert generated_sql_statements1 == ( + f"SELECT " + # event like 'a', + "ifNull(like(events.nullable_field, %(hogql_val_0)s), 0), " + # event like null, + "isNull(events.nullable_field), " + # null like event, + "isNull(events.nullable_field), " + # null like 'a', + "ifNull(like(NULL, %(hogql_val_1)s), 0), " + # 'a' like event, + "ifNull(like(%(hogql_val_2)s, events.nullable_field), 0), " + # 'a' like null + "isNull(%(hogql_val_3)s) " + f"FROM events WHERE equals(events.team_id, {self.team.pk}) LIMIT 10000" + ) + + def test_field_nullable_not_like(self): + context = HogQLContext(team_id=self.team.pk, enable_select_queries=True, database=Database()) + context.database.events.fields["nullable_field"] = StringDatabaseField(name="nullable_field", nullable=True) # type: ignore + generated_sql_statements1 = self._select( + "SELECT " + "nullable_field not like 'a', " + "nullable_field not like null, " + "null not like nullable_field, " + "null not like 'a', " + "'a' not like nullable_field, " + "'a' not like null " + "FROM events", + context, + ) + + context = HogQLContext(team_id=self.team.pk, enable_select_queries=True, database=Database()) + context.database.events.fields["nullable_field"] = StringDatabaseField(name="nullable_field", nullable=True) # type: ignore + generated_sql_statements2 = self._select( + "SELECT " + "notLike(nullable_field, 'a'), " + "notLike(nullable_field, null), " + "notLike(null, nullable_field), " + "notLike(null, 'a'), " + "notLike('a', nullable_field), " + "notLike('a', null) " + "FROM events", + context, + ) + assert generated_sql_statements1 == generated_sql_statements2 + assert generated_sql_statements1 == ( + f"SELECT " + # event like 'a', + "ifNull(notLike(events.nullable_field, %(hogql_val_0)s), 1), " + # event like null, + "isNotNull(events.nullable_field), " + # null like event, + "isNotNull(events.nullable_field), " + # null like 'a', + "ifNull(notLike(NULL, %(hogql_val_1)s), 1), " + # 'a' like event, + "ifNull(notLike(%(hogql_val_2)s, events.nullable_field), 1), " + # 'a' like null + "isNotNull(%(hogql_val_3)s) " + f"FROM events WHERE equals(events.team_id, {self.team.pk}) LIMIT 10000" + ) + def test_print_global_settings(self): query = parse_select("SELECT 1 FROM events") printed = print_ast(