Skip to content

Commit e3f5b22

Browse files
NickCrewsclaude
andcommitted
Type SQLExpression and spark expr() as LiteralString
Encourages static detection of SQL injection: passing a dynamically built string to SQLExpression() or the spark expr() function now fails typechecking unless explicitly suppressed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 5829acc commit e3f5b22

3 files changed

Lines changed: 10 additions & 5 deletions

File tree

_duckdb-stubs/__init__.pyi

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import os
22
import pathlib
33
import typing
4-
from typing_extensions import Self
4+
from typing_extensions import LiteralString, Self
55
from ._expression import Expression
66
from ._enums import (
77
CSVLineTerminator,
@@ -863,7 +863,7 @@ def ConstantExpression(value: PythonLiteral) -> Expression: ...
863863
def DefaultExpression() -> Expression: ...
864864
def FunctionExpression(function_name: str, *args: IntoExpr) -> Expression: ...
865865
def LambdaExpression(lhs: IntoExprColumn | tuple[IntoExprColumn, ...], rhs: IntoExpr) -> Expression: ...
866-
def SQLExpression(expression: str) -> Expression: ...
866+
def SQLExpression(expression: LiteralString) -> Expression: ...
867867
def StarExpression(*, exclude: Iterable[IntoExprColumn] | None = None) -> Expression: ...
868868
def aggregate(
869869
df: pandas.DataFrame,

duckdb/experimental/spark/sql/functions.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import warnings
22
from collections.abc import Callable
3-
from typing import TYPE_CHECKING, Any, Optional, Union, overload
3+
from typing import TYPE_CHECKING, Any, LiteralString, Optional, Union, overload
44

55
from duckdb import (
66
CaseExpression,
@@ -6176,7 +6176,7 @@ def instr(str: "ColumnOrName", substr: str) -> Column:
61766176
return _invoke_function("instr", _to_column_expr(str), ConstantExpression(substr))
61776177

61786178

6179-
def expr(str: str) -> Column:
6179+
def expr(str: LiteralString) -> Column:
61806180
"""Parses the expression string into the column that it represents.
61816181
61826182
.. versionadded:: 1.5.0
@@ -6186,7 +6186,7 @@ def expr(str: str) -> Column:
61866186
61876187
Parameters
61886188
----------
6189-
str : str
6189+
str : LiteralString
61906190
expression defined in string.
61916191
61926192
Returns:

tests/fast/test_sql_expression.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ def test_sql_expression_basic(self, duckdb_cursor):
4949
rel = duckdb_cursor.sql("SELECT 1").select(expr)
5050
assert rel.fetchall() == [("hello world",)]
5151

52+
# The annotation widens this to plain str, so passing it where a
53+
# LiteralString is expected must give a typechecking error.
54+
dangerous: str = "'untrusted input'"
55+
SQLExpression("'this is ' || " + dangerous) # type: ignore[arg-type]
56+
5257
def test_sql_expression_with_columns(self, duckdb_cursor):
5358
# Create a test table
5459
duckdb_cursor.execute(

0 commit comments

Comments
 (0)