fakesnow 0.9.7__py3-none-any.whl → 0.9.9__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 +41 -10
- {fakesnow-0.9.7.dist-info → fakesnow-0.9.9.dist-info}/METADATA +2 -2
- {fakesnow-0.9.7.dist-info → fakesnow-0.9.9.dist-info}/RECORD +8 -8
- {fakesnow-0.9.7.dist-info → fakesnow-0.9.9.dist-info}/LICENSE +0 -0
- {fakesnow-0.9.7.dist-info → fakesnow-0.9.9.dist-info}/WHEEL +0 -0
- {fakesnow-0.9.7.dist-info → fakesnow-0.9.9.dist-info}/entry_points.txt +0 -0
- {fakesnow-0.9.7.dist-info → fakesnow-0.9.9.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
@@ -309,7 +309,7 @@ def extract_comment_on_table(expression: exp.Expression) -> exp.Expression:
|
|
309
309
|
if props := cast(exp.Properties, expression.args.get("properties")):
|
310
310
|
other_props = []
|
311
311
|
for p in props.expressions:
|
312
|
-
if isinstance(p, exp.SchemaCommentProperty) and (isinstance(p.this, (exp.Literal, exp.
|
312
|
+
if isinstance(p, exp.SchemaCommentProperty) and (isinstance(p.this, (exp.Literal, exp.Var))):
|
313
313
|
comment = p.this.this
|
314
314
|
else:
|
315
315
|
other_props.append(p)
|
@@ -360,10 +360,19 @@ def extract_text_length(expression: exp.Expression) -> exp.Expression:
|
|
360
360
|
|
361
361
|
if isinstance(expression, (exp.Create, exp.AlterTable)):
|
362
362
|
text_lengths = []
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
363
|
+
|
364
|
+
# exp.Select is for a ctas, exp.Schema is a plain definition
|
365
|
+
if cols := expression.find(exp.Select, exp.Schema):
|
366
|
+
expressions = cols.expressions
|
367
|
+
else:
|
368
|
+
# alter table
|
369
|
+
expressions = expression.args.get("actions") or []
|
370
|
+
for e in expressions:
|
371
|
+
if dts := [
|
372
|
+
dt for dt in e.find_all(exp.DataType) if dt.this in (exp.DataType.Type.VARCHAR, exp.DataType.Type.TEXT)
|
373
|
+
]:
|
374
|
+
col_name = e.alias if isinstance(e, exp.Alias) else e.name
|
375
|
+
if len(dts) == 1 and (dt_size := dts[0].find(exp.DataTypeParam)):
|
367
376
|
size = (
|
368
377
|
isinstance(dt_size.this, exp.Literal)
|
369
378
|
and isinstance(dt_size.this.this, str)
|
@@ -566,6 +575,9 @@ def json_extract_cast_as_varchar(expression: exp.Expression) -> exp.Expression:
|
|
566
575
|
"""
|
567
576
|
if (
|
568
577
|
isinstance(expression, exp.Cast)
|
578
|
+
and (to := expression.to)
|
579
|
+
and isinstance(to, exp.DataType)
|
580
|
+
and to.this in {exp.DataType.Type.VARCHAR, exp.DataType.Type.TEXT}
|
569
581
|
and (je := expression.this)
|
570
582
|
and isinstance(je, exp.JSONExtract)
|
571
583
|
and (path := je.expression)
|
@@ -580,7 +592,7 @@ def json_extract_precedence(expression: exp.Expression) -> exp.Expression:
|
|
580
592
|
|
581
593
|
See https://github.com/tekumara/fakesnow/issues/53
|
582
594
|
"""
|
583
|
-
if isinstance(expression, exp.JSONExtract):
|
595
|
+
if isinstance(expression, (exp.JSONExtract, exp.JSONExtractScalar)):
|
584
596
|
return exp.Paren(this=expression)
|
585
597
|
return expression
|
586
598
|
|
@@ -779,7 +791,10 @@ def set_schema(expression: exp.Expression, current_database: str | None) -> exp.
|
|
779
791
|
|
780
792
|
if kind.name.upper() == "DATABASE":
|
781
793
|
# duckdb's default schema is main
|
782
|
-
|
794
|
+
database = expression.this.name
|
795
|
+
return exp.Command(
|
796
|
+
this="SET", expression=exp.Literal.string(f"schema = '{database}.main'"), set_database=database
|
797
|
+
)
|
783
798
|
else:
|
784
799
|
# SCHEMA
|
785
800
|
if db := expression.this.args.get("db"): # noqa: SIM108
|
@@ -788,9 +803,10 @@ def set_schema(expression: exp.Expression, current_database: str | None) -> exp.
|
|
788
803
|
# isn't qualified with a database
|
789
804
|
db_name = current_database or MISSING_DATABASE
|
790
805
|
|
791
|
-
|
792
|
-
|
793
|
-
|
806
|
+
schema = expression.this.name
|
807
|
+
return exp.Command(
|
808
|
+
this="SET", expression=exp.Literal.string(f"schema = '{db_name}.{schema}'"), set_schema=schema
|
809
|
+
)
|
794
810
|
|
795
811
|
return expression
|
796
812
|
|
@@ -1095,6 +1111,21 @@ def timestamp_ntz_ns(expression: exp.Expression) -> exp.Expression:
|
|
1095
1111
|
return expression
|
1096
1112
|
|
1097
1113
|
|
1114
|
+
def trim_cast_varchar(expression: exp.Expression) -> exp.Expression:
|
1115
|
+
"""Snowflake's TRIM casts input to VARCHAR implicitly."""
|
1116
|
+
|
1117
|
+
if not (isinstance(expression, exp.Trim)):
|
1118
|
+
return expression
|
1119
|
+
|
1120
|
+
operand = expression.this
|
1121
|
+
if isinstance(operand, exp.Cast) and operand.to.this in [exp.DataType.Type.VARCHAR, exp.DataType.Type.TEXT]:
|
1122
|
+
return expression
|
1123
|
+
|
1124
|
+
return exp.Trim(
|
1125
|
+
this=exp.Cast(this=operand, to=exp.DataType(this=exp.DataType.Type.VARCHAR, nested=False, prefix=False))
|
1126
|
+
)
|
1127
|
+
|
1128
|
+
|
1098
1129
|
def try_parse_json(expression: exp.Expression) -> exp.Expression:
|
1099
1130
|
"""Convert TRY_PARSE_JSON() to TRY_CAST(... as JSON).
|
1100
1131
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: fakesnow
|
3
|
-
Version: 0.9.
|
3
|
+
Version: 0.9.9
|
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 ~=23.
|
216
|
+
Requires-Dist: sqlglot ~=23.12.2
|
217
217
|
Provides-Extra: dev
|
218
218
|
Requires-Dist: build ~=1.0 ; extra == 'dev'
|
219
219
|
Requires-Dist: pandas-stubs ; extra == 'dev'
|
@@ -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=5-JWBE4d6NDTqDSXDZjZXk1gSeK0sjHOZVy5RJPiPQA,50059
|
13
|
+
fakesnow-0.9.9.dist-info/LICENSE,sha256=kW-7NWIyaRMQiDpryfSmF2DObDZHGR1cJZ39s6B1Svg,11344
|
14
|
+
fakesnow-0.9.9.dist-info/METADATA,sha256=08vNDl-q6ssZOlj4X3w8R559uXZtDlN09d6XvgVS4mQ,17832
|
15
|
+
fakesnow-0.9.9.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
16
|
+
fakesnow-0.9.9.dist-info/entry_points.txt,sha256=2riAUgu928ZIHawtO8EsfrMEJhi-EH-z_Vq7Q44xKPM,47
|
17
|
+
fakesnow-0.9.9.dist-info/top_level.txt,sha256=500evXI1IFX9so82cizGIEMHAb_dJNPaZvd2H9dcKTA,24
|
18
|
+
fakesnow-0.9.9.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|