fakesnow 0.9.6__py3-none-any.whl → 0.9.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fakesnow/fakes.py +83 -70
- fakesnow/transforms.py +329 -44
- {fakesnow-0.9.6.dist-info → fakesnow-0.9.8.dist-info}/METADATA +3 -4
- {fakesnow-0.9.6.dist-info → fakesnow-0.9.8.dist-info}/RECORD +8 -8
- {fakesnow-0.9.6.dist-info → fakesnow-0.9.8.dist-info}/LICENSE +0 -0
- {fakesnow-0.9.6.dist-info → fakesnow-0.9.8.dist-info}/WHEEL +0 -0
- {fakesnow-0.9.6.dist-info → fakesnow-0.9.8.dist-info}/entry_points.txt +0 -0
- {fakesnow-0.9.6.dist-info → fakesnow-0.9.8.dist-info}/top_level.txt +0 -0
fakesnow/fakes.py
CHANGED
@@ -11,6 +11,7 @@ from types import TracebackType
|
|
11
11
|
from typing import TYPE_CHECKING, Any, Literal, Optional, cast
|
12
12
|
|
13
13
|
import duckdb
|
14
|
+
from sqlglot import exp
|
14
15
|
|
15
16
|
if TYPE_CHECKING:
|
16
17
|
import pandas as pd
|
@@ -22,7 +23,7 @@ import sqlglot
|
|
22
23
|
from duckdb import DuckDBPyConnection
|
23
24
|
from snowflake.connector.cursor import DictCursor, ResultMetadata, SnowflakeCursor
|
24
25
|
from snowflake.connector.result_batch import ResultBatch
|
25
|
-
from sqlglot import
|
26
|
+
from sqlglot import parse_one
|
26
27
|
from typing_extensions import Self
|
27
28
|
|
28
29
|
import fakesnow.checks as checks
|
@@ -112,7 +113,9 @@ class FakeSnowflakeCursor:
|
|
112
113
|
def description(self) -> list[ResultMetadata]:
|
113
114
|
# use a separate cursor to avoid consuming the result set on this cursor
|
114
115
|
with self._conn.cursor() as cur:
|
115
|
-
|
116
|
+
# self._duck_conn.execute(sql, params)
|
117
|
+
expression = sqlglot.parse_one(f"DESCRIBE {self._last_sql}", read="duckdb")
|
118
|
+
cur._execute(expression, self._last_params) # noqa: SLF001
|
116
119
|
meta = FakeSnowflakeCursor._describe_as_result_metadata(cur.fetchall())
|
117
120
|
|
118
121
|
return meta
|
@@ -126,43 +129,20 @@ class FakeSnowflakeCursor:
|
|
126
129
|
) -> FakeSnowflakeCursor:
|
127
130
|
try:
|
128
131
|
self._sqlstate = None
|
129
|
-
|
132
|
+
|
133
|
+
if os.environ.get("FAKESNOW_DEBUG") == "snowflake":
|
134
|
+
print(f"{command};{params=}" if params else f"{command};", file=sys.stderr)
|
135
|
+
|
136
|
+
command, params = self._rewrite_with_params(command, params)
|
137
|
+
expression = parse_one(command, read="snowflake")
|
138
|
+
transformed = self._transform(expression)
|
139
|
+
return self._execute(transformed, params)
|
130
140
|
except snowflake.connector.errors.ProgrammingError as e:
|
131
141
|
self._sqlstate = e.sqlstate
|
132
142
|
raise e
|
133
143
|
|
134
|
-
def
|
135
|
-
|
136
|
-
command: str,
|
137
|
-
params: Sequence[Any] | dict[Any, Any] | None = None,
|
138
|
-
*args: Any,
|
139
|
-
**kwargs: Any,
|
140
|
-
) -> FakeSnowflakeCursor:
|
141
|
-
self._arrow_table = None
|
142
|
-
self._arrow_table_fetch_index = None
|
143
|
-
self._rowcount = None
|
144
|
-
|
145
|
-
command, params = self._rewrite_with_params(command, params)
|
146
|
-
expression = parse_one(command, read="snowflake")
|
147
|
-
|
148
|
-
cmd = expr.key_command(expression)
|
149
|
-
|
150
|
-
no_database, no_schema = checks.is_unqualified_table_expression(expression)
|
151
|
-
|
152
|
-
if no_database and not self._conn.database_set:
|
153
|
-
raise snowflake.connector.errors.ProgrammingError(
|
154
|
-
msg=f"Cannot perform {cmd}. This session does not have a current database. Call 'USE DATABASE', or use a qualified name.", # noqa: E501
|
155
|
-
errno=90105,
|
156
|
-
sqlstate="22000",
|
157
|
-
)
|
158
|
-
elif no_schema and not self._conn.schema_set:
|
159
|
-
raise snowflake.connector.errors.ProgrammingError(
|
160
|
-
msg=f"Cannot perform {cmd}. This session does not have a current schema. Call 'USE SCHEMA', or use a qualified name.", # noqa: E501
|
161
|
-
errno=90106,
|
162
|
-
sqlstate="22000",
|
163
|
-
)
|
164
|
-
|
165
|
-
transformed = (
|
144
|
+
def _transform(self, expression: exp.Expression) -> exp.Expression:
|
145
|
+
return (
|
166
146
|
expression.transform(transforms.upper_case_unquoted_identifiers)
|
167
147
|
.transform(transforms.set_schema, current_database=self._conn.database)
|
168
148
|
.transform(transforms.create_database, db_path=self._conn.db_path)
|
@@ -173,7 +153,9 @@ class FakeSnowflakeCursor:
|
|
173
153
|
.transform(transforms.drop_schema_cascade)
|
174
154
|
.transform(transforms.tag)
|
175
155
|
.transform(transforms.semi_structured_types)
|
176
|
-
.transform(transforms.
|
156
|
+
.transform(transforms.try_parse_json)
|
157
|
+
# NOTE: trim_cast_varchar must be before json_extract_cast_as_varchar
|
158
|
+
.transform(transforms.trim_cast_varchar)
|
177
159
|
# indices_to_json_extract must be before regex_substr
|
178
160
|
.transform(transforms.indices_to_json_extract)
|
179
161
|
.transform(transforms.json_extract_cast_as_varchar)
|
@@ -185,6 +167,7 @@ class FakeSnowflakeCursor:
|
|
185
167
|
.transform(transforms.values_columns)
|
186
168
|
.transform(transforms.to_date)
|
187
169
|
.transform(transforms.to_decimal)
|
170
|
+
.transform(transforms.try_to_decimal)
|
188
171
|
.transform(transforms.to_timestamp_ntz)
|
189
172
|
.transform(transforms.to_timestamp)
|
190
173
|
.transform(transforms.object_construct)
|
@@ -196,6 +179,11 @@ class FakeSnowflakeCursor:
|
|
196
179
|
.transform(transforms.array_size)
|
197
180
|
.transform(transforms.random)
|
198
181
|
.transform(transforms.identifier)
|
182
|
+
.transform(transforms.array_agg_within_group)
|
183
|
+
.transform(transforms.array_agg_to_json)
|
184
|
+
.transform(transforms.dateadd_date_cast)
|
185
|
+
.transform(transforms.dateadd_string_literal_timestamp_cast)
|
186
|
+
.transform(transforms.datediff_string_literal_timestamp_cast)
|
199
187
|
.transform(lambda e: transforms.show_schemas(e, self._conn.database))
|
200
188
|
.transform(lambda e: transforms.show_objects_tables(e, self._conn.database))
|
201
189
|
# TODO collapse into a single show_keys function
|
@@ -204,16 +192,42 @@ class FakeSnowflakeCursor:
|
|
204
192
|
.transform(lambda e: transforms.show_keys(e, self._conn.database, kind="FOREIGN"))
|
205
193
|
.transform(transforms.show_users)
|
206
194
|
.transform(transforms.create_user)
|
195
|
+
.transform(transforms.sha256)
|
207
196
|
)
|
197
|
+
|
198
|
+
def _execute(
|
199
|
+
self, transformed: exp.Expression, params: Sequence[Any] | dict[Any, Any] | None = None
|
200
|
+
) -> FakeSnowflakeCursor:
|
201
|
+
self._arrow_table = None
|
202
|
+
self._arrow_table_fetch_index = None
|
203
|
+
self._rowcount = None
|
204
|
+
|
205
|
+
cmd = expr.key_command(transformed)
|
206
|
+
|
207
|
+
no_database, no_schema = checks.is_unqualified_table_expression(transformed)
|
208
|
+
|
209
|
+
if no_database and not self._conn.database_set:
|
210
|
+
raise snowflake.connector.errors.ProgrammingError(
|
211
|
+
msg=f"Cannot perform {cmd}. This session does not have a current database. Call 'USE DATABASE', or use a qualified name.", # noqa: E501
|
212
|
+
errno=90105,
|
213
|
+
sqlstate="22000",
|
214
|
+
)
|
215
|
+
elif no_schema and not self._conn.schema_set:
|
216
|
+
raise snowflake.connector.errors.ProgrammingError(
|
217
|
+
msg=f"Cannot perform {cmd}. This session does not have a current schema. Call 'USE SCHEMA', or use a qualified name.", # noqa: E501
|
218
|
+
errno=90106,
|
219
|
+
sqlstate="22000",
|
220
|
+
)
|
221
|
+
|
208
222
|
sql = transformed.sql(dialect="duckdb")
|
209
|
-
result_sql = None
|
210
223
|
|
211
224
|
if transformed.find(exp.Select) and (seed := transformed.args.get("seed")):
|
212
225
|
sql = f"SELECT setseed({seed}); {sql}"
|
213
226
|
|
214
|
-
if fs_debug := os.environ.get("FAKESNOW_DEBUG"):
|
215
|
-
|
216
|
-
|
227
|
+
if (fs_debug := os.environ.get("FAKESNOW_DEBUG")) and fs_debug != "snowflake":
|
228
|
+
print(f"{sql};{params=}" if params else f"{sql};", file=sys.stderr)
|
229
|
+
|
230
|
+
result_sql = None
|
217
231
|
|
218
232
|
try:
|
219
233
|
self._duck_conn.execute(sql, params)
|
@@ -237,17 +251,12 @@ class FakeSnowflakeCursor:
|
|
237
251
|
|
238
252
|
affected_count = None
|
239
253
|
|
240
|
-
if
|
241
|
-
|
242
|
-
else:
|
243
|
-
ident = None
|
244
|
-
|
245
|
-
if cmd == "USE DATABASE" and ident:
|
246
|
-
self._conn.database = ident
|
254
|
+
if set_database := transformed.args.get("set_database"):
|
255
|
+
self._conn.database = set_database
|
247
256
|
self._conn.database_set = True
|
248
257
|
|
249
|
-
elif
|
250
|
-
self._conn.schema =
|
258
|
+
elif set_schema := transformed.args.get("set_schema"):
|
259
|
+
self._conn.schema = set_schema
|
251
260
|
self._conn.schema_set = True
|
252
261
|
|
253
262
|
elif create_db_name := transformed.args.get("create_db_name"):
|
@@ -255,26 +264,6 @@ class FakeSnowflakeCursor:
|
|
255
264
|
self._duck_conn.execute(info_schema.creation_sql(create_db_name))
|
256
265
|
result_sql = SQL_CREATED_DATABASE.substitute(name=create_db_name)
|
257
266
|
|
258
|
-
elif cmd == "CREATE SCHEMA" and ident:
|
259
|
-
result_sql = SQL_CREATED_SCHEMA.substitute(name=ident)
|
260
|
-
|
261
|
-
elif cmd == "CREATE TABLE" and ident:
|
262
|
-
result_sql = SQL_CREATED_TABLE.substitute(name=ident)
|
263
|
-
|
264
|
-
elif cmd == "CREATE VIEW" and ident:
|
265
|
-
result_sql = SQL_CREATED_VIEW.substitute(name=ident)
|
266
|
-
|
267
|
-
elif cmd.startswith("DROP") and ident:
|
268
|
-
result_sql = SQL_DROPPED.substitute(name=ident)
|
269
|
-
|
270
|
-
# if dropping the current database/schema then reset conn metadata
|
271
|
-
if cmd == "DROP DATABASE" and ident == self._conn.database:
|
272
|
-
self._conn.database = None
|
273
|
-
self._conn.schema = None
|
274
|
-
|
275
|
-
elif cmd == "DROP SCHEMA" and ident == self._conn.schema:
|
276
|
-
self._conn.schema = None
|
277
|
-
|
278
267
|
elif cmd == "INSERT":
|
279
268
|
(affected_count,) = self._duck_conn.fetchall()[0]
|
280
269
|
result_sql = SQL_INSERTED_ROWS.substitute(count=affected_count)
|
@@ -294,6 +283,28 @@ class FakeSnowflakeCursor:
|
|
294
283
|
lambda e: transforms.describe_table(e, self._conn.database, self._conn.schema)
|
295
284
|
).sql(dialect="duckdb")
|
296
285
|
|
286
|
+
elif (eid := transformed.find(exp.Identifier, bfs=False)) and isinstance(eid.this, str):
|
287
|
+
ident = eid.this if eid.quoted else eid.this.upper()
|
288
|
+
if cmd == "CREATE SCHEMA" and ident:
|
289
|
+
result_sql = SQL_CREATED_SCHEMA.substitute(name=ident)
|
290
|
+
|
291
|
+
elif cmd == "CREATE TABLE" and ident:
|
292
|
+
result_sql = SQL_CREATED_TABLE.substitute(name=ident)
|
293
|
+
|
294
|
+
elif cmd == "CREATE VIEW" and ident:
|
295
|
+
result_sql = SQL_CREATED_VIEW.substitute(name=ident)
|
296
|
+
|
297
|
+
elif cmd.startswith("DROP") and ident:
|
298
|
+
result_sql = SQL_DROPPED.substitute(name=ident)
|
299
|
+
|
300
|
+
# if dropping the current database/schema then reset conn metadata
|
301
|
+
if cmd == "DROP DATABASE" and ident == self._conn.database:
|
302
|
+
self._conn.database = None
|
303
|
+
self._conn.schema = None
|
304
|
+
|
305
|
+
elif cmd == "DROP SCHEMA" and ident == self._conn.schema:
|
306
|
+
self._conn.schema = None
|
307
|
+
|
297
308
|
if table_comment := cast(tuple[exp.Table, str], transformed.args.get("table_comment")):
|
298
309
|
# record table comment
|
299
310
|
table, comment = table_comment
|
@@ -617,7 +628,9 @@ class FakeSnowflakeConnection:
|
|
617
628
|
# don't jsonify string
|
618
629
|
df[col] = df[col].apply(lambda x: json.dumps(x) if isinstance(x, (dict, list)) else x)
|
619
630
|
|
620
|
-
|
631
|
+
escaped_cols = ",".join(f'"{col}"' for col in df.columns.to_list())
|
632
|
+
self._duck_conn.execute(f"INSERT INTO {table_name}({escaped_cols}) SELECT * FROM df")
|
633
|
+
|
621
634
|
return self._duck_conn.fetchall()[0][0]
|
622
635
|
|
623
636
|
|
fakesnow/transforms.py
CHANGED
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|
2
2
|
|
3
3
|
from pathlib import Path
|
4
4
|
from string import Template
|
5
|
-
from typing import Literal, cast
|
5
|
+
from typing import ClassVar, Literal, cast
|
6
6
|
|
7
7
|
import sqlglot
|
8
8
|
from sqlglot import exp
|
@@ -22,6 +22,39 @@ def array_size(expression: exp.Expression) -> exp.Expression:
|
|
22
22
|
return expression
|
23
23
|
|
24
24
|
|
25
|
+
def array_agg_to_json(expression: exp.Expression) -> exp.Expression:
|
26
|
+
if isinstance(expression, exp.ArrayAgg):
|
27
|
+
return exp.Anonymous(this="TO_JSON", expressions=[expression])
|
28
|
+
|
29
|
+
return expression
|
30
|
+
|
31
|
+
|
32
|
+
def array_agg_within_group(expression: exp.Expression) -> exp.Expression:
|
33
|
+
"""Convert ARRAY_AGG(<expr>) WITHIN GROUP (<order-by-clause>) to ARRAY_AGG( <expr> <order-by-clause> )
|
34
|
+
Snowflake uses ARRAY_AGG(<expr>) WITHIN GROUP (ORDER BY <order-by-clause>)
|
35
|
+
to order the array, but DuckDB uses ARRAY_AGG( <expr> <order-by-clause> ).
|
36
|
+
See;
|
37
|
+
- https://docs.snowflake.com/en/sql-reference/functions/array_agg
|
38
|
+
- https://duckdb.org/docs/sql/aggregates.html#order-by-clause-in-aggregate-functions
|
39
|
+
Note; Snowflake has following restriction;
|
40
|
+
If you specify DISTINCT and WITHIN GROUP, both must refer to the same column.
|
41
|
+
Transformation does not handle this restriction.
|
42
|
+
"""
|
43
|
+
if (
|
44
|
+
isinstance(expression, exp.WithinGroup)
|
45
|
+
and (agg := expression.find(exp.ArrayAgg))
|
46
|
+
and (order := expression.expression)
|
47
|
+
):
|
48
|
+
return exp.ArrayAgg(
|
49
|
+
this=exp.Order(
|
50
|
+
this=agg.this,
|
51
|
+
expressions=order.expressions,
|
52
|
+
)
|
53
|
+
)
|
54
|
+
|
55
|
+
return expression
|
56
|
+
|
57
|
+
|
25
58
|
# TODO: move this into a Dialect as a transpilation
|
26
59
|
def create_database(expression: exp.Expression, db_path: Path | None = None) -> exp.Expression:
|
27
60
|
"""Transform create database to attach database.
|
@@ -136,6 +169,98 @@ def drop_schema_cascade(expression: exp.Expression) -> exp.Expression:
|
|
136
169
|
return new
|
137
170
|
|
138
171
|
|
172
|
+
def dateadd_date_cast(expression: exp.Expression) -> exp.Expression:
|
173
|
+
"""Cast result of DATEADD to DATE if the given expression is a cast to DATE
|
174
|
+
and unit is either DAY, WEEK, MONTH or YEAR to mimic Snowflake's DATEADD
|
175
|
+
behaviour.
|
176
|
+
|
177
|
+
Snowflake;
|
178
|
+
SELECT DATEADD(DAY, 3, '2023-03-03'::DATE) as D;
|
179
|
+
D: 2023-03-06 (DATE)
|
180
|
+
DuckDB;
|
181
|
+
SELECT CAST('2023-03-03' AS DATE) + INTERVAL 3 DAY AS D
|
182
|
+
D: 2023-03-06 00:00:00 (TIMESTAMP)
|
183
|
+
"""
|
184
|
+
|
185
|
+
if not isinstance(expression, exp.DateAdd):
|
186
|
+
return expression
|
187
|
+
|
188
|
+
if expression.unit is None:
|
189
|
+
return expression
|
190
|
+
|
191
|
+
if not isinstance(expression.unit.this, str):
|
192
|
+
return expression
|
193
|
+
|
194
|
+
if (unit := expression.unit.this.upper()) and unit.upper() not in {"DAY", "WEEK", "MONTH", "YEAR"}:
|
195
|
+
return expression
|
196
|
+
|
197
|
+
if not isinstance(expression.this, exp.Cast):
|
198
|
+
return expression
|
199
|
+
|
200
|
+
if expression.this.to.this != exp.DataType.Type.DATE:
|
201
|
+
return expression
|
202
|
+
|
203
|
+
return exp.Cast(
|
204
|
+
this=expression,
|
205
|
+
to=exp.DataType(this=exp.DataType.Type.DATE, nested=False, prefix=False),
|
206
|
+
)
|
207
|
+
|
208
|
+
|
209
|
+
def dateadd_string_literal_timestamp_cast(expression: exp.Expression) -> exp.Expression:
|
210
|
+
"""Snowflake's DATEADD function implicitly casts string literals to
|
211
|
+
timestamps regardless of unit.
|
212
|
+
"""
|
213
|
+
if not isinstance(expression, exp.DateAdd):
|
214
|
+
return expression
|
215
|
+
|
216
|
+
if not isinstance(expression.this, exp.Literal) or not expression.this.is_string:
|
217
|
+
return expression
|
218
|
+
|
219
|
+
new_dateadd = expression.copy()
|
220
|
+
new_dateadd.set(
|
221
|
+
"this",
|
222
|
+
exp.Cast(
|
223
|
+
this=expression.this,
|
224
|
+
# TODO: support TIMESTAMP_TYPE_MAPPING of TIMESTAMP_LTZ/TZ
|
225
|
+
to=exp.DataType(this=exp.DataType.Type.TIMESTAMP, nested=False, prefix=False),
|
226
|
+
),
|
227
|
+
)
|
228
|
+
|
229
|
+
return new_dateadd
|
230
|
+
|
231
|
+
|
232
|
+
def datediff_string_literal_timestamp_cast(expression: exp.Expression) -> exp.Expression:
|
233
|
+
"""Snowflake's DATEDIFF function implicitly casts string literals to
|
234
|
+
timestamps regardless of unit.
|
235
|
+
"""
|
236
|
+
|
237
|
+
if not isinstance(expression, exp.DateDiff):
|
238
|
+
return expression
|
239
|
+
|
240
|
+
op1 = expression.this.copy()
|
241
|
+
op2 = expression.expression.copy()
|
242
|
+
|
243
|
+
if isinstance(op1, exp.Literal) and op1.is_string:
|
244
|
+
op1 = exp.Cast(
|
245
|
+
this=op1,
|
246
|
+
# TODO: support TIMESTAMP_TYPE_MAPPING of TIMESTAMP_LTZ/TZ
|
247
|
+
to=exp.DataType(this=exp.DataType.Type.TIMESTAMP, nested=False, prefix=False),
|
248
|
+
)
|
249
|
+
|
250
|
+
if isinstance(op2, exp.Literal) and op2.is_string:
|
251
|
+
op2 = exp.Cast(
|
252
|
+
this=op2,
|
253
|
+
# TODO: support TIMESTAMP_TYPE_MAPPING of TIMESTAMP_LTZ/TZ
|
254
|
+
to=exp.DataType(this=exp.DataType.Type.TIMESTAMP, nested=False, prefix=False),
|
255
|
+
)
|
256
|
+
|
257
|
+
new_datediff = expression.copy()
|
258
|
+
new_datediff.set("this", op1)
|
259
|
+
new_datediff.set("expression", op2)
|
260
|
+
|
261
|
+
return new_datediff
|
262
|
+
|
263
|
+
|
139
264
|
def extract_comment_on_columns(expression: exp.Expression) -> exp.Expression:
|
140
265
|
"""Extract column comments, removing it from the Expression.
|
141
266
|
|
@@ -441,6 +566,9 @@ def json_extract_cast_as_varchar(expression: exp.Expression) -> exp.Expression:
|
|
441
566
|
"""
|
442
567
|
if (
|
443
568
|
isinstance(expression, exp.Cast)
|
569
|
+
and (to := expression.to)
|
570
|
+
and isinstance(to, exp.DataType)
|
571
|
+
and to.this in {exp.DataType.Type.VARCHAR, exp.DataType.Type.TEXT}
|
444
572
|
and (je := expression.this)
|
445
573
|
and isinstance(je, exp.JSONExtract)
|
446
574
|
and (path := je.expression)
|
@@ -455,7 +583,7 @@ def json_extract_precedence(expression: exp.Expression) -> exp.Expression:
|
|
455
583
|
|
456
584
|
See https://github.com/tekumara/fakesnow/issues/53
|
457
585
|
"""
|
458
|
-
if isinstance(expression, exp.JSONExtract):
|
586
|
+
if isinstance(expression, (exp.JSONExtract, exp.JSONExtractScalar)):
|
459
587
|
return exp.Paren(this=expression)
|
460
588
|
return expression
|
461
589
|
|
@@ -508,38 +636,26 @@ def object_construct(expression: exp.Expression) -> exp.Expression:
|
|
508
636
|
"""
|
509
637
|
|
510
638
|
if isinstance(expression, exp.Struct):
|
511
|
-
|
512
|
-
for
|
513
|
-
if
|
514
|
-
|
639
|
+
non_null_expressions = []
|
640
|
+
for e in expression.expressions:
|
641
|
+
if not (isinstance(e, exp.PropertyEQ)):
|
642
|
+
non_null_expressions.append(e)
|
643
|
+
continue
|
515
644
|
|
516
|
-
|
645
|
+
left = e.left
|
646
|
+
right = e.right
|
517
647
|
|
518
|
-
|
648
|
+
left_is_null = isinstance(left, exp.Null)
|
649
|
+
right_is_null = isinstance(right, exp.Null)
|
519
650
|
|
651
|
+
if left_is_null or right_is_null:
|
652
|
+
continue
|
520
653
|
|
521
|
-
|
522
|
-
"""Convert parse_json() to json().
|
654
|
+
non_null_expressions.append(e)
|
523
655
|
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
"CREATE TABLE table1 (name JSON)"
|
528
|
-
Args:
|
529
|
-
expression (exp.Expression): the expression that will be transformed.
|
530
|
-
|
531
|
-
Returns:
|
532
|
-
exp.Expression: The transformed expression.
|
533
|
-
"""
|
534
|
-
|
535
|
-
if (
|
536
|
-
isinstance(expression, exp.Anonymous)
|
537
|
-
and isinstance(expression.this, str)
|
538
|
-
and expression.this.upper() == "PARSE_JSON"
|
539
|
-
):
|
540
|
-
new = expression.copy()
|
541
|
-
new.args["this"] = "JSON"
|
542
|
-
return new
|
656
|
+
new_struct = expression.copy()
|
657
|
+
new_struct.set("expressions", non_null_expressions)
|
658
|
+
return exp.Anonymous(this="TO_JSON", expressions=[new_struct])
|
543
659
|
|
544
660
|
return expression
|
545
661
|
|
@@ -666,7 +782,10 @@ def set_schema(expression: exp.Expression, current_database: str | None) -> exp.
|
|
666
782
|
|
667
783
|
if kind.name.upper() == "DATABASE":
|
668
784
|
# duckdb's default schema is main
|
669
|
-
|
785
|
+
database = expression.this.name
|
786
|
+
return exp.Command(
|
787
|
+
this="SET", expression=exp.Literal.string(f"schema = '{database}.main'"), set_database=database
|
788
|
+
)
|
670
789
|
else:
|
671
790
|
# SCHEMA
|
672
791
|
if db := expression.this.args.get("db"): # noqa: SIM108
|
@@ -675,9 +794,10 @@ def set_schema(expression: exp.Expression, current_database: str | None) -> exp.
|
|
675
794
|
# isn't qualified with a database
|
676
795
|
db_name = current_database or MISSING_DATABASE
|
677
796
|
|
678
|
-
|
679
|
-
|
680
|
-
|
797
|
+
schema = expression.this.name
|
798
|
+
return exp.Command(
|
799
|
+
this="SET", expression=exp.Literal.string(f"schema = '{db_name}.{schema}'"), set_schema=schema
|
800
|
+
)
|
681
801
|
|
682
802
|
return expression
|
683
803
|
|
@@ -829,30 +949,107 @@ def to_date(expression: exp.Expression) -> exp.Expression:
|
|
829
949
|
return expression
|
830
950
|
|
831
951
|
|
952
|
+
def _get_to_number_args(e: exp.ToNumber) -> tuple[exp.Expression | None, exp.Expression | None, exp.Expression | None]:
|
953
|
+
arg_format = e.args.get("format")
|
954
|
+
arg_precision = e.args.get("precision")
|
955
|
+
arg_scale = e.args.get("scale")
|
956
|
+
|
957
|
+
_format = None
|
958
|
+
_precision = None
|
959
|
+
_scale = None
|
960
|
+
|
961
|
+
# to_number(value, <format>, <precision>, <scale>)
|
962
|
+
if arg_format:
|
963
|
+
if arg_format.is_string:
|
964
|
+
# to_number('100', 'TM9' ...)
|
965
|
+
_format = arg_format
|
966
|
+
|
967
|
+
# to_number('100', 'TM9', 10 ...)
|
968
|
+
if arg_precision:
|
969
|
+
_precision = arg_precision
|
970
|
+
|
971
|
+
# to_number('100', 'TM9', 10, 2)
|
972
|
+
if arg_scale:
|
973
|
+
_scale = arg_scale
|
974
|
+
else:
|
975
|
+
pass
|
976
|
+
else:
|
977
|
+
# to_number('100', 10, ...)
|
978
|
+
# arg_format is not a string, so it must be precision.
|
979
|
+
_precision = arg_format
|
980
|
+
|
981
|
+
# to_number('100', 10, 2)
|
982
|
+
# And arg_precision must be scale
|
983
|
+
if arg_precision:
|
984
|
+
_scale = arg_precision
|
985
|
+
else:
|
986
|
+
# If format is not provided, just check for precision and scale directly
|
987
|
+
if arg_precision:
|
988
|
+
_precision = arg_precision
|
989
|
+
if arg_scale:
|
990
|
+
_scale = arg_scale
|
991
|
+
|
992
|
+
return _format, _precision, _scale
|
993
|
+
|
994
|
+
|
995
|
+
def _to_decimal(expression: exp.Expression, cast_node: type[exp.Cast]) -> exp.Expression:
|
996
|
+
expressions: list[exp.Expression] = expression.expressions
|
997
|
+
|
998
|
+
if len(expressions) > 1 and expressions[1].is_string:
|
999
|
+
# see https://docs.snowflake.com/en/sql-reference/functions/to_decimal#arguments
|
1000
|
+
raise NotImplementedError(f"{expression.this} with format argument")
|
1001
|
+
|
1002
|
+
precision = expressions[1] if len(expressions) > 1 else exp.Literal(this="38", is_string=False)
|
1003
|
+
scale = expressions[2] if len(expressions) > 2 else exp.Literal(this="0", is_string=False)
|
1004
|
+
|
1005
|
+
return cast_node(
|
1006
|
+
this=expressions[0],
|
1007
|
+
to=exp.DataType(this=exp.DataType.Type.DECIMAL, expressions=[precision, scale], nested=False, prefix=False),
|
1008
|
+
)
|
1009
|
+
|
1010
|
+
|
832
1011
|
def to_decimal(expression: exp.Expression) -> exp.Expression:
|
833
1012
|
"""Transform to_decimal, to_number, to_numeric expressions from snowflake to duckdb.
|
834
1013
|
|
835
1014
|
See https://docs.snowflake.com/en/sql-reference/functions/to_decimal
|
836
1015
|
"""
|
837
1016
|
|
1017
|
+
if isinstance(expression, exp.ToNumber):
|
1018
|
+
format_, precision, scale = _get_to_number_args(expression)
|
1019
|
+
if format_:
|
1020
|
+
raise NotImplementedError(f"{expression.this} with format argument")
|
1021
|
+
|
1022
|
+
if not precision:
|
1023
|
+
precision = exp.Literal(this="38", is_string=False)
|
1024
|
+
if not scale:
|
1025
|
+
scale = exp.Literal(this="0", is_string=False)
|
1026
|
+
|
1027
|
+
return exp.Cast(
|
1028
|
+
this=expression.this,
|
1029
|
+
to=exp.DataType(this=exp.DataType.Type.DECIMAL, expressions=[precision, scale], nested=False, prefix=False),
|
1030
|
+
)
|
1031
|
+
|
838
1032
|
if (
|
839
1033
|
isinstance(expression, exp.Anonymous)
|
840
1034
|
and isinstance(expression.this, str)
|
841
|
-
and expression.this.upper() in ["TO_DECIMAL", "
|
1035
|
+
and expression.this.upper() in ["TO_DECIMAL", "TO_NUMERIC"]
|
842
1036
|
):
|
843
|
-
|
1037
|
+
return _to_decimal(expression, exp.Cast)
|
844
1038
|
|
845
|
-
|
846
|
-
# see https://docs.snowflake.com/en/sql-reference/functions/to_decimal#arguments
|
847
|
-
raise NotImplementedError(f"{expression.this} with format argument")
|
1039
|
+
return expression
|
848
1040
|
|
849
|
-
precision = expressions[1] if len(expressions) > 1 else exp.Literal(this="38", is_string=False)
|
850
|
-
scale = expressions[2] if len(expressions) > 2 else exp.Literal(this="0", is_string=False)
|
851
1041
|
|
852
|
-
|
853
|
-
|
854
|
-
|
855
|
-
|
1042
|
+
def try_to_decimal(expression: exp.Expression) -> exp.Expression:
|
1043
|
+
"""Transform try_to_decimal, try_to_number, try_to_numeric expressions from snowflake to duckdb.
|
1044
|
+
See https://docs.snowflake.com/en/sql-reference/functions/try_to_decimal
|
1045
|
+
"""
|
1046
|
+
|
1047
|
+
if (
|
1048
|
+
isinstance(expression, exp.Anonymous)
|
1049
|
+
and isinstance(expression.this, str)
|
1050
|
+
and expression.this.upper() in ["TRY_TO_DECIMAL", "TRY_TO_NUMBER", "TRY_TO_NUMERIC"]
|
1051
|
+
):
|
1052
|
+
return _to_decimal(expression, exp.TryCast)
|
856
1053
|
|
857
1054
|
return expression
|
858
1055
|
|
@@ -905,6 +1102,49 @@ def timestamp_ntz_ns(expression: exp.Expression) -> exp.Expression:
|
|
905
1102
|
return expression
|
906
1103
|
|
907
1104
|
|
1105
|
+
def trim_cast_varchar(expression: exp.Expression) -> exp.Expression:
|
1106
|
+
"""Snowflake's TRIM casts input to VARCHAR implicitly."""
|
1107
|
+
|
1108
|
+
if not (isinstance(expression, exp.Trim)):
|
1109
|
+
return expression
|
1110
|
+
|
1111
|
+
operand = expression.this
|
1112
|
+
if isinstance(operand, exp.Cast) and operand.to.this in [exp.DataType.Type.VARCHAR, exp.DataType.Type.TEXT]:
|
1113
|
+
return expression
|
1114
|
+
|
1115
|
+
return exp.Trim(
|
1116
|
+
this=exp.Cast(this=operand, to=exp.DataType(this=exp.DataType.Type.VARCHAR, nested=False, prefix=False))
|
1117
|
+
)
|
1118
|
+
|
1119
|
+
|
1120
|
+
def try_parse_json(expression: exp.Expression) -> exp.Expression:
|
1121
|
+
"""Convert TRY_PARSE_JSON() to TRY_CAST(... as JSON).
|
1122
|
+
|
1123
|
+
Example:
|
1124
|
+
>>> import sqlglot
|
1125
|
+
>>> sqlglot.parse_one("select try_parse_json('{}')").transform(parse_json).sql()
|
1126
|
+
"SELECT TRY_CAST('{}' AS JSON)"
|
1127
|
+
Args:
|
1128
|
+
expression (exp.Expression): the expression that will be transformed.
|
1129
|
+
|
1130
|
+
Returns:
|
1131
|
+
exp.Expression: The transformed expression.
|
1132
|
+
"""
|
1133
|
+
|
1134
|
+
if (
|
1135
|
+
isinstance(expression, exp.Anonymous)
|
1136
|
+
and isinstance(expression.this, str)
|
1137
|
+
and expression.this.upper() == "TRY_PARSE_JSON"
|
1138
|
+
):
|
1139
|
+
expressions = expression.expressions
|
1140
|
+
return exp.TryCast(
|
1141
|
+
this=expressions[0],
|
1142
|
+
to=exp.DataType(this=exp.DataType.Type.JSON, nested=False),
|
1143
|
+
)
|
1144
|
+
|
1145
|
+
return expression
|
1146
|
+
|
1147
|
+
|
908
1148
|
# sqlglot.parse_one("create table example(date TIMESTAMP_NTZ(9));", read="snowflake")
|
909
1149
|
def semi_structured_types(expression: exp.Expression) -> exp.Expression:
|
910
1150
|
"""Convert OBJECT, ARRAY, and VARIANT types to duckdb compatible types.
|
@@ -1087,3 +1327,48 @@ def show_keys(
|
|
1087
1327
|
raise NotImplementedError(f"SHOW PRIMARY KEYS with {scope_kind} not yet supported")
|
1088
1328
|
return sqlglot.parse_one(statement)
|
1089
1329
|
return expression
|
1330
|
+
|
1331
|
+
|
1332
|
+
class SHA256(exp.Func):
|
1333
|
+
_sql_names: ClassVar = ["SHA256"]
|
1334
|
+
arg_types: ClassVar = {"this": True}
|
1335
|
+
|
1336
|
+
|
1337
|
+
def sha256(expression: exp.Expression) -> exp.Expression:
|
1338
|
+
"""Convert sha2() or sha2_hex() to sha256().
|
1339
|
+
|
1340
|
+
Convert sha2_binary() to unhex(sha256()).
|
1341
|
+
|
1342
|
+
Example:
|
1343
|
+
>>> import sqlglot
|
1344
|
+
>>> sqlglot.parse_one("insert into table1 (name) select sha2('foo')").transform(sha256).sql()
|
1345
|
+
"INSERT INTO table1 (name) SELECT SHA256('foo')"
|
1346
|
+
Args:
|
1347
|
+
expression (exp.Expression): the expression that will be transformed.
|
1348
|
+
|
1349
|
+
Returns:
|
1350
|
+
exp.Expression: The transformed expression.
|
1351
|
+
"""
|
1352
|
+
|
1353
|
+
if isinstance(expression, exp.SHA2) and expression.args.get("length", exp.Literal.number(256)).this == "256":
|
1354
|
+
return SHA256(this=expression.this)
|
1355
|
+
elif (
|
1356
|
+
isinstance(expression, exp.Anonymous)
|
1357
|
+
and expression.this.upper() == "SHA2_HEX"
|
1358
|
+
and (
|
1359
|
+
len(expression.expressions) == 1
|
1360
|
+
or (len(expression.expressions) == 2 and expression.expressions[1].this == "256")
|
1361
|
+
)
|
1362
|
+
):
|
1363
|
+
return SHA256(this=expression.expressions[0])
|
1364
|
+
elif (
|
1365
|
+
isinstance(expression, exp.Anonymous)
|
1366
|
+
and expression.this.upper() == "SHA2_BINARY"
|
1367
|
+
and (
|
1368
|
+
len(expression.expressions) == 1
|
1369
|
+
or (len(expression.expressions) == 2 and expression.expressions[1].this == "256")
|
1370
|
+
)
|
1371
|
+
):
|
1372
|
+
return exp.Unhex(this=SHA256(this=expression.expressions[0]))
|
1373
|
+
|
1374
|
+
return expression
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: fakesnow
|
3
|
-
Version: 0.9.
|
3
|
+
Version: 0.9.8
|
4
4
|
Summary: Fake Snowflake Connector for Python. Run, mock and test Snowflake DB locally.
|
5
5
|
License: Apache License
|
6
6
|
Version 2.0, January 2004
|
@@ -213,7 +213,7 @@ License-File: LICENSE
|
|
213
213
|
Requires-Dist: duckdb ~=0.10.0
|
214
214
|
Requires-Dist: pyarrow
|
215
215
|
Requires-Dist: snowflake-connector-python
|
216
|
-
Requires-Dist: sqlglot ~=
|
216
|
+
Requires-Dist: sqlglot ~=23.3.0
|
217
217
|
Provides-Extra: dev
|
218
218
|
Requires-Dist: build ~=1.0 ; extra == 'dev'
|
219
219
|
Requires-Dist: pandas-stubs ; extra == 'dev'
|
@@ -233,8 +233,7 @@ Requires-Dist: jupysql ; extra == 'notebook'
|
|
233
233
|
[](https://github.com/tekumara/fakesnow/actions/workflows/ci.yml)
|
234
234
|
[](https://github.com/tekumara/fakesnow/actions/workflows/release.yml)
|
235
235
|
[](https://pypi.org/project/fakesnow/)
|
236
|
-
|
237
|
-
[](../../actions/workflows/ci.yml)
|
236
|
+
[](https://pypi.org/project/fakesnow/)
|
238
237
|
|
239
238
|
Fake [Snowflake Connector for Python](https://docs.snowflake.com/en/user-guide/python-connector). Run and mock Snowflake DB locally.
|
240
239
|
|
@@ -3,16 +3,16 @@ fakesnow/__main__.py,sha256=GDrGyNTvBFuqn_UfDjKs7b3LPtU6gDv1KwosVDrukIM,76
|
|
3
3
|
fakesnow/checks.py,sha256=-QMvdcrRbhN60rnzxLBJ0IkUBWyLR8gGGKKmCS0w9mA,2383
|
4
4
|
fakesnow/cli.py,sha256=9qfI-Ssr6mo8UmIlXkUAOz2z2YPBgDsrEVaZv9FjGFs,2201
|
5
5
|
fakesnow/expr.py,sha256=CAxuYIUkwI339DQIBzvFF0F-m1tcVGKEPA5rDTzmH9A,892
|
6
|
-
fakesnow/fakes.py,sha256=
|
6
|
+
fakesnow/fakes.py,sha256=k9i3xohKfgS55ABexd06ubqtTDsc36asoSGv_kzzdxg,29442
|
7
7
|
fakesnow/fixtures.py,sha256=G-NkVeruSQAJ7fvSS2fR2oysUn0Yra1pohHlOvacKEk,455
|
8
8
|
fakesnow/global_database.py,sha256=WTVIP1VhNvdCeX7TQncX1TRpGQU5rBf5Pbxim40zeSU,1399
|
9
9
|
fakesnow/info_schema.py,sha256=CdIcGXHEQ_kmEAzdQKvA-PX41LA6wlK-4p1J45qgKYA,6266
|
10
10
|
fakesnow/macros.py,sha256=pX1YJDnQOkFJSHYUjQ6ErEkYIKvFI6Ncz_au0vv1csA,265
|
11
11
|
fakesnow/py.typed,sha256=B-DLSjYBi7pkKjwxCSdpVj2J02wgfJr-E7B1wOUyxYU,80
|
12
|
-
fakesnow/transforms.py,sha256=
|
13
|
-
fakesnow-0.9.
|
14
|
-
fakesnow-0.9.
|
15
|
-
fakesnow-0.9.
|
16
|
-
fakesnow-0.9.
|
17
|
-
fakesnow-0.9.
|
18
|
-
fakesnow-0.9.
|
12
|
+
fakesnow/transforms.py,sha256=s-RvjTWBJdFIzfxrz2Qcub19yfTludBJeJ8KSdOZJYA,49714
|
13
|
+
fakesnow-0.9.8.dist-info/LICENSE,sha256=kW-7NWIyaRMQiDpryfSmF2DObDZHGR1cJZ39s6B1Svg,11344
|
14
|
+
fakesnow-0.9.8.dist-info/METADATA,sha256=QjIA4H99CCnfzoo2qBhg5EJAq8wFp4kbwXWDuTqV1CQ,17831
|
15
|
+
fakesnow-0.9.8.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
16
|
+
fakesnow-0.9.8.dist-info/entry_points.txt,sha256=2riAUgu928ZIHawtO8EsfrMEJhi-EH-z_Vq7Q44xKPM,47
|
17
|
+
fakesnow-0.9.8.dist-info/top_level.txt,sha256=500evXI1IFX9so82cizGIEMHAb_dJNPaZvd2H9dcKTA,24
|
18
|
+
fakesnow-0.9.8.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|