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 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 exp, parse_one
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
- cur.execute(f"DESCRIBE {self._last_sql}", self._last_params)
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
- return self._execute(command, params, *args, **kwargs)
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 _execute(
135
- self,
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
- debug = command if fs_debug == "snowflake" else sql
223
- print(f"{debug};{params=}" if params else f"{debug};", file=sys.stderr)
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 (maybe_ident := expression.find(exp.Identifier, bfs=False)) and isinstance(maybe_ident.this, str):
248
- ident = maybe_ident.this if maybe_ident.quoted else maybe_ident.this.upper()
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 cmd == "USE SCHEMA" and ident:
257
- self._conn.schema = ident
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
- name = f"{expression.this.name}.main"
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
- name = f"{db_name}.{expression.this.name}"
792
-
793
- return exp.Command(this="SET", expression=exp.Literal.string(f"schema = '{name}'"))
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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fakesnow
3
- Version: 0.9.7
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
@@ -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=3tTPaAC1vBaTLmSG92o51QA0AzIT9XDieYiZsMzvY9M,28929
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=77hqWLWsZNvi6fLrn-JhIIeDy8CgiJ-zlNIAm8rQLf0,48818
13
- fakesnow-0.9.7.dist-info/LICENSE,sha256=kW-7NWIyaRMQiDpryfSmF2DObDZHGR1cJZ39s6B1Svg,11344
14
- fakesnow-0.9.7.dist-info/METADATA,sha256=ISDnq1yQPohGORq0isidKp11g_vWYt37rdtWz2vaoKE,17831
15
- fakesnow-0.9.7.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
16
- fakesnow-0.9.7.dist-info/entry_points.txt,sha256=2riAUgu928ZIHawtO8EsfrMEJhi-EH-z_Vq7Q44xKPM,47
17
- fakesnow-0.9.7.dist-info/top_level.txt,sha256=500evXI1IFX9so82cizGIEMHAb_dJNPaZvd2H9dcKTA,24
18
- fakesnow-0.9.7.dist-info/RECORD,,
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,,