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 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)
@@ -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.parse_json)
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
- debug = command if fs_debug == "snowflake" else sql
216
- 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
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 (maybe_ident := expression.find(exp.Identifier, bfs=False)) and isinstance(maybe_ident.this, str):
241
- ident = maybe_ident.this if maybe_ident.quoted else maybe_ident.this.upper()
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 cmd == "USE SCHEMA" and ident:
250
- self._conn.schema = ident
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
- self._duck_conn.execute(f"INSERT INTO {table_name}({','.join(df.columns.to_list())}) SELECT * FROM df")
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
- # remove expressions containing NULL
512
- for enull in expression.find_all(exp.Null):
513
- if enull.parent:
514
- enull.parent.pop()
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
- return exp.Anonymous(this="TO_JSON", expressions=[expression])
645
+ left = e.left
646
+ right = e.right
517
647
 
518
- return expression
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
- def parse_json(expression: exp.Expression) -> exp.Expression:
522
- """Convert parse_json() to json().
654
+ non_null_expressions.append(e)
523
655
 
524
- Example:
525
- >>> import sqlglot
526
- >>> sqlglot.parse_one("insert into table1 (name) select parse_json('{}')").transform(parse_json).sql()
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
- 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
+ )
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
- name = f"{db_name}.{expression.this.name}"
679
-
680
- 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
+ )
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", "TO_NUMBER", "TO_NUMERIC"]
1035
+ and expression.this.upper() in ["TO_DECIMAL", "TO_NUMERIC"]
842
1036
  ):
843
- expressions: list[exp.Expression] = expression.expressions
1037
+ return _to_decimal(expression, exp.Cast)
844
1038
 
845
- if len(expressions) > 1 and expressions[1].is_string:
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
- return exp.Cast(
853
- this=expressions[0],
854
- to=exp.DataType(this=exp.DataType.Type.DECIMAL, expressions=[precision, scale], nested=False, prefix=False),
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.6
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 ~=21.2.0
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
  [![ci](https://github.com/tekumara/fakesnow/actions/workflows/ci.yml/badge.svg)](https://github.com/tekumara/fakesnow/actions/workflows/ci.yml)
234
234
  [![release](https://github.com/tekumara/fakesnow/actions/workflows/release.yml/badge.svg)](https://github.com/tekumara/fakesnow/actions/workflows/release.yml)
235
235
  [![PyPI](https://img.shields.io/pypi/v/fakesnow?color=violet)](https://pypi.org/project/fakesnow/)
236
-
237
- [![ci](../../actions/workflows/ci.yml/badge.svg)](../../actions/workflows/ci.yml)
236
+ [![PyPI - Downloads](https://img.shields.io/pypi/dm/fakesnow?color=violet)](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=5Fq_Qk-Iqxxzl-1XkcMyGDw2NY5hzE_50e5xii-jxhA,28463
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=4cGfNbcd-X1l0UEGFudjoRejTjxSwWo-E0NXGpdVlaE,40109
13
- fakesnow-0.9.6.dist-info/LICENSE,sha256=kW-7NWIyaRMQiDpryfSmF2DObDZHGR1cJZ39s6B1Svg,11344
14
- fakesnow-0.9.6.dist-info/METADATA,sha256=PBc3zlOgUpHFpxAJoTHnZaKcgZz8J6LqfVnP_yMytRw,17802
15
- fakesnow-0.9.6.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
16
- fakesnow-0.9.6.dist-info/entry_points.txt,sha256=2riAUgu928ZIHawtO8EsfrMEJhi-EH-z_Vq7Q44xKPM,47
17
- fakesnow-0.9.6.dist-info/top_level.txt,sha256=500evXI1IFX9so82cizGIEMHAb_dJNPaZvd2H9dcKTA,24
18
- fakesnow-0.9.6.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,,