fakesnow 0.9.7__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 +72 -68
- fakesnow/transforms.py +27 -5
- {fakesnow-0.9.7.dist-info → fakesnow-0.9.8.dist-info}/METADATA +1 -1
- {fakesnow-0.9.7.dist-info → fakesnow-0.9.8.dist-info}/RECORD +8 -8
- {fakesnow-0.9.7.dist-info → fakesnow-0.9.8.dist-info}/LICENSE +0 -0
- {fakesnow-0.9.7.dist-info → fakesnow-0.9.8.dist-info}/WHEEL +0 -0
- {fakesnow-0.9.7.dist-info → fakesnow-0.9.8.dist-info}/entry_points.txt +0 -0
- {fakesnow-0.9.7.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)
|
@@ -174,6 +154,8 @@ class FakeSnowflakeCursor:
|
|
174
154
|
.transform(transforms.tag)
|
175
155
|
.transform(transforms.semi_structured_types)
|
176
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)
|
@@ -212,15 +194,40 @@ class FakeSnowflakeCursor:
|
|
212
194
|
.transform(transforms.create_user)
|
213
195
|
.transform(transforms.sha256)
|
214
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
|
+
|
215
222
|
sql = transformed.sql(dialect="duckdb")
|
216
|
-
result_sql = None
|
217
223
|
|
218
224
|
if transformed.find(exp.Select) and (seed := transformed.args.get("seed")):
|
219
225
|
sql = f"SELECT setseed({seed}); {sql}"
|
220
226
|
|
221
|
-
if fs_debug := os.environ.get("FAKESNOW_DEBUG"):
|
222
|
-
|
223
|
-
|
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
|
224
231
|
|
225
232
|
try:
|
226
233
|
self._duck_conn.execute(sql, params)
|
@@ -244,17 +251,12 @@ class FakeSnowflakeCursor:
|
|
244
251
|
|
245
252
|
affected_count = None
|
246
253
|
|
247
|
-
if
|
248
|
-
|
249
|
-
else:
|
250
|
-
ident = None
|
251
|
-
|
252
|
-
if cmd == "USE DATABASE" and ident:
|
253
|
-
self._conn.database = ident
|
254
|
+
if set_database := transformed.args.get("set_database"):
|
255
|
+
self._conn.database = set_database
|
254
256
|
self._conn.database_set = True
|
255
257
|
|
256
|
-
elif
|
257
|
-
self._conn.schema =
|
258
|
+
elif set_schema := transformed.args.get("set_schema"):
|
259
|
+
self._conn.schema = set_schema
|
258
260
|
self._conn.schema_set = True
|
259
261
|
|
260
262
|
elif create_db_name := transformed.args.get("create_db_name"):
|
@@ -262,26 +264,6 @@ class FakeSnowflakeCursor:
|
|
262
264
|
self._duck_conn.execute(info_schema.creation_sql(create_db_name))
|
263
265
|
result_sql = SQL_CREATED_DATABASE.substitute(name=create_db_name)
|
264
266
|
|
265
|
-
elif cmd == "CREATE SCHEMA" and ident:
|
266
|
-
result_sql = SQL_CREATED_SCHEMA.substitute(name=ident)
|
267
|
-
|
268
|
-
elif cmd == "CREATE TABLE" and ident:
|
269
|
-
result_sql = SQL_CREATED_TABLE.substitute(name=ident)
|
270
|
-
|
271
|
-
elif cmd == "CREATE VIEW" and ident:
|
272
|
-
result_sql = SQL_CREATED_VIEW.substitute(name=ident)
|
273
|
-
|
274
|
-
elif cmd.startswith("DROP") and ident:
|
275
|
-
result_sql = SQL_DROPPED.substitute(name=ident)
|
276
|
-
|
277
|
-
# if dropping the current database/schema then reset conn metadata
|
278
|
-
if cmd == "DROP DATABASE" and ident == self._conn.database:
|
279
|
-
self._conn.database = None
|
280
|
-
self._conn.schema = None
|
281
|
-
|
282
|
-
elif cmd == "DROP SCHEMA" and ident == self._conn.schema:
|
283
|
-
self._conn.schema = None
|
284
|
-
|
285
267
|
elif cmd == "INSERT":
|
286
268
|
(affected_count,) = self._duck_conn.fetchall()[0]
|
287
269
|
result_sql = SQL_INSERTED_ROWS.substitute(count=affected_count)
|
@@ -301,6 +283,28 @@ class FakeSnowflakeCursor:
|
|
301
283
|
lambda e: transforms.describe_table(e, self._conn.database, self._conn.schema)
|
302
284
|
).sql(dialect="duckdb")
|
303
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
|
+
|
304
308
|
if table_comment := cast(tuple[exp.Table, str], transformed.args.get("table_comment")):
|
305
309
|
# record table comment
|
306
310
|
table, comment = table_comment
|
fakesnow/transforms.py
CHANGED
@@ -566,6 +566,9 @@ def json_extract_cast_as_varchar(expression: exp.Expression) -> exp.Expression:
|
|
566
566
|
"""
|
567
567
|
if (
|
568
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}
|
569
572
|
and (je := expression.this)
|
570
573
|
and isinstance(je, exp.JSONExtract)
|
571
574
|
and (path := je.expression)
|
@@ -580,7 +583,7 @@ def json_extract_precedence(expression: exp.Expression) -> exp.Expression:
|
|
580
583
|
|
581
584
|
See https://github.com/tekumara/fakesnow/issues/53
|
582
585
|
"""
|
583
|
-
if isinstance(expression, exp.JSONExtract):
|
586
|
+
if isinstance(expression, (exp.JSONExtract, exp.JSONExtractScalar)):
|
584
587
|
return exp.Paren(this=expression)
|
585
588
|
return expression
|
586
589
|
|
@@ -779,7 +782,10 @@ def set_schema(expression: exp.Expression, current_database: str | None) -> exp.
|
|
779
782
|
|
780
783
|
if kind.name.upper() == "DATABASE":
|
781
784
|
# duckdb's default schema is main
|
782
|
-
|
785
|
+
database = expression.this.name
|
786
|
+
return exp.Command(
|
787
|
+
this="SET", expression=exp.Literal.string(f"schema = '{database}.main'"), set_database=database
|
788
|
+
)
|
783
789
|
else:
|
784
790
|
# SCHEMA
|
785
791
|
if db := expression.this.args.get("db"): # noqa: SIM108
|
@@ -788,9 +794,10 @@ def set_schema(expression: exp.Expression, current_database: str | None) -> exp.
|
|
788
794
|
# isn't qualified with a database
|
789
795
|
db_name = current_database or MISSING_DATABASE
|
790
796
|
|
791
|
-
|
792
|
-
|
793
|
-
|
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
|
+
)
|
794
801
|
|
795
802
|
return expression
|
796
803
|
|
@@ -1095,6 +1102,21 @@ def timestamp_ntz_ns(expression: exp.Expression) -> exp.Expression:
|
|
1095
1102
|
return expression
|
1096
1103
|
|
1097
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
|
+
|
1098
1120
|
def try_parse_json(expression: exp.Expression) -> exp.Expression:
|
1099
1121
|
"""Convert TRY_PARSE_JSON() to TRY_CAST(... as JSON).
|
1100
1122
|
|
@@ -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
|