summaryrefslogtreecommitdiffstats
path: root/sqlglot/generator.py
diff options
context:
space:
mode:
Diffstat (limited to 'sqlglot/generator.py')
-rw-r--r--sqlglot/generator.py102
1 files changed, 73 insertions, 29 deletions
diff --git a/sqlglot/generator.py b/sqlglot/generator.py
index d3cf9f0..8d82db4 100644
--- a/sqlglot/generator.py
+++ b/sqlglot/generator.py
@@ -5,7 +5,7 @@ import typing as t
from sqlglot import exp
from sqlglot.errors import ErrorLevel, UnsupportedError, concat_messages
-from sqlglot.helper import apply_index_offset, csv, seq_get, should_identify
+from sqlglot.helper import apply_index_offset, csv, seq_get
from sqlglot.time import format_time
from sqlglot.tokens import TokenType
@@ -56,39 +56,40 @@ class Generator:
exp.TsOrDsAdd: lambda self, e: self.func(
"TS_OR_DS_ADD", e.this, e.expression, exp.Literal.string(e.text("unit"))
),
- exp.VarMap: lambda self, e: self.func("MAP", e.args["keys"], e.args["values"]),
+ exp.CaseSpecificColumnConstraint: lambda self, e: f"{'NOT ' if e.args.get('not_') else ''}CASESPECIFIC",
+ exp.CharacterSetColumnConstraint: lambda self, e: f"CHARACTER SET {self.sql(e, 'this')}",
exp.CharacterSetProperty: lambda self, e: f"{'DEFAULT ' if e.args.get('default') else ''}CHARACTER SET={self.sql(e, 'this')}",
+ exp.CheckColumnConstraint: lambda self, e: f"CHECK ({self.sql(e, 'this')})",
+ exp.CollateColumnConstraint: lambda self, e: f"COLLATE {self.sql(e, 'this')}",
+ exp.CopyGrantsProperty: lambda self, e: "COPY GRANTS",
+ exp.CommentColumnConstraint: lambda self, e: f"COMMENT {self.sql(e, 'this')}",
+ exp.DateFormatColumnConstraint: lambda self, e: f"FORMAT {self.sql(e, 'this')}",
+ exp.DefaultColumnConstraint: lambda self, e: f"DEFAULT {self.sql(e, 'this')}",
+ exp.EncodeColumnConstraint: lambda self, e: f"ENCODE {self.sql(e, 'this')}",
exp.ExecuteAsProperty: lambda self, e: self.naked_property(e),
exp.ExternalProperty: lambda self, e: "EXTERNAL",
+ exp.InlineLengthColumnConstraint: lambda self, e: f"INLINE LENGTH {self.sql(e, 'this')}",
exp.LanguageProperty: lambda self, e: self.naked_property(e),
exp.LocationProperty: lambda self, e: self.naked_property(e),
exp.LogProperty: lambda self, e: f"{'NO ' if e.args.get('no') else ''}LOG",
exp.MaterializedProperty: lambda self, e: "MATERIALIZED",
exp.NoPrimaryIndexProperty: lambda self, e: "NO PRIMARY INDEX",
exp.OnCommitProperty: lambda self, e: f"ON COMMIT {'DELETE' if e.args.get('delete') else 'PRESERVE'} ROWS",
+ exp.OnUpdateColumnConstraint: lambda self, e: f"ON UPDATE {self.sql(e, 'this')}",
+ exp.PathColumnConstraint: lambda self, e: f"PATH {self.sql(e, 'this')}",
exp.ReturnsProperty: lambda self, e: self.naked_property(e),
exp.SetProperty: lambda self, e: f"{'MULTI' if e.args.get('multi') else ''}SET",
exp.SettingsProperty: lambda self, e: f"SETTINGS{self.seg('')}{(self.expressions(e))}",
exp.SqlSecurityProperty: lambda self, e: f"SQL SECURITY {'DEFINER' if e.args.get('definer') else 'INVOKER'}",
+ exp.StabilityProperty: lambda self, e: e.name,
exp.TemporaryProperty: lambda self, e: f"TEMPORARY",
exp.ToTableProperty: lambda self, e: f"TO {self.sql(e.this)}",
exp.TransientProperty: lambda self, e: "TRANSIENT",
- exp.StabilityProperty: lambda self, e: e.name,
+ exp.TitleColumnConstraint: lambda self, e: f"TITLE {self.sql(e, 'this')}",
+ exp.UppercaseColumnConstraint: lambda self, e: f"UPPERCASE",
+ exp.VarMap: lambda self, e: self.func("MAP", e.args["keys"], e.args["values"]),
exp.VolatileProperty: lambda self, e: "VOLATILE",
exp.WithJournalTableProperty: lambda self, e: f"WITH JOURNAL TABLE={self.sql(e, 'this')}",
- exp.CaseSpecificColumnConstraint: lambda self, e: f"{'NOT ' if e.args.get('not_') else ''}CASESPECIFIC",
- exp.CharacterSetColumnConstraint: lambda self, e: f"CHARACTER SET {self.sql(e, 'this')}",
- exp.DateFormatColumnConstraint: lambda self, e: f"FORMAT {self.sql(e, 'this')}",
- exp.OnUpdateColumnConstraint: lambda self, e: f"ON UPDATE {self.sql(e, 'this')}",
- exp.UppercaseColumnConstraint: lambda self, e: f"UPPERCASE",
- exp.TitleColumnConstraint: lambda self, e: f"TITLE {self.sql(e, 'this')}",
- exp.PathColumnConstraint: lambda self, e: f"PATH {self.sql(e, 'this')}",
- exp.CheckColumnConstraint: lambda self, e: f"CHECK ({self.sql(e, 'this')})",
- exp.CommentColumnConstraint: lambda self, e: f"COMMENT {self.sql(e, 'this')}",
- exp.CollateColumnConstraint: lambda self, e: f"COLLATE {self.sql(e, 'this')}",
- exp.EncodeColumnConstraint: lambda self, e: f"ENCODE {self.sql(e, 'this')}",
- exp.DefaultColumnConstraint: lambda self, e: f"DEFAULT {self.sql(e, 'this')}",
- exp.InlineLengthColumnConstraint: lambda self, e: f"INLINE LENGTH {self.sql(e, 'this')}",
}
# Whether or not null ordering is supported in order by
@@ -142,6 +143,9 @@ class Generator:
# Whether or not comparing against booleans (e.g. x IS TRUE) is supported
IS_BOOL_ALLOWED = True
+ # https://cloud.google.com/bigquery/docs/reference/standard-sql/query-syntax
+ SELECT_KINDS: t.Tuple[str, ...] = ("STRUCT", "VALUE")
+
TYPE_MAPPING = {
exp.DataType.Type.NCHAR: "CHAR",
exp.DataType.Type.NVARCHAR: "VARCHAR",
@@ -182,6 +186,7 @@ class Generator:
exp.CharacterSetProperty: exp.Properties.Location.POST_SCHEMA,
exp.ChecksumProperty: exp.Properties.Location.POST_NAME,
exp.CollateProperty: exp.Properties.Location.POST_SCHEMA,
+ exp.CopyGrantsProperty: exp.Properties.Location.POST_SCHEMA,
exp.Cluster: exp.Properties.Location.POST_SCHEMA,
exp.DataBlocksizeProperty: exp.Properties.Location.POST_NAME,
exp.DefinerProperty: exp.Properties.Location.POST_CREATE,
@@ -263,6 +268,8 @@ class Generator:
NORMALIZE_FUNCTIONS: bool | str = "upper"
NULL_ORDERING = "nulls_are_small"
+ can_identify: t.Callable[[str, str | bool], bool]
+
# Delimiters for quotes, identifiers and the corresponding escape characters
QUOTE_START = "'"
QUOTE_END = "'"
@@ -771,9 +778,11 @@ class Generator:
return this
def rawstring_sql(self, expression: exp.RawString) -> str:
+ string = expression.this
if self.RAW_START:
- return f"{self.RAW_START}{expression.name}{self.RAW_END}"
- return self.sql(exp.Literal.string(expression.name.replace("\\", "\\\\")))
+ return f"{self.RAW_START}{self.escape_str(expression.this)}{self.RAW_END}"
+ string = self.escape_str(string.replace("\\", "\\\\"))
+ return f"{self.QUOTE_START}{string}{self.QUOTE_END}"
def datatypesize_sql(self, expression: exp.DataTypeSize) -> str:
this = self.sql(expression, "this")
@@ -815,7 +824,8 @@ class Generator:
)
where_sql = self.sql(expression, "where")
returning = self.sql(expression, "returning")
- sql = f"DELETE{this}{using_sql}{where_sql}{returning}"
+ limit = self.sql(expression, "limit")
+ sql = f"DELETE{this}{using_sql}{where_sql}{returning}{limit}"
return self.prepend_ctes(expression, sql)
def drop_sql(self, expression: exp.Drop) -> str:
@@ -883,7 +893,7 @@ class Generator:
text = text.replace(self.IDENTIFIER_END, self._escaped_identifier_end)
if (
expression.quoted
- or should_identify(text, self.identify)
+ or self.can_identify(text, self.identify)
or lower in self.RESERVED_KEYWORDS
or (not self.IDENTIFIERS_CAN_START_WITH_DIGIT and text[:1].isdigit())
):
@@ -1157,6 +1167,15 @@ class Generator:
null = f" NULL DEFINED AS {null}" if null else ""
return f"ROW FORMAT DELIMITED{fields}{escaped}{items}{keys}{lines}{null}"
+ def withtablehint_sql(self, expression: exp.WithTableHint) -> str:
+ return f"WITH ({self.expressions(expression, flat=True)})"
+
+ def indextablehint_sql(self, expression: exp.IndexTableHint) -> str:
+ this = f"{self.sql(expression, 'this')} INDEX"
+ target = self.sql(expression, "target")
+ target = f" FOR {target}" if target else ""
+ return f"{this}{target} ({self.expressions(expression, flat=True)})"
+
def table_sql(self, expression: exp.Table, sep: str = " AS ") -> str:
table = ".".join(
part
@@ -1170,8 +1189,8 @@ class Generator:
alias = self.sql(expression, "alias")
alias = f"{sep}{alias}" if alias else ""
- hints = self.expressions(expression, key="hints", flat=True)
- hints = f" WITH ({hints})" if hints and self.TABLE_HINTS else ""
+ hints = self.expressions(expression, key="hints", sep=" ")
+ hints = f" {hints}" if hints and self.TABLE_HINTS else ""
pivots = self.expressions(expression, key="pivots", sep=" ", flat=True)
pivots = f" {pivots}" if pivots else ""
joins = self.expressions(expression, key="joins", sep="")
@@ -1238,7 +1257,8 @@ class Generator:
from_sql = self.sql(expression, "from")
where_sql = self.sql(expression, "where")
returning = self.sql(expression, "returning")
- sql = f"UPDATE {this} SET {set_sql}{from_sql}{where_sql}{returning}"
+ limit = self.sql(expression, "limit")
+ sql = f"UPDATE {this} SET {set_sql}{from_sql}{where_sql}{returning}{limit}"
return self.prepend_ctes(expression, sql)
def values_sql(self, expression: exp.Values) -> str:
@@ -1413,10 +1433,13 @@ class Generator:
def literal_sql(self, expression: exp.Literal) -> str:
text = expression.this or ""
if expression.is_string:
- text = text.replace(self.QUOTE_END, self._escaped_quote_end)
- if self.pretty:
- text = text.replace("\n", self.SENTINEL_LINE_BREAK)
- text = f"{self.QUOTE_START}{text}{self.QUOTE_END}"
+ text = f"{self.QUOTE_START}{self.escape_str(text)}{self.QUOTE_END}"
+ return text
+
+ def escape_str(self, text: str) -> str:
+ text = text.replace(self.QUOTE_END, self._escaped_quote_end)
+ if self.pretty:
+ text = text.replace("\n", self.SENTINEL_LINE_BREAK)
return text
def loaddata_sql(self, expression: exp.LoadData) -> str:
@@ -1565,9 +1588,30 @@ class Generator:
hint = self.sql(expression, "hint")
distinct = self.sql(expression, "distinct")
distinct = f" {distinct}" if distinct else ""
- kind = expression.args.get("kind")
- kind = f" AS {kind}" if kind else ""
+ kind = self.sql(expression, "kind").upper()
expressions = self.expressions(expression)
+
+ if kind:
+ if kind in self.SELECT_KINDS:
+ kind = f" AS {kind}"
+ else:
+ if kind == "STRUCT":
+ expressions = self.expressions(
+ sqls=[
+ self.sql(
+ exp.Struct(
+ expressions=[
+ exp.column(e.output_name).eq(
+ e.this if isinstance(e, exp.Alias) else e
+ )
+ for e in expression.expressions
+ ]
+ )
+ )
+ ]
+ )
+ kind = ""
+
expressions = f"{self.sep()}{expressions}" if expressions else expressions
sql = self.query_modifiers(
expression,