Skip to content

Commit 0d33d57

Browse files
feat(duckdb): Add transpilation support for SUBSTR-SUBSTRING functions
1 parent 3835725 commit 0d33d57

4 files changed

Lines changed: 37 additions & 2 deletions

File tree

sqlglot-integration-tests

sqlglot/expressions/string.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ class Stuff(Expression, Func):
220220

221221
class Substring(Expression, Func):
222222
_sql_names = ["SUBSTRING", "SUBSTR"]
223-
arg_types = {"this": True, "start": False, "length": False}
223+
arg_types = {"this": True, "start": False, "length": False, "zero_start": False}
224224

225225

226226
class SubstringIndex(Expression, Func):

sqlglot/generators/duckdb.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2504,6 +2504,35 @@ def strposition_sql(self, expression: exp.StrPosition) -> str:
25042504

25052505
return strposition_sql(self, expression)
25062506

2507+
def substring_sql(self, expression: exp.Substring) -> str:
2508+
if expression.args.get("zero_start"):
2509+
start = expression.args.get("start")
2510+
length = expression.args.get("length")
2511+
2512+
new_start = (
2513+
exp.Literal.number(1)
2514+
if start is not None and start.is_number and start.to_py() == 0
2515+
else exp.If(this=start.eq(0), true=exp.Literal.number(1), false=start.copy())
2516+
if start is not None and not start.is_number
2517+
else None
2518+
)
2519+
new_length = (
2520+
exp.Literal.number(0)
2521+
if length is not None and length.is_number and length.to_py() < 0
2522+
else exp.If(this=length.copy() < 0, true=exp.Literal.number(0), false=length.copy())
2523+
if length is not None and not length.is_number
2524+
else None
2525+
)
2526+
2527+
if new_start is not None or new_length is not None:
2528+
expression = expression.copy()
2529+
if new_start is not None:
2530+
expression.set("start", new_start)
2531+
if new_length is not None:
2532+
expression.set("length", new_length)
2533+
2534+
return self.function_fallback_sql(expression)
2535+
25072536
def strtotime_sql(self, expression: exp.StrToTime) -> str:
25082537
# Check if target_type requires TIMESTAMPTZ (for LTZ/TZ variants)
25092538
target_type = expression.args.get("target_type")

sqlglot/parsers/snowflake.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -726,6 +726,7 @@ class SnowflakeParser(parser.Parser):
726726
"OBJECT_CONSTRUCT_KEEP_NULL": lambda self: self._parse_json_object(),
727727
"LISTAGG": lambda self: self._parse_string_agg(),
728728
"SEMANTIC_VIEW": lambda self: self._parse_semantic_view(),
729+
"SUBSTR": lambda self: self._parse_substring(),
729730
}
730731
FUNCTION_PARSERS = {k: v for k, v in FUNCTION_PARSERS.items() if k != "TRIM"}
731732

@@ -1284,6 +1285,11 @@ def _parse_position(self, haystack_first: bool = False) -> exp.StrPosition:
12841285
result.set("clamp_position", True)
12851286
return result
12861287

1288+
def _parse_substring(self) -> exp.Substring:
1289+
result = super()._parse_substring()
1290+
result.set("zero_start", True)
1291+
return result
1292+
12871293
def _parse_window(self, this: exp.Expr | None, alias: bool = False) -> exp.Expr | None:
12881294
if isinstance(this, exp.NthValue):
12891295
if self._match_text_seq("FROM"):

0 commit comments

Comments
 (0)