Skip to content

Commit 0c017e3

Browse files
sampenningtonclaude
andcommitted
fix(insights): coerce nullable string args of array-returning functions
splitByChar (and the other string-splitting functions) return Array(...). Given a Nullable string argument — e.g. a breakdown on splitByChar('@', properties.$distinct_id) — ClickHouse would produce Nullable(Array(...)), which it rejects, failing the whole insight query. The printer now wraps a nullable string argument of these functions in ifNull(arg, '') so the result type stays a plain Array. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent c206fa4 commit 0c017e3

2 files changed

Lines changed: 34 additions & 0 deletions

File tree

posthog/hogql/printer/clickhouse.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,22 @@ def team_id_guard_for_table(table_type: ast.TableOrSelectType, context: HogQLCon
7171
)
7272

7373

74+
# String functions that return an Array — a Nullable string arg would produce an
75+
# illegal Nullable(Array(...)) type, so their nullable string args are coerced.
76+
_ARRAY_RETURNING_STRING_FUNCTIONS = frozenset(
77+
{
78+
"splitByChar",
79+
"splitByString",
80+
"splitByRegexp",
81+
"splitByWhitespace",
82+
"splitByNonAlpha",
83+
"alphaTokens",
84+
"extractAllGroups",
85+
"ngrams",
86+
"tokens",
87+
}
88+
)
89+
7490
# In non-nullable materialized columns, these values are treated as NULL
7591
MAT_COL_NULL_SENTINELS = ["", "null"]
7692

@@ -155,6 +171,18 @@ def _render_function_call(self, node: ast.Call, func_meta) -> str:
155171
args.append(f"ifNull({self.visit(arg)}, '')")
156172
else:
157173
args.append(f"ifNull(toString({self.visit(arg)}), '')")
174+
elif node.name in _ARRAY_RETURNING_STRING_FUNCTIONS:
175+
# These return Array(...). A Nullable string argument would make the
176+
# result Nullable(Array(...)), which ClickHouse rejects, so coerce
177+
# nullable string args to a non-nullable empty string.
178+
args = []
179+
for arg in node_args:
180+
visited = self.visit(arg)
181+
arg_type = arg.type.resolve_constant_type(self.context) if arg.type is not None else None
182+
if isinstance(arg_type, ast.StringType) and arg_type.nullable:
183+
args.append(f"ifNull({visited}, '')")
184+
else:
185+
args.append(visited)
158186
else:
159187
args = [self.visit(arg) for arg in node_args]
160188

posthog/hogql/printer/test/test_printer.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4813,6 +4813,12 @@ def test_can_call_parametric_function(self):
48134813
)
48144814
assert query_response.results == [(6,)]
48154815

4816+
def test_split_function_on_nullable_property(self):
4817+
# splitByChar on a nullable property would produce an illegal Nullable(Array)
4818+
query = parse_select("SELECT splitByChar('@', properties.email) FROM events")
4819+
query_response = execute_hogql_query(team=self.team, query=query)
4820+
assert query_response.results is not None
4821+
48164822

48174823
class TestPostgresPrinter(BaseTest):
48184824
maxDiff = None

0 commit comments

Comments
 (0)