Skip to content

Commit 2ac3dc1

Browse files
crerichaeakmanrq
andauthored
Feat: Render should not quote identifiers by default (#726)
* Do not add quotes to non-quoted identifiers when rendering * Fix migrate magic * First stab at migrations documentation * Remove quoting in expected results * fix quotes for schema diff --------- Co-authored-by: eakmanrq <6326532+eakmanrq@users.noreply.github.com>
1 parent d8b664b commit 2ac3dc1

23 files changed

Lines changed: 355 additions & 198 deletions

docs/guides/migrations.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Migrations guide
2+
3+
New versions of SQLMesh may be incompatible with the project's stored metadata format. Migrations provide a way to upgrade the project metadata format to operate with the new SQLMesh version.
4+
5+
## Detecting incompatibility
6+
When issuing a SQLMesh command, SQLMesh will automatically check for incompatibilities between the installed version of SQLMesh and the project's metadata format, prompting what action is required. SQLMesh commands will not execute until the action is complete.
7+
8+
### Installed version is newer than metadata format
9+
In this scenario, the project's metadata format needs to be migrated.
10+
11+
```bash
12+
> sqlmesh plan my_dev
13+
Error: SQLMesh (local) is using version '2' which is ahead of '1' (remote). Please run a migration.
14+
```
15+
16+
### Installed version is older than metadata format
17+
Here, the installed version of SQLMesh needs to be upgraded.
18+
19+
```bash
20+
> sqlmesh plan my_dev
21+
SQLMeshError: SQLMesh (local) is using version '1' which is behind '2' (remote). Please upgrade SQLMesh.
22+
```
23+
24+
## How to migrate
25+
The project metadata can be migrated to the latest metadata format using SQLMesh's migrate command.
26+
27+
```bash
28+
> sqlmesh migrate
29+
```
30+
31+
Migrating project metadata will affect all users of the project. Please consult your SQLMesh administrator before running the migrate command.

docs/reference/cli.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ Commands:
1818
fetchdf Runs a sql query and displays the results.
1919
format Format all models in a given directory.
2020
init Create a new SQLMesh repository.
21+
migrate Migrate SQLMesh to the current running version.
2122
plan Plan a migration of the current context's models with the...
2223
render Renders a model's query, optionally expanding referenced models.
2324
run Evaluates the DAG of models using the built-in scheduler.
@@ -169,3 +170,12 @@ Options:
169170
--file TEXT The file to which the dag image should be written.
170171
--help Show this message and exit.
171172
```
173+
174+
## migrate
175+
```
176+
Usage: sqlmesh migrate
177+
178+
Migrates SQLMesh to the current running version.
179+
180+
Please contact your SQLMesh administrator before doing this.
181+
```

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ nav:
1212
- guides/scheduling.md
1313
- guides/connections.md
1414
- guides/multi_repo.md
15+
- guides/migrations.md
1516
- Concepts:
1617
- concepts/overview.md
1718
- Models:

sqlmesh/cli/main.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,5 +342,13 @@ def ide(
342342
uvicorn.run("web.server.main:app", host=host, port=port, log_level="info")
343343

344344

345+
@cli.command("migrate")
346+
@click.pass_context
347+
@error_handler
348+
def migrate(ctx: click.Context) -> None:
349+
"""Migrate SQLMesh to the current running version."""
350+
ctx.obj.migrate()
351+
352+
345353
if __name__ == "__main__":
346354
cli()

sqlmesh/core/engine_adapter/base.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -777,7 +777,6 @@ def _to_sql(self, e: exp.Expression, **kwargs: t.Any) -> str:
777777
"dialect": self.dialect,
778778
"pretty": False,
779779
"comments": False,
780-
"identify": True,
781780
**self.DEFAULT_SQL_GEN_KWARGS,
782781
**self.sql_gen_kwargs,
783782
**kwargs,

sqlmesh/core/engine_adapter/databricks.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,5 @@ class DatabricksSparkSessionEngineAdapter(SparkEngineAdapter):
99
SCHEMA_DIFFER = SchemaDiffer(
1010
support_positional_add=True,
1111
support_nested_operations=True,
12-
array_suffix=".element",
12+
array_element_selector="element",
1313
)

sqlmesh/core/engine_adapter/databricks_api.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ class DatabricksSQLEngineAdapter(BaseSparkEngineAdapter):
1717
SCHEMA_DIFFER = SchemaDiffer(
1818
support_positional_add=True,
1919
support_nested_operations=True,
20-
array_suffix=".element",
20+
array_element_selector="element",
2121
)
2222

2323
def _fetch_native_df(self, query: t.Union[exp.Expression, str]) -> DF:

sqlmesh/core/engine_adapter/postgres.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ def replace_query(
4242
if not self.table_exists(table_name):
4343
return self.ctas(table_name, query_or_df, columns_to_types, exists=False)
4444
with self.transaction(TransactionType.DDL):
45-
sql = f"TRUNCATE {exp.to_table(table_name).sql(identify=True, dialect=self.dialect)}"
45+
sql = f"TRUNCATE {exp.to_table(table_name).sql(dialect=self.dialect)}"
4646
self.execute(sql)
4747
return self.insert_append(table_name, query_or_df, columns_to_types)
4848

sqlmesh/core/schema_diff.py

Lines changed: 70 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -32,43 +32,61 @@ class TableAlterColumn(PydanticModel):
3232
is_struct: bool
3333
is_array_of_struct: bool
3434
is_array_of_primitive: bool
35+
quoted: bool = False
3536

3637
@classmethod
37-
def primitive(self, name: str) -> TableAlterColumn:
38+
def primitive(self, name: str, quoted: bool = False) -> TableAlterColumn:
3839
return self(
39-
name=name, is_struct=False, is_array_of_struct=False, is_array_of_primitive=False
40+
name=name,
41+
is_struct=False,
42+
is_array_of_struct=False,
43+
is_array_of_primitive=False,
44+
quoted=quoted,
4045
)
4146

4247
@classmethod
43-
def struct(self, name: str) -> TableAlterColumn:
48+
def struct(self, name: str, quoted: bool = False) -> TableAlterColumn:
4449
return self(
45-
name=name, is_struct=True, is_array_of_struct=False, is_array_of_primitive=False
50+
name=name,
51+
is_struct=True,
52+
is_array_of_struct=False,
53+
is_array_of_primitive=False,
54+
quoted=quoted,
4655
)
4756

4857
@classmethod
49-
def array_of_struct(self, name: str) -> TableAlterColumn:
58+
def array_of_struct(self, name: str, quoted: bool = False) -> TableAlterColumn:
5059
return self(
51-
name=name, is_struct=False, is_array_of_struct=True, is_array_of_primitive=False
60+
name=name,
61+
is_struct=False,
62+
is_array_of_struct=True,
63+
is_array_of_primitive=False,
64+
quoted=quoted,
5265
)
5366

5467
@classmethod
55-
def array_of_primitive(self, name: str) -> TableAlterColumn:
68+
def array_of_primitive(self, name: str, quoted: bool = False) -> TableAlterColumn:
5669
return self(
57-
name=name, is_struct=False, is_array_of_struct=False, is_array_of_primitive=True
70+
name=name,
71+
is_struct=False,
72+
is_array_of_struct=False,
73+
is_array_of_primitive=True,
74+
quoted=quoted,
5875
)
5976

6077
@classmethod
6178
def from_struct_kwarg(self, struct: exp.StructKwarg) -> TableAlterColumn:
6279
name = struct.alias_or_name
80+
quoted = struct.this.quoted
6381
if struct.expression.is_type(exp.DataType.Type.STRUCT):
64-
return self.struct(name)
82+
return self.struct(name, quoted=quoted)
6583
elif struct.expression.is_type(exp.DataType.Type.ARRAY):
6684
if struct.expression.expressions[0].is_type(exp.DataType.Type.STRUCT):
67-
return self.array_of_struct(name)
85+
return self.array_of_struct(name, quoted=quoted)
6886
else:
69-
return self.array_of_primitive(name)
87+
return self.array_of_primitive(name, quoted=quoted)
7088
else:
71-
return self.primitive(name)
89+
return self.primitive(name, quoted=quoted)
7290

7391
@property
7492
def is_array(self) -> bool:
@@ -82,23 +100,29 @@ def is_primitive(self) -> bool:
82100
def is_nested(self) -> bool:
83101
return not self.is_primitive
84102

103+
@property
104+
def identifier(self) -> exp.Identifier:
105+
return exp.to_identifier(self.name, quoted=self.quoted)
106+
85107

86108
class TableAlterColumnPosition(PydanticModel):
87109
is_first: bool
88110
is_last: bool
89-
after: t.Optional[str] = None
111+
after: t.Optional[exp.Identifier] = None
90112

91113
@classmethod
92114
def first(self) -> TableAlterColumnPosition:
93115
return self(is_first=True, is_last=False, after=None)
94116

95117
@classmethod
96-
def last(self, after: t.Optional[str] = None) -> TableAlterColumnPosition:
97-
return self(is_first=False, is_last=True, after=after)
118+
def last(
119+
self, after: t.Optional[t.Union[str, exp.Identifier]] = None
120+
) -> TableAlterColumnPosition:
121+
return self(is_first=False, is_last=True, after=exp.to_identifier(after) if after else None)
98122

99123
@classmethod
100-
def middle(self, after: str) -> TableAlterColumnPosition:
101-
return self(is_first=False, is_last=False, after=after)
124+
def middle(self, after: t.Union[str, exp.Identifier]) -> TableAlterColumnPosition:
125+
return self(is_first=False, is_last=False, after=exp.to_identifier(after))
102126

103127
@classmethod
104128
def create(
@@ -117,7 +141,7 @@ def create(
117141

118142
@property
119143
def column_position_node(self) -> t.Optional[exp.ColumnPosition]:
120-
column = exp.column(self.after) if self.after and not self.is_last else None
144+
column = self.after if not self.is_last else None
121145
position = None
122146
if self.is_first:
123147
position = "FIRST"
@@ -195,45 +219,49 @@ def is_drop(self) -> bool:
195219
def is_alter_type(self) -> bool:
196220
return self.op.is_alter_type
197221

198-
def full_column_path(self, array_suffix: str) -> str:
222+
def column_identifiers(self, array_element_selector: str) -> t.List[exp.Identifier]:
199223
results = []
200224
for column in self.columns:
201-
if column.is_array_of_struct and len(self.columns) > 1:
202-
results.append(column.name + array_suffix)
203-
else:
204-
results.append(column.name)
205-
return ".".join(results)
206-
207-
def column(self, array_suffix: str) -> exp.Column:
208-
return exp.column(self.full_column_path(array_suffix))
209-
210-
def column_def(self, array_suffix: str) -> exp.ColumnDef:
225+
results.append(column.identifier)
226+
if column.is_array_of_struct and len(self.columns) > 1 and array_element_selector:
227+
results.append(exp.to_identifier(array_element_selector))
228+
return results
229+
230+
def column(self, array_element_selector: str) -> t.Union[exp.Dot, exp.Identifier]:
231+
columns = self.column_identifiers(array_element_selector)
232+
if len(columns) == 1:
233+
return columns[0]
234+
return exp.Dot.build(columns)
235+
236+
def column_def(self, array_element_selector: str) -> exp.ColumnDef:
211237
return exp.ColumnDef(
212-
this=exp.to_identifier(self.full_column_path(array_suffix)),
238+
this=self.column(array_element_selector),
213239
kind=self.column_type,
214240
)
215241

216-
def expression(self, table_name: t.Union[str, exp.Table], array_suffix: str) -> exp.AlterTable:
242+
def expression(
243+
self, table_name: t.Union[str, exp.Table], array_element_selector: str
244+
) -> exp.AlterTable:
217245
if self.is_alter_type:
218246
return exp.AlterTable(
219247
this=exp.to_table(table_name),
220248
actions=[
221249
exp.AlterColumn(
222-
this=self.column(array_suffix),
250+
this=self.column(array_element_selector),
223251
dtype=self.column_type,
224252
)
225253
],
226254
)
227255
elif self.is_add:
228256
alter_table = exp.AlterTable(this=exp.to_table(table_name))
229-
column = self.column_def(array_suffix)
257+
column = self.column_def(array_element_selector)
230258
alter_table.set("actions", [column])
231259
if self.add_position:
232260
column.set("position", self.add_position.column_position_node)
233261
return alter_table
234262
elif self.is_drop:
235263
alter_table = exp.AlterTable(this=exp.to_table(table_name))
236-
drop_column = exp.Drop(this=self.column(array_suffix), kind="COLUMN")
264+
drop_column = exp.Drop(this=self.column(array_element_selector), kind="COLUMN")
237265
alter_table.set("actions", [drop_column])
238266
return alter_table
239267
else:
@@ -260,7 +288,7 @@ class SchemaDiffer(PydanticModel):
260288

261289
support_positional_add: bool = False
262290
support_nested_operations: bool = False
263-
array_suffix: str = ""
291+
array_element_selector: str = ""
264292
compatible_types: t.Dict[exp.DataType, t.Set[exp.DataType]] = {}
265293

266294
@classmethod
@@ -287,20 +315,20 @@ def _get_matching_kwarg(
287315
current_pos: int,
288316
) -> t.Tuple[t.Optional[int], t.Optional[exp.StructKwarg]]:
289317
current_name = (
290-
current_kwarg
318+
exp.to_identifier(current_kwarg)
291319
if isinstance(current_kwarg, str)
292320
else _get_name_and_type(current_kwarg)[0]
293321
)
294322
# First check if we have the same column in the same position to get O(1) complexity
295323
new_kwarg = seq_get(new_struct.expressions, current_pos)
296324
if new_kwarg:
297325
new_name, new_type = _get_name_and_type(new_kwarg)
298-
if current_name == new_name:
326+
if current_name.this == new_name.this:
299327
return current_pos, new_kwarg
300328
# If not, check if we have the same column in all positions with O(n) complexity
301329
for i, new_kwarg in enumerate(new_struct.expressions):
302330
new_name, new_type = _get_name_and_type(new_kwarg)
303-
if current_name == new_name:
331+
if current_name.this == new_name.this:
304332
return i, new_kwarg
305333
return None, None
306334

@@ -499,7 +527,8 @@ def compare_structs(
499527
The list of table alter operations.
500528
"""
501529
return [
502-
op.expression(table_name, self.array_suffix) for op in self._from_structs(current, new)
530+
op.expression(table_name, self.array_element_selector)
531+
for op in self._from_structs(current, new)
503532
]
504533

505534
def compare_columns(
@@ -523,5 +552,5 @@ def compare_columns(
523552
)
524553

525554

526-
def _get_name_and_type(struct: exp.StructKwarg) -> t.Tuple[str, exp.DataType]:
527-
return struct.alias_or_name, struct.expression
555+
def _get_name_and_type(struct: exp.StructKwarg) -> t.Tuple[exp.Identifier, exp.DataType]:
556+
return struct.this, struct.expression

sqlmesh/core/snapshot/definition.py

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -733,10 +733,7 @@ def serialize_hooks(hooks: t.List[HookCall]) -> t.Iterable[str]:
733733
name, args = hook
734734
serialized.append(
735735
f"{name}:"
736-
+ ",".join(
737-
f"{k}={v.sql(identify=True, comments=False)}"
738-
for k, v in sorted(args.items())
739-
)
736+
+ ",".join(f"{k}={v.sql(comments=False)}" for k, v in sorted(args.items()))
740737
)
741738
return serialized
742739

@@ -747,14 +744,14 @@ def serialize_hooks(hooks: t.List[HookCall]) -> t.Iterable[str]:
747744
model.storage_format,
748745
physical_schema,
749746
*(model.partitioned_by or []),
750-
*(expression.sql(identify=True, comments=False) for expression in model.expressions or []),
747+
*(expression.sql(comments=False) for expression in model.expressions or []),
751748
*serialize_hooks(model.pre),
752749
*serialize_hooks(model.post),
753750
model.stamp,
754751
]
755752

756753
if isinstance(model, SqlModel):
757-
data.append(model.query.sql(identify=True, comments=False))
754+
data.append(model.query.sql(comments=False))
758755

759756
for macro_name, macro in sorted(model.jinja_macros.root_macros.items(), key=lambda x: x[0]):
760757
data.append(macro_name)
@@ -800,13 +797,13 @@ def _model_metadata_hash(model: Model, audits: t.Dict[str, Audit]) -> str:
800797
if audit_name in BUILT_IN_AUDITS:
801798
for arg_name, arg_value in audit_args.items():
802799
metadata.append(arg_name)
803-
metadata.append(arg_value.sql(identify=True, comments=True))
800+
metadata.append(arg_value.sql(comments=True))
804801
elif audit_name in audits:
805802
audit = audits[audit_name]
806803
metadata.extend(
807804
[
808805
audit.render_query(model, **t.cast(t.Dict[str, t.Any], audit_args)).sql(
809-
identify=True, comments=True
806+
comments=True
810807
),
811808
audit.dialect,
812809
str(audit.skip),

0 commit comments

Comments
 (0)