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 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
@@ -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.Identifier))):
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
- for dt in expression.find_all(exp.DataType):
364
- if dt.this in (exp.DataType.Type.VARCHAR, exp.DataType.Type.TEXT):
365
- col_name = dt.parent and dt.parent.this and dt.parent.this.this
366
- if dt_size := dt.find(exp.DataTypeParam):
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
- name = f"{expression.this.name}.main"
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
- name = f"{db_name}.{expression.this.name}"
792
-
793
- return exp.Command(this="SET", expression=exp.Literal.string(f"schema = '{name}'"))
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.7
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.3.0
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=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=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,,