diff options
Diffstat (limited to 'sqlglot/generator.py')
-rw-r--r-- | sqlglot/generator.py | 102 |
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, |