Skip to content

Commit 82a1bb4

Browse files
authored
feat: Support for RPAD & LPAD functions (#3757)
* feat: Support for RPAD & LPAD functions * Switch to boolean-based kind * Refactor boolean arg
1 parent 321051a commit 82a1bb4

File tree

9 files changed

+65
-0
lines changed

9 files changed

+65
-0
lines changed

sqlglot/dialects/duckdb.py

+1
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,7 @@ class Generator(generator.Generator):
389389
SUPPORTS_TO_NUMBER = False
390390
COPY_HAS_INTO_KEYWORD = False
391391
STAR_EXCEPT = "EXCLUDE"
392+
PAD_FILL_PATTERN_IS_REQUIRED = True
392393

393394
TRANSFORMS = {
394395
**generator.Generator.TRANSFORMS,

sqlglot/dialects/hive.py

+1
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,7 @@ class Generator(generator.Generator):
447447
SUPPORTS_TO_NUMBER = False
448448
WITH_PROPERTIES_PREFIX = "TBLPROPERTIES"
449449
PARSE_JSON_NAME = None
450+
PAD_FILL_PATTERN_IS_REQUIRED = True
450451

451452
EXPRESSIONS_WITHOUT_NESTED_CTES = {
452453
exp.Insert,

sqlglot/dialects/mysql.py

+1
Original file line numberDiff line numberDiff line change
@@ -690,6 +690,7 @@ class Generator(generator.Generator):
690690
JSON_KEY_VALUE_PAIR_SEP = ","
691691
SUPPORTS_TO_NUMBER = False
692692
PARSE_JSON_NAME = None
693+
PAD_FILL_PATTERN_IS_REQUIRED = True
693694

694695
TRANSFORMS = {
695696
**generator.Generator.TRANSFORMS,

sqlglot/dialects/presto.py

+1
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,7 @@ class Generator(generator.Generator):
352352
SUPPORTS_TO_NUMBER = False
353353
HEX_FUNC = "TO_HEX"
354354
PARSE_JSON_NAME = "JSON_PARSE"
355+
PAD_FILL_PATTERN_IS_REQUIRED = True
355356

356357
PROPERTIES_LOCATION = {
357358
**generator.Generator.PROPERTIES_LOCATION,

sqlglot/dialects/spark.py

+1
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ def _parse_generated_as_identity(
131131

132132
class Generator(Spark2.Generator):
133133
SUPPORTS_TO_NUMBER = True
134+
PAD_FILL_PATTERN_IS_REQUIRED = False
134135

135136
TYPE_MAPPING = {
136137
**Spark2.Generator.TYPE_MAPPING,

sqlglot/expressions.py

+5
Original file line numberDiff line numberDiff line change
@@ -4798,6 +4798,11 @@ class List(Func):
47984798
is_var_len_args = True
47994799

48004800

4801+
# String pad, kind True -> LPAD, False -> RPAD
4802+
class Pad(Func):
4803+
arg_types = {"this": True, "expression": True, "fill_pattern": False, "is_left": True}
4804+
4805+
48014806
# https://docs.snowflake.com/en/sql-reference/functions/to_char
48024807
# https://docs.oracle.com/en/database/oracle/oracle-database/23/sqlrf/TO_CHAR-number.html
48034808
class ToChar(Func):

sqlglot/generator.py

+12
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,9 @@ class Generator(metaclass=_Generator):
375375
# Whether to quote the generated expression of exp.JsonPath
376376
QUOTE_JSON_PATH = True
377377

378+
# Whether the text pattern/fill (3rd) parameter of RPAD()/LPAD() is optional (defaults to space)
379+
PAD_FILL_PATTERN_IS_REQUIRED = False
380+
378381
# The name to generate for the JSONPath expression. If `None`, only `this` will be generated
379382
PARSE_JSON_NAME: t.Optional[str] = "PARSE_JSON"
380383

@@ -4048,3 +4051,12 @@ def changes_sql(self, expression: exp.Changes) -> str:
40484051
end = f"{self.seg('')}{end}" if end else ""
40494052

40504053
return f"CHANGES ({information}){at_before}{end}"
4054+
4055+
def pad_sql(self, expression: exp.Pad) -> str:
4056+
prefix = "L" if expression.args.get("is_left") else "R"
4057+
4058+
fill_pattern = self.sql(expression, "fill_pattern") or None
4059+
if not fill_pattern and self.PAD_FILL_PATTERN_IS_REQUIRED:
4060+
fill_pattern = "' '"
4061+
4062+
return self.func(f"{prefix}PAD", expression.this, expression.expression, fill_pattern)

sqlglot/parser.py

+13
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,15 @@ def build_mod(args: t.List) -> exp.Mod:
108108
return exp.Mod(this=this, expression=expression)
109109

110110

111+
def build_pad(args: t.List, is_left: bool = True):
112+
return exp.Pad(
113+
this=seq_get(args, 0),
114+
expression=seq_get(args, 1),
115+
fill_pattern=seq_get(args, 2),
116+
is_left=is_left,
117+
)
118+
119+
111120
class _Parser(type):
112121
def __new__(cls, clsname, bases, attrs):
113122
klass = super().__new__(cls, clsname, bases, attrs)
@@ -159,7 +168,11 @@ class Parser(metaclass=_Parser):
159168
"LOG2": lambda args: exp.Log(this=exp.Literal.number(2), expression=seq_get(args, 0)),
160169
"LOG10": lambda args: exp.Log(this=exp.Literal.number(10), expression=seq_get(args, 0)),
161170
"LOWER": build_lower,
171+
"LPAD": lambda args: build_pad(args),
172+
"LEFTPAD": lambda args: build_pad(args),
162173
"MOD": build_mod,
174+
"RPAD": lambda args: build_pad(args, is_left=False),
175+
"RIGHTPAD": lambda args: build_pad(args, is_left=False),
163176
"SCOPE_RESOLUTION": lambda args: exp.ScopeResolution(expression=seq_get(args, 0))
164177
if len(args) != 2
165178
else exp.ScopeResolution(this=seq_get(args, 0), expression=seq_get(args, 1)),

tests/dialects/test_dialect.py

+30
Original file line numberDiff line numberDiff line change
@@ -2573,3 +2573,33 @@ def test_reserved_keywords(self):
25732573
"""SELECT partition.d FROM t PARTITION (d)""",
25742574
"""SELECT partition.d FROM t AS PARTITION(d)""",
25752575
)
2576+
2577+
def test_string_functions(self):
2578+
for pad_func in ("LPAD", "RPAD"):
2579+
ch_alias = "LEFTPAD" if pad_func == "LPAD" else "RIGHTPAD"
2580+
for fill_pattern in ("", ", ' '"):
2581+
with self.subTest(f"Testing {pad_func}() with pattern {fill_pattern}"):
2582+
self.validate_all(
2583+
f"SELECT {pad_func}('bar', 5{fill_pattern})",
2584+
read={
2585+
"snowflake": f"SELECT {pad_func}('bar', 5{fill_pattern})",
2586+
"databricks": f"SELECT {pad_func}('bar', 5{fill_pattern})",
2587+
"spark": f"SELECT {pad_func}('bar', 5{fill_pattern})",
2588+
"postgres": f"SELECT {pad_func}('bar', 5{fill_pattern})",
2589+
"clickhouse": f"SELECT {ch_alias}('bar', 5{fill_pattern})",
2590+
},
2591+
write={
2592+
"": f"SELECT {pad_func}('bar', 5{fill_pattern})",
2593+
"spark": f"SELECT {pad_func}('bar', 5{fill_pattern})",
2594+
"postgres": f"SELECT {pad_func}('bar', 5{fill_pattern})",
2595+
"clickhouse": f"SELECT {pad_func}('bar', 5{fill_pattern})",
2596+
"snowflake": f"SELECT {pad_func}('bar', 5{fill_pattern})",
2597+
"databricks": f"SELECT {pad_func}('bar', 5{fill_pattern})",
2598+
"duckdb": f"SELECT {pad_func}('bar', 5, ' ')",
2599+
"mysql": f"SELECT {pad_func}('bar', 5, ' ')",
2600+
"hive": f"SELECT {pad_func}('bar', 5, ' ')",
2601+
"spark2": f"SELECT {pad_func}('bar', 5, ' ')",
2602+
"presto": f"SELECT {pad_func}('bar', 5, ' ')",
2603+
"trino": f"SELECT {pad_func}('bar', 5, ' ')",
2604+
},
2605+
)

0 commit comments

Comments
 (0)