fakesnow 0.9.41__py3-none-any.whl → 0.9.42__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/copy_into.py CHANGED
@@ -1,9 +1,11 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import datetime
4
+ import os
4
5
  from collections.abc import Sequence
5
6
  from dataclasses import dataclass, field
6
- from typing import Any, NamedTuple, Protocol, cast
7
+ from pathlib import Path
8
+ from typing import Any, NamedTuple, Protocol, Union, cast
7
9
  from urllib.parse import urlparse, urlunparse
8
10
 
9
11
  import duckdb
@@ -13,6 +15,9 @@ from sqlglot import exp
13
15
 
14
16
  import fakesnow.transforms.stage as stage
15
17
  from fakesnow import logger
18
+ from fakesnow.params import MutableParams, pop_qmark_param
19
+
20
+ Params = Union[Sequence[Any], dict[Any, Any]]
16
21
 
17
22
 
18
23
  class LoadHistoryRecord(NamedTuple):
@@ -38,9 +43,9 @@ def copy_into(
38
43
  current_database: str | None,
39
44
  current_schema: str | None,
40
45
  expr: exp.Copy,
41
- params: Sequence[Any] | dict[Any, Any] | None = None,
46
+ params: MutableParams | None = None,
42
47
  ) -> str:
43
- cparams = _params(expr)
48
+ cparams = _params(expr, params)
44
49
  if isinstance(cparams.file_format, ReadParquet):
45
50
  from_ = expr.args["files"][0]
46
51
  # parquet must use MATCH_BY_COLUMN_NAME (TODO) or a copy transformation
@@ -54,11 +59,15 @@ def copy_into(
54
59
 
55
60
  from_source = _from_source(expr)
56
61
  source = (
57
- stage_url_from_var(from_source, duck_conn, current_database, current_schema)
62
+ stage_url_from_var(from_source[1:], duck_conn, current_database, current_schema)
58
63
  if from_source.startswith("@")
59
64
  else from_source
60
65
  )
61
- urls = _source_urls(source, cparams.files)
66
+ urls = _source_urls(source, cparams.files) if cparams.files else _source_glob(source, duck_conn)
67
+ if not urls:
68
+ sql = "SELECT 'Copy executed with 0 files processed.' AS status"
69
+ duck_conn.execute(sql)
70
+ return sql
62
71
 
63
72
  inserts = _inserts(expr, cparams, urls)
64
73
  table = expr.this
@@ -95,6 +104,10 @@ def copy_into(
95
104
  error_limit = 1
96
105
  error_count = 0
97
106
  first_error_message = None
107
+ path = urlparse(url).path
108
+ if cparams.purge and stage.is_internal(path):
109
+ # If the file is internal, we can remove it from the stage
110
+ os.remove(path)
98
111
 
99
112
  history = LoadHistoryRecord(
100
113
  schema_name=schema,
@@ -123,7 +136,7 @@ def copy_into(
123
136
  "first_error_character, first_error_column_name"
124
137
  )
125
138
  values = "\n, ".join(
126
- f"('{h.file_name}', '{h.status}', {h.row_parsed}, {h.row_count}, "
139
+ f"('{_result_file_name(h.file_name)}', '{h.status}', {h.row_parsed}, {h.row_count}, "
127
140
  f"{h.error_limit or 'NULL'}, {h.error_count}, "
128
141
  f"{repr(h.first_error_message) if h.first_error_message else 'NULL'}, "
129
142
  f"{h.first_error_line_number or 'NULL'}, {h.first_error_character_position or 'NULL'}, "
@@ -132,6 +145,7 @@ def copy_into(
132
145
  )
133
146
  sql = f"SELECT * FROM (VALUES\n {values}\n) AS t({columns})"
134
147
  duck_conn.execute(sql)
148
+
135
149
  return sql
136
150
  except duckdb.HTTPException as e:
137
151
  raise snowflake.connector.errors.ProgrammingError(msg=e.args[0], errno=91016, sqlstate="22000") from None
@@ -139,13 +153,23 @@ def copy_into(
139
153
  raise snowflake.connector.errors.ProgrammingError(msg=e.args[0], errno=100038, sqlstate="22018") from None
140
154
 
141
155
 
142
- def _params(expr: exp.Copy) -> Params:
156
+ def _result_file_name(url: str) -> str:
157
+ if not stage.is_internal(urlparse(url).path):
158
+ return url
159
+
160
+ # for internal stages, return the stage name lowered + file name
161
+ parts = url.split("/")
162
+ return f"{parts[-2].lower()}/{parts[-1]}"
163
+
164
+
165
+ def _params(expr: exp.Copy, params: MutableParams | None = None) -> CopyParams:
143
166
  kwargs = {}
144
167
  force = False
168
+ purge = False
169
+ on_error = "ABORT_STATEMENT"
145
170
 
146
- params = cast(list[exp.CopyParameter], expr.args.get("params", []))
147
- cparams = Params()
148
- for param in params:
171
+ cparams = CopyParams()
172
+ for param in cast(list[exp.CopyParameter], expr.args.get("params", [])):
149
173
  assert isinstance(param.this, exp.Var), f"{param.this.__class__} is not a Var"
150
174
  var = param.this.name.upper()
151
175
  if var == "FILE_FORMAT":
@@ -166,10 +190,22 @@ def _params(expr: exp.Copy) -> Params:
166
190
  force = True
167
191
  elif var == "FILES":
168
192
  kwargs["files"] = [lit.name for lit in param.find_all(exp.Literal)]
193
+ elif var == "PURGE":
194
+ purge = True
195
+ elif var == "ON_ERROR":
196
+ if isinstance(param.expression, exp.Var):
197
+ on_error = param.expression.name.upper()
198
+ elif isinstance(param.expression, exp.Placeholder):
199
+ on_error = pop_qmark_param(params, expr, param.expression)
200
+ else:
201
+ raise NotImplementedError(f"{param.expression.__class__=}")
202
+
203
+ if not (isinstance(on_error, str) and on_error.upper() == "ABORT_STATEMENT"):
204
+ raise NotImplementedError(param)
169
205
  else:
170
206
  raise ValueError(f"Unknown copy parameter: {param.this}")
171
207
 
172
- return Params(force=force, **kwargs)
208
+ return CopyParams(force=force, purge=purge, on_error=on_error, **kwargs)
173
209
 
174
210
 
175
211
  def _from_source(expr: exp.Copy) -> str:
@@ -191,6 +227,9 @@ def _from_source(expr: exp.Copy) -> str:
191
227
  )
192
228
  # return the name of the stage, eg: @stage1
193
229
  return var.this
230
+ elif isinstance(from_, exp.Var):
231
+ # return the name of the stage, eg: @stage1
232
+ return from_.this
194
233
 
195
234
  assert isinstance(from_, exp.Literal), f"{from_} is not a exp.Literal"
196
235
  # return url
@@ -198,9 +237,9 @@ def _from_source(expr: exp.Copy) -> str:
198
237
 
199
238
 
200
239
  def stage_url_from_var(
201
- from_source: str, duck_conn: DuckDBPyConnection, current_database: str | None, current_schema: str | None
240
+ var: str, duck_conn: DuckDBPyConnection, current_database: str | None, current_schema: str | None
202
241
  ) -> str:
203
- database_name, schema_name, name = stage.parts_from_var(from_source, current_database, current_schema)
242
+ database_name, schema_name, name = stage.parts_from_var(var, current_database, current_schema)
204
243
 
205
244
  # Look up the stage URL
206
245
  duck_conn.execute(
@@ -211,7 +250,9 @@ def stage_url_from_var(
211
250
  (database_name, schema_name, name),
212
251
  )
213
252
  if result := duck_conn.fetchone():
214
- return result[0]
253
+ # if no URL is found, it is an internal stage ie: local directory
254
+ url = result[0] or stage.internal_dir(f"{database_name}.{schema_name}.{name}")
255
+ return url
215
256
  else:
216
257
  raise snowflake.connector.errors.ProgrammingError(
217
258
  msg=f"SQL compilation error:\nStage '{database_name}.{schema_name}.{name}' does not exist or not authorized.", # noqa: E501
@@ -220,16 +261,29 @@ def stage_url_from_var(
220
261
  )
221
262
 
222
263
 
223
- def _source_urls(from_source: str, files: list[str]) -> list[str]:
264
+ def _source_urls(source: str, files: list[str]) -> list[str]:
224
265
  """Convert from_source to a list of URLs."""
225
- scheme, netloc, path, params, query, fragment = urlparse(from_source)
266
+ scheme, netloc, path, params, query, fragment = urlparse(source)
226
267
  if not scheme:
227
268
  raise snowflake.connector.errors.ProgrammingError(
228
- msg=f"SQL compilation error:\ninvalid URL prefix found in: '{from_source}'", errno=1011, sqlstate="42601"
269
+ msg=f"SQL compilation error:\ninvalid URL prefix found in: '{source}'", errno=1011, sqlstate="42601"
229
270
  )
230
271
 
231
272
  # rebuild url from components to ensure correct handling of host slash
232
- return [_urlunparse(scheme, netloc, path, params, query, fragment, file) for file in files] or [from_source]
273
+ return [_urlunparse(scheme, netloc, path, params, query, fragment, file) for file in files] or [source]
274
+
275
+
276
+ def _source_glob(source: str, duck_conn: DuckDBPyConnection) -> list[str]:
277
+ """List files from the source using duckdb glob."""
278
+ if stage.is_internal(source):
279
+ source = Path(source).as_uri() # convert local directory to a file URL
280
+
281
+ scheme, _netloc, _path, _params, _query, _fragment = urlparse(source)
282
+ glob = f"{source}/*" if scheme == "file" else f"{source}*"
283
+ sql = f"SELECT file FROM glob('{glob}')"
284
+ logger.log_sql(sql)
285
+ result = duck_conn.execute(sql).fetchall()
286
+ return [r[0] for r in result]
233
287
 
234
288
 
235
289
  def _urlunparse(scheme: str, netloc: str, path: str, params: str, query: str, fragment: str, suffix: str) -> str:
@@ -245,7 +299,7 @@ def _urlunparse(scheme: str, netloc: str, path: str, params: str, query: str, fr
245
299
  return urlunparse((scheme, netloc, path, params, query, fragment))
246
300
 
247
301
 
248
- def _inserts(expr: exp.Copy, params: Params, urls: list[str]) -> list[exp.Expression]:
302
+ def _inserts(expr: exp.Copy, params: CopyParams, urls: list[str]) -> list[exp.Expression]:
249
303
  # INTO expression
250
304
  target = expr.this
251
305
 
@@ -331,10 +385,8 @@ class ReadCSV(FileTypeHandler):
331
385
  delimiter: str = ","
332
386
 
333
387
  def read_expression(self, url: str) -> exp.Expression:
334
- args = []
335
-
336
388
  # don't parse header and use as column names, keep them as column0, column1, etc
337
- args.append(self.make_eq("header", False))
389
+ args = [self.make_eq("header", False)]
338
390
 
339
391
  if self.skip_header:
340
392
  args.append(self.make_eq("skip", 1))
@@ -357,8 +409,10 @@ class ReadParquet(FileTypeHandler):
357
409
 
358
410
 
359
411
  @dataclass
360
- class Params:
412
+ class CopyParams:
361
413
  files: list[str] = field(default_factory=list)
362
414
  # Snowflake defaults to CSV when no file format is specified
363
415
  file_format: FileTypeHandler = field(default_factory=ReadCSV)
364
416
  force: bool = False
417
+ purge: bool = False
418
+ on_error: str = "ABORT_STATEMENT" # Default to ABORT_STATEMENT
fakesnow/cursor.py CHANGED
@@ -10,6 +10,7 @@ from types import TracebackType
10
10
  from typing import TYPE_CHECKING, Any, cast
11
11
 
12
12
  import duckdb
13
+ import pandas as pd
13
14
  import pyarrow # needed by fetch_arrow_table()
14
15
  import snowflake.connector.converter
15
16
  import snowflake.connector.errors
@@ -27,7 +28,9 @@ import fakesnow.info_schema as info_schema
27
28
  import fakesnow.transforms as transforms
28
29
  from fakesnow import logger
29
30
  from fakesnow.copy_into import copy_into
31
+ from fakesnow.params import MutableParams
30
32
  from fakesnow.rowtype import describe_as_result_metadata
33
+ from fakesnow.transforms import stage
31
34
 
32
35
  if TYPE_CHECKING:
33
36
  # don't require pandas at import time
@@ -41,6 +44,7 @@ SCHEMA_UNSET = "schema_unset"
41
44
  SQL_SUCCESS = "SELECT 'Statement executed successfully.' as 'status'"
42
45
  SQL_CREATED_DATABASE = Template("SELECT 'Database ${name} successfully created.' as 'status'")
43
46
  SQL_CREATED_SCHEMA = Template("SELECT 'Schema ${name} successfully created.' as 'status'")
47
+ SQL_CREATED_SECRET = Template("SELECT 'Secret ${name} successfully created.' as 'status'")
44
48
  SQL_CREATED_TABLE = Template("SELECT 'Table ${name} successfully created.' as 'status'")
45
49
  SQL_CREATED_VIEW = Template("SELECT 'View ${name} successfully created.' as 'status'")
46
50
  SQL_CREATED_STAGE = Template("SELECT 'Stage area ${name} successfully created.' as status")
@@ -146,7 +150,8 @@ class FakeSnowflakeCursor:
146
150
  self._sqlstate = None
147
151
 
148
152
  if os.environ.get("FAKESNOW_DEBUG") == "snowflake":
149
- print(f"{command};{params=}" if params else f"{command};", file=sys.stderr)
153
+ p = params or kwargs.get("binding_params")
154
+ print(f"{command};params={p}" if p else f"{command};", file=sys.stderr)
150
155
 
151
156
  command = self._inline_variables(command)
152
157
  if kwargs.get("binding_params"):
@@ -155,6 +160,10 @@ class FakeSnowflakeCursor:
155
160
  else:
156
161
  command, params = self._rewrite_with_params(command, params)
157
162
 
163
+ # convert tuple to mutable list
164
+ if not isinstance(params, (list, dict)) and params is not None:
165
+ params = list(params)
166
+
158
167
  if self._conn.nop_regexes and any(re.match(p, command, re.IGNORECASE) for p in self._conn.nop_regexes):
159
168
  transformed = transforms.SUCCESS_NOP
160
169
  self._execute(transformed, params)
@@ -164,9 +173,12 @@ class FakeSnowflakeCursor:
164
173
  self.check_db_and_schema(expression)
165
174
 
166
175
  for exp in self._transform_explode(expression):
167
- transformed = self._transform(exp)
176
+ transformed = self._transform(exp, params)
168
177
  self._execute(transformed, params)
169
178
 
179
+ if not kwargs.get("server") and (put_stage_data := transformed.args.get("put_stage_data")): # pyright: ignore[reportPossiblyUnboundVariable]
180
+ self._put_files(put_stage_data)
181
+
170
182
  return self
171
183
  except snowflake.connector.errors.ProgrammingError as e:
172
184
  self._sqlstate = e.sqlstate
@@ -180,6 +192,13 @@ class FakeSnowflakeCursor:
180
192
  msg = f"{e} not implemented. Please raise an issue via https://github.com/tekumara/fakesnow/issues/new"
181
193
  raise snowflake.connector.errors.ProgrammingError(msg=msg, errno=9999, sqlstate="99999") from e
182
194
 
195
+ def _put_files(self, put_stage_data: stage.UploadCommandDict) -> None:
196
+ results = stage.upload_files(put_stage_data)
197
+ _df = pd.DataFrame.from_records(results)
198
+ self._duck_conn.execute("select * from _df")
199
+ self._arrow_table = self._duck_conn.fetch_arrow_table()
200
+ self._rowcount = self._arrow_table.num_rows
201
+
183
202
  def check_db_and_schema(self, expression: exp.Expression) -> None:
184
203
  no_database, no_schema = checks.is_unqualified_table_expression(expression)
185
204
 
@@ -198,9 +217,10 @@ class FakeSnowflakeCursor:
198
217
  sqlstate="22000",
199
218
  )
200
219
 
201
- def _transform(self, expression: exp.Expression) -> exp.Expression:
220
+ def _transform(self, expression: exp.Expression, params: MutableParams | None) -> exp.Expression:
202
221
  return (
203
- expression.transform(transforms.upper_case_unquoted_identifiers)
222
+ expression.transform(lambda e: transforms.identifier(e, params))
223
+ .transform(transforms.upper_case_unquoted_identifiers)
204
224
  .transform(transforms.update_variables, variables=self._conn.variables)
205
225
  .transform(transforms.set_schema, current_database=self._conn.database)
206
226
  .transform(transforms.create_database, db_path=self._conn.db_path)
@@ -238,7 +258,6 @@ class FakeSnowflakeCursor:
238
258
  .transform(transforms.sample)
239
259
  .transform(transforms.array_size)
240
260
  .transform(transforms.random)
241
- .transform(transforms.identifier)
242
261
  .transform(transforms.array_agg_within_group)
243
262
  .transform(transforms.array_agg)
244
263
  .transform(transforms.array_construct_etc)
@@ -264,7 +283,8 @@ class FakeSnowflakeCursor:
264
283
  .transform(transforms.alias_in_join)
265
284
  .transform(transforms.alter_table_strip_cluster_by)
266
285
  .transform(lambda e: transforms.create_stage(e, self._conn.database, self._conn.schema))
267
- .transform(lambda e: transforms.put_stage(e, self._conn.database, self._conn.schema))
286
+ .transform(lambda e: transforms.list_stage(e, self._conn.database, self._conn.schema))
287
+ .transform(lambda e: transforms.put_stage(e, self._conn.database, self._conn.schema, params))
268
288
  )
269
289
 
270
290
  def _transform_explode(self, expression: exp.Expression) -> list[exp.Expression]:
@@ -272,7 +292,7 @@ class FakeSnowflakeCursor:
272
292
  # Split transforms have limited support at the moment.
273
293
  return transforms.merge(expression)
274
294
 
275
- def _execute(self, transformed: exp.Expression, params: Sequence[Any] | dict[Any, Any] | None = None) -> None:
295
+ def _execute(self, transformed: exp.Expression, params: MutableParams | None = None) -> None:
276
296
  self._arrow_table = None
277
297
  self._arrow_table_fetch_index = None
278
298
  self._rowcount = None
@@ -335,13 +355,26 @@ class FakeSnowflakeCursor:
335
355
  result_sql = SQL_CREATED_DATABASE.substitute(name=create_db_name)
336
356
 
337
357
  elif stage_name := transformed.args.get("create_stage_name"):
338
- if stage_name == "?":
339
- assert isinstance(params, (tuple, list)) and len(params) == 1, (
340
- "Expected single parameter for create_stage_name"
358
+ (affected_count,) = self._duck_conn.fetchall()[0]
359
+ if affected_count == 0:
360
+ raise snowflake.connector.errors.ProgrammingError(
361
+ msg=f"SQL compilation error:\nObject '{stage_name}' already exists.",
362
+ errno=2002,
363
+ sqlstate="42710",
341
364
  )
342
- result_sql = SQL_CREATED_STAGE.substitute(name=params[0].upper())
343
- else:
344
- result_sql = SQL_CREATED_STAGE.substitute(name=stage_name.upper())
365
+ result_sql = SQL_CREATED_STAGE.substitute(name=stage_name)
366
+
367
+ elif stage_name := transformed.args.get("list_stage_name") or transformed.args.get("put_stage_name"):
368
+ if self._duck_conn.fetch_arrow_table().num_rows != 1:
369
+ raise snowflake.connector.errors.ProgrammingError(
370
+ msg=f"SQL compilation error:\nStage '{stage_name}' does not exist or not authorized.",
371
+ errno=2003,
372
+ sqlstate="02000",
373
+ )
374
+ if transformed.args.get("list_stage_name"):
375
+ result_sql = stage.list_stage_files_sql(stage_name)
376
+ elif transformed.args.get("put_stage_name"):
377
+ result_sql = SQL_SUCCESS
345
378
 
346
379
  elif cmd == "INSERT":
347
380
  (affected_count,) = self._duck_conn.fetchall()[0]
@@ -365,8 +398,8 @@ class FakeSnowflakeCursor:
365
398
  lambda e: transforms.describe_table(e, self._conn.database, self._conn.schema)
366
399
  ).sql(dialect="duckdb")
367
400
 
368
- elif (eid := transformed.find(exp.Identifier, bfs=False)) and isinstance(eid.this, str):
369
- ident = eid.this if eid.quoted else eid.this.upper()
401
+ elif eid := transformed.find(exp.Identifier, bfs=False):
402
+ ident = eid.name
370
403
  if cmd == "CREATE SCHEMA" and ident:
371
404
  result_sql = SQL_CREATED_SCHEMA.substitute(name=ident)
372
405
 
@@ -389,6 +422,15 @@ class FakeSnowflakeCursor:
389
422
 
390
423
  elif cmd == "DROP SCHEMA" and ident == self._conn.schema:
391
424
  self._conn._schema = None # noqa: SLF001
425
+ elif (
426
+ cmd == "CREATE"
427
+ and isinstance(transformed, exp.Command)
428
+ and isinstance(transformed.expression, str)
429
+ and transformed.expression.upper().startswith(" SECRET")
430
+ ):
431
+ match = re.search(r"SECRET\s+(\w+)\s*\(", transformed.expression, re.IGNORECASE)
432
+ secret_name = match[1].upper() if match else "UNKNOWN"
433
+ result_sql = SQL_CREATED_SECRET.substitute(name=secret_name)
392
434
 
393
435
  if table_comment := cast(tuple[exp.Table, str], transformed.args.get("table_comment")):
394
436
  # record table comment
@@ -415,18 +457,6 @@ class FakeSnowflakeCursor:
415
457
  self._rowcount = affected_count or self._arrow_table.num_rows
416
458
  self._sfqid = str(uuid.uuid4())
417
459
 
418
- if stage_name := transformed.args.get("put_stage_name"):
419
- if stage_name == "?":
420
- assert isinstance(params, (tuple, list)) and len(params) == 1, (
421
- "Expected single parameter for put_stage_name"
422
- )
423
- if self._arrow_table.num_rows != 1:
424
- raise snowflake.connector.errors.ProgrammingError(
425
- msg=f"SQL compilation error:\nStage '{stage_name}' does not exist or not authorized.",
426
- errno=2003,
427
- sqlstate="02000",
428
- )
429
-
430
460
  self._last_sql = result_sql or sql
431
461
  self._last_params = None if result_sql else params
432
462
  self._last_transformed = transformed
fakesnow/params.py ADDED
@@ -0,0 +1,29 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Union
4
+
5
+ from sqlglot import exp
6
+
7
+ MutableParams = Union[list[Any], dict[Any, Any]]
8
+
9
+
10
+ def pop_qmark_param(params: MutableParams | None, expr: exp.Expression, pl: exp.Placeholder) -> Any: # noqa: ANN401
11
+ assert isinstance(params, list), "params must be provided as a list or tuple to resolve qmarks"
12
+ i = index_of_placeholder(expr, pl)
13
+ return params.pop(i)
14
+
15
+
16
+ def index_of_placeholder(expr: exp.Expression, target: exp.Placeholder) -> int:
17
+ """Count the number of prior placeholders to determine the index.
18
+
19
+ Args:
20
+ expression (exp.Expression): The expression to search.
21
+ ph (exp.Placeholder): The placeholder to find.
22
+
23
+ Returns:
24
+ int: The index of the placeholder, or -1 if not found.
25
+ """
26
+ for index, ph in enumerate(expr.find_all(exp.Placeholder, bfs=False)):
27
+ if ph is target:
28
+ return index
29
+ return -1
fakesnow/server.py CHANGED
@@ -94,7 +94,7 @@ async def query_request(request: Request) -> JSONResponse:
94
94
 
95
95
  try:
96
96
  # only a single sql statement is sent at a time by the python snowflake connector
97
- cur = await run_in_threadpool(conn.cursor().execute, sql_text, binding_params=params)
97
+ cur = await run_in_threadpool(conn.cursor().execute, sql_text, binding_params=params, server=True)
98
98
  rowtype = describe_as_rowtype(cur._describe_last_sql()) # noqa: SLF001
99
99
 
100
100
  expr = cur._last_transformed # noqa: SLF001
@@ -124,7 +124,7 @@ async def query_request(request: Request) -> JSONResponse:
124
124
  )
125
125
  except Exception as e:
126
126
  # we have a bug or use of an unsupported feature
127
- msg = f"{sql_text=} Unhandled exception"
127
+ msg = f"{sql_text=} {params=} Unhandled exception"
128
128
  logger.error(msg, exc_info=e)
129
129
  # my guess at mimicking a 500 error as per https://docs.snowflake.com/en/developer-guide/sql-api/reference
130
130
  # and https://github.com/snowflakedb/gosnowflake/blob/8ed4c75ffd707dd712ad843f40189843ace683c4/restful.go#L318
@@ -15,6 +15,7 @@ from fakesnow.transforms.show import (
15
15
  )
16
16
  from fakesnow.transforms.stage import (
17
17
  create_stage as create_stage,
18
+ list_stage as list_stage,
18
19
  put_stage as put_stage,
19
20
  )
20
21
  from fakesnow.transforms.transforms import (
@@ -62,9 +62,7 @@ def show_columns(
62
62
 
63
63
  See https://docs.snowflake.com/en/sql-reference/sql/show-columns
64
64
  """
65
- if not (
66
- isinstance(expression, exp.Show) and isinstance(expression.this, str) and expression.this.upper() == "COLUMNS"
67
- ):
65
+ if not (isinstance(expression, exp.Show) and expression.name.upper() == "COLUMNS"):
68
66
  return expression
69
67
 
70
68
  scope_kind = expression.args.get("scope_kind")
@@ -139,7 +137,7 @@ def show_databases(expression: exp.Expression) -> exp.Expression:
139
137
 
140
138
  See https://docs.snowflake.com/en/sql-reference/sql/show-databases
141
139
  """
142
- if isinstance(expression, exp.Show) and isinstance(expression.this, str) and expression.this.upper() == "DATABASES":
140
+ if isinstance(expression, exp.Show) and expression.name.upper() == "DATABASES":
143
141
  return sqlglot.parse_one("SELECT * FROM _fs_global._fs_information_schema._fs_show_databases", read="duckdb")
144
142
 
145
143
  return expression
@@ -177,7 +175,7 @@ def show_functions(expression: exp.Expression) -> exp.Expression:
177
175
 
178
176
  See https://docs.snowflake.com/en/sql-reference/sql/show-functions
179
177
  """
180
- if isinstance(expression, exp.Show) and isinstance(expression.this, str) and expression.this.upper() == "FUNCTIONS":
178
+ if isinstance(expression, exp.Show) and expression.name.upper() == "FUNCTIONS":
181
179
  return sqlglot.parse_one("SELECT * FROM _fs_global._fs_information_schema._fs_show_functions", read="duckdb")
182
180
 
183
181
  return expression
@@ -197,11 +195,7 @@ def show_keys(
197
195
  if kind == "FOREIGN":
198
196
  snowflake_kind = "IMPORTED"
199
197
 
200
- if (
201
- isinstance(expression, exp.Show)
202
- and isinstance(expression.this, str)
203
- and expression.this.upper() == f"{snowflake_kind} KEYS"
204
- ):
198
+ if isinstance(expression, exp.Show) and expression.name.upper() == f"{snowflake_kind} KEYS":
205
199
  if kind == "FOREIGN":
206
200
  statement = f"""
207
201
  SELECT
@@ -298,11 +292,7 @@ def show_procedures(expression: exp.Expression) -> exp.Expression:
298
292
 
299
293
  See https://docs.snowflake.com/en/sql-reference/sql/show-procedures
300
294
  """
301
- if (
302
- isinstance(expression, exp.Show)
303
- and isinstance(expression.this, str)
304
- and expression.this.upper() == "PROCEDURES"
305
- ):
295
+ if isinstance(expression, exp.Show) and expression.name.upper() == "PROCEDURES":
306
296
  return sqlglot.parse_one(
307
297
  "SELECT * FROM _fs_global._fs_information_schema._fs_show_procedures",
308
298
  read="duckdb",
@@ -333,7 +323,7 @@ def show_schemas(expression: exp.Expression, current_database: str | None) -> ex
333
323
 
334
324
  See https://docs.snowflake.com/en/sql-reference/sql/show-schemas
335
325
  """
336
- if isinstance(expression, exp.Show) and isinstance(expression.this, str) and expression.this.upper() == "SCHEMAS":
326
+ if isinstance(expression, exp.Show) and expression.name.upper() == "SCHEMAS":
337
327
  if (ident := expression.find(exp.Identifier)) and isinstance(ident.this, str):
338
328
  database = ident.this
339
329
  else:
@@ -350,9 +340,7 @@ def show_schemas(expression: exp.Expression, current_database: str | None) -> ex
350
340
 
351
341
  def show_stages(expression: exp.Expression, current_database: str | None, current_schema: str | None) -> exp.Expression:
352
342
  """Transform SHOW STAGES to a select from the fake _fs_stages table."""
353
- if not (
354
- isinstance(expression, exp.Show) and isinstance(expression.this, str) and expression.this.upper() == "STAGES"
355
- ):
343
+ if not (isinstance(expression, exp.Show) and expression.name.upper() == "STAGES"):
356
344
  return expression
357
345
 
358
346
  scope_kind = expression.args.get("scope_kind")
@@ -480,8 +468,7 @@ def show_tables_etc(
480
468
  """Transform SHOW OBJECTS/TABLES/VIEWS to a query against the _fs_information_schema views."""
481
469
  if not (
482
470
  isinstance(expression, exp.Show)
483
- and isinstance(expression.this, str)
484
- and (show := expression.this.upper())
471
+ and (show := expression.name.upper())
485
472
  and show in {"OBJECTS", "TABLES", "VIEWS"}
486
473
  ):
487
474
  return expression
@@ -538,7 +525,7 @@ def show_users(expression: exp.Expression) -> exp.Expression:
538
525
 
539
526
  https://docs.snowflake.com/en/sql-reference/sql/show-users
540
527
  """
541
- if isinstance(expression, exp.Show) and isinstance(expression.this, str) and expression.this.upper() == "USERS":
528
+ if isinstance(expression, exp.Show) and expression.name.upper() == "USERS":
542
529
  return sqlglot.parse_one("SELECT * FROM _fs_global._fs_information_schema._fs_users", read="duckdb")
543
530
 
544
531
  return expression
@@ -593,11 +580,7 @@ def show_warehouses(expression: exp.Expression) -> exp.Expression:
593
580
 
594
581
  See https://docs.snowflake.com/en/sql-reference/sql/show-warehouses
595
582
  """
596
- if (
597
- isinstance(expression, exp.Show)
598
- and isinstance(expression.this, str)
599
- and expression.this.upper() == "WAREHOUSES"
600
- ):
583
+ if isinstance(expression, exp.Show) and expression.name.upper() == "WAREHOUSES":
601
584
  return sqlglot.parse_one(SQL_SHOW_WAREHOUSES, read="duckdb")
602
585
 
603
586
  return expression
@@ -1,18 +1,44 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import datetime
4
+ import os
5
+ import tempfile
6
+ from pathlib import PurePath
7
+ from typing import Any, TypedDict
4
8
  from urllib.parse import urlparse
5
9
  from urllib.request import url2pathname
6
10
 
7
11
  import snowflake.connector.errors
8
12
  import sqlglot
13
+ from snowflake.connector.file_util import SnowflakeFileUtil
9
14
  from sqlglot import exp
10
15
 
11
- LOCAL_BUCKET_PATH = "/tmp/fakesnow_bucket"
16
+ from fakesnow.params import MutableParams
17
+
18
+ # TODO: clean up temp files on exit
19
+ LOCAL_BUCKET_PATH = tempfile.mkdtemp(prefix="fakesnow_bucket_")
20
+
21
+
22
+ class StageInfoDict(TypedDict):
23
+ locationType: str
24
+ location: str
25
+ creds: dict[str, Any]
26
+
27
+
28
+ class UploadCommandDict(TypedDict):
29
+ stageInfo: StageInfoDict
30
+ src_locations: list[str]
31
+ parallel: int
32
+ autoCompress: bool
33
+ sourceCompression: str
34
+ overwrite: bool
35
+ command: str
12
36
 
13
37
 
14
38
  def create_stage(
15
- expression: exp.Expression, current_database: str | None, current_schema: str | None
39
+ expression: exp.Expression,
40
+ current_database: str | None,
41
+ current_schema: str | None,
16
42
  ) -> exp.Expression:
17
43
  """Transform CREATE STAGE to an INSERT statement for the fake stages table."""
18
44
  if not (
@@ -24,15 +50,17 @@ def create_stage(
24
50
  ):
25
51
  return expression
26
52
 
53
+ ident = table.this
54
+ if not isinstance(ident, exp.Identifier):
55
+ raise snowflake.connector.errors.ProgrammingError(
56
+ msg=f"SQL compilation error:\nInvalid identifier type {ident.__class__.__name__} for stage name.",
57
+ errno=1003,
58
+ sqlstate="42000",
59
+ )
60
+
27
61
  catalog = table.catalog or current_database
28
62
  schema = table.db or current_schema
29
- ident = table.this
30
- if isinstance(ident, exp.Placeholder):
31
- stage_name = "?"
32
- elif isinstance(ident, exp.Identifier):
33
- stage_name = ident.this if ident.quoted else ident.this.upper()
34
- else:
35
- raise ValueError(f"Invalid identifier type {ident.__class__.__name__} for stage name")
63
+ stage_name = ident.this
36
64
  now = datetime.datetime.now(datetime.timezone.utc).isoformat()
37
65
 
38
66
  is_temp = False
@@ -55,17 +83,19 @@ def create_stage(
55
83
  cloud = "AWS" if url.startswith("s3://") else None
56
84
 
57
85
  stage_type = ("EXTERNAL" if url else "INTERNAL") + (" TEMPORARY" if is_temp else "")
58
- stage_name_value = stage_name if stage_name == "?" else repr(stage_name)
59
86
 
60
87
  insert_sql = f"""
61
88
  INSERT INTO _fs_global._fs_information_schema._fs_stages
62
89
  (created_on, name, database_name, schema_name, url, has_credentials, has_encryption_key, owner,
63
90
  comment, region, type, cloud, notification_channel, storage_integration, endpoint, owner_role_type,
64
91
  directory_enabled)
65
- VALUES (
66
- '{now}', {stage_name_value}, '{catalog}', '{schema}', '{url}', 'N', 'N', 'SYSADMIN',
92
+ SELECT
93
+ '{now}', '{stage_name}', '{catalog}', '{schema}', '{url}', 'N', 'N', 'SYSADMIN',
67
94
  '', NULL, '{stage_type}', {f"'{cloud}'" if cloud else "NULL"}, NULL, NULL, NULL, 'ROLE',
68
95
  'N'
96
+ WHERE NOT EXISTS (
97
+ SELECT 1 FROM _fs_global._fs_information_schema._fs_stages
98
+ WHERE name = '{stage_name}' AND database_name = '{catalog}' AND schema_name = '{schema}'
69
99
  )
70
100
  """
71
101
  transformed = sqlglot.parse_one(insert_sql, read="duckdb")
@@ -73,10 +103,44 @@ def create_stage(
73
103
  return transformed
74
104
 
75
105
 
76
- # TODO: handle ?
106
+ def list_stage(expression: exp.Expression, current_database: str | None, current_schema: str | None) -> exp.Expression:
107
+ """Transform LIST to list file system operation.
77
108
 
109
+ See https://docs.snowflake.com/en/sql-reference/sql/list
110
+ """
111
+ if not (
112
+ isinstance(expression, exp.Alias)
113
+ and isinstance(expression.this, exp.Column)
114
+ and isinstance(expression.this.this, exp.Identifier)
115
+ and isinstance(expression.this.this.this, str)
116
+ and expression.this.this.this.upper() == "LIST"
117
+ ):
118
+ return expression
119
+
120
+ stage = expression.args["alias"].this
121
+ if not isinstance(stage, exp.Var):
122
+ raise ValueError(f"LIST command requires a stage name as a Var, got {stage}")
123
+
124
+ var = stage.text("this")
125
+ catalog, schema, stage_name = parts_from_var(var, current_database=current_database, current_schema=current_schema)
78
126
 
79
- def put_stage(expression: exp.Expression, current_database: str | None, current_schema: str | None) -> exp.Expression:
127
+ query = f"""
128
+ SELECT *
129
+ from _fs_global._fs_information_schema._fs_stages
130
+ where database_name = '{catalog}' and schema_name = '{schema}' and name = '{stage_name}'
131
+ """
132
+
133
+ transformed = sqlglot.parse_one(query, read="duckdb")
134
+ transformed.args["list_stage_name"] = f"{catalog}.{schema}.{stage_name}"
135
+ return transformed
136
+
137
+
138
+ def put_stage(
139
+ expression: exp.Expression,
140
+ current_database: str | None,
141
+ current_schema: str | None,
142
+ params: MutableParams | None,
143
+ ) -> exp.Expression:
80
144
  """Transform PUT to a SELECT statement to locate the stage.
81
145
 
82
146
  See https://docs.snowflake.com/en/sql-reference/sql/put
@@ -90,14 +154,20 @@ def put_stage(expression: exp.Expression, current_database: str | None, current_
90
154
  target = expression.args["target"]
91
155
 
92
156
  assert isinstance(target, exp.Var), f"{target} is not a exp.Var"
93
- var = target.text("this")
94
- if not var.startswith("@"):
95
- msg = f"SQL compilation error:\n{var} does not start with @"
157
+ this = target.text("this")
158
+ if this == "?":
159
+ if not (isinstance(params, list) and len(params) == 1):
160
+ raise NotImplementedError("PUT requires a single parameter for the stage name")
161
+ this = params.pop(0)
162
+ if not this.startswith("@"):
163
+ msg = f"SQL compilation error:\n{this} does not start with @"
96
164
  raise snowflake.connector.errors.ProgrammingError(
97
165
  msg=msg,
98
166
  errno=1003,
99
167
  sqlstate="42000",
100
168
  )
169
+ # strip leading @
170
+ var = this[1:]
101
171
  catalog, schema, stage_name = parts_from_var(var, current_database=current_database, current_schema=current_schema)
102
172
 
103
173
  query = f"""
@@ -107,12 +177,13 @@ def put_stage(expression: exp.Expression, current_database: str | None, current_
107
177
  """
108
178
 
109
179
  transformed = sqlglot.parse_one(query, read="duckdb")
110
- transformed.args["put_stage_name"] = f"{catalog}.{schema}.{stage_name}"
180
+ fqname = f"{catalog}.{schema}.{stage_name}"
181
+ transformed.args["put_stage_name"] = fqname
111
182
  transformed.args["put_stage_data"] = {
112
183
  "stageInfo": {
113
184
  # use LOCAL_FS otherwise we need to mock S3 with HTTPS which requires a certificate
114
185
  "locationType": "LOCAL_FS",
115
- "location": f"{LOCAL_BUCKET_PATH}/{stage_name}/",
186
+ "location": internal_dir(fqname),
116
187
  "creds": {},
117
188
  },
118
189
  "src_locations": [src_path],
@@ -139,7 +210,7 @@ def normalise_ident(name: str) -> str:
139
210
 
140
211
 
141
212
  def parts_from_var(var: str, current_database: str | None, current_schema: str | None) -> tuple[str, str, str]:
142
- parts = var[1:].split(".")
213
+ parts = var.split(".")
143
214
  if len(parts) == 3:
144
215
  # Fully qualified name
145
216
  database_name, schema_name, name = parts
@@ -161,3 +232,62 @@ def parts_from_var(var: str, current_database: str | None, current_schema: str |
161
232
  name = normalise_ident(name)
162
233
 
163
234
  return database_name, schema_name, name
235
+
236
+
237
+ def is_internal(s: str) -> bool:
238
+ return PurePath(s).is_relative_to(LOCAL_BUCKET_PATH)
239
+
240
+
241
+ def internal_dir(fqname: str) -> str:
242
+ """
243
+ Given a fully qualified stage name, return the directory path where the stage files are stored.
244
+ """
245
+ catalog, schema, stage_name = fqname.split(".")
246
+ return f"{LOCAL_BUCKET_PATH}/{catalog}/{schema}/{stage_name}/"
247
+
248
+
249
+ def list_stage_files_sql(stage_name: str) -> str:
250
+ """
251
+ Generate SQL to list files in a stage directory, matching Snowflake's LIST output format.
252
+ """
253
+ sdir = internal_dir(stage_name)
254
+ return f"""
255
+ select
256
+ lower(split_part(filename, '/', -2)) || '/' || split_part(filename, '/', -1) AS name,
257
+ size,
258
+ md5(content) as md5,
259
+ strftime(last_modified, '%a, %d %b %Y %H:%M:%S GMT') as last_modified
260
+ from read_blob('{sdir}/*')
261
+ """
262
+
263
+
264
+ def upload_files(put_stage_data: UploadCommandDict) -> list[dict[str, Any]]:
265
+ results = []
266
+ for src in put_stage_data["src_locations"]:
267
+ basename = os.path.basename(src)
268
+ stage_dir = put_stage_data["stageInfo"]["location"]
269
+
270
+ os.makedirs(stage_dir, exist_ok=True)
271
+ gzip_file_name, target_size = SnowflakeFileUtil.compress_file_with_gzip(src, stage_dir)
272
+
273
+ # Rename to match expected .gz extension on upload
274
+ target_basename = basename + ".gz"
275
+ target = os.path.join(stage_dir, target_basename)
276
+ os.replace(gzip_file_name, target)
277
+
278
+ target_size = os.path.getsize(target)
279
+ source_size = os.path.getsize(src)
280
+
281
+ results.append(
282
+ {
283
+ "source": basename,
284
+ "target": target_basename,
285
+ "source_size": source_size,
286
+ "target_size": target_size,
287
+ "source_compression": "NONE",
288
+ "target_compression": "GZIP",
289
+ "status": "UPLOADED",
290
+ "message": "",
291
+ }
292
+ )
293
+ return results
@@ -7,6 +7,7 @@ from typing import ClassVar, cast
7
7
  import sqlglot
8
8
  from sqlglot import exp
9
9
 
10
+ from fakesnow.params import MutableParams, pop_qmark_param
10
11
  from fakesnow.variables import Variables
11
12
 
12
13
  SUCCESS_NOP = sqlglot.parse_one("SELECT 'Statement executed successfully.' as status")
@@ -230,7 +231,7 @@ def describe_table(
230
231
  catalog = table.catalog or current_database
231
232
  schema = table.db or current_schema
232
233
 
233
- if schema and schema.upper() == "_FS_INFORMATION_SCHEMA":
234
+ if schema == "_FS_INFORMATION_SCHEMA":
234
235
  # describing an information_schema view
235
236
  # (schema already transformed from information_schema -> _fs_information_schema)
236
237
  return sqlglot.parse_one(SQL_DESCRIBE_INFO_SCHEMA.substitute(view=f"{schema}.{table.name}"), read="duckdb")
@@ -537,22 +538,47 @@ def float_to_double(expression: exp.Expression) -> exp.Expression:
537
538
  return expression
538
539
 
539
540
 
540
- def identifier(expression: exp.Expression) -> exp.Expression:
541
- """Convert identifier function to an identifier.
541
+ def identifier(expression: exp.Expression, params: MutableParams | None) -> exp.Expression:
542
+ """Convert identifier function to an identifier or table.
542
543
 
543
544
  See https://docs.snowflake.com/en/sql-reference/identifier-literal
544
545
  """
545
546
 
546
547
  if (
547
- isinstance(expression, exp.Anonymous)
548
- and isinstance(expression.this, str)
549
- and expression.this.upper() == "IDENTIFIER"
548
+ isinstance(expression, exp.Table)
549
+ and isinstance(expression.this, exp.Anonymous)
550
+ and isinstance(expression.this.this, str)
551
+ and expression.this.this.upper() == "IDENTIFIER"
550
552
  ):
551
- arg = expression.expressions[0]
553
+ arg = expression.this.expressions[0]
554
+
552
555
  # ? is parsed as exp.Placeholder
553
- if isinstance(arg, exp.Placeholder):
554
- return arg
555
- return exp.Identifier(this=arg.this, quoted=False)
556
+ val: str = pop_qmark_param(params, arg.root(), arg) if isinstance(arg, exp.Placeholder) else arg.this
557
+
558
+ # If the whole identifier is quoted, treat as a single quoted identifier inside a Table node
559
+ if val.startswith('"') and val.endswith('"'):
560
+ return exp.Table(this=exp.Identifier(this=val[1:-1], quoted=True))
561
+
562
+ # Split a dotted identifier string into parts, identifying and stripping quoted segments
563
+ parts = [(p[1:-1], True) if p.startswith('"') and p.endswith('"') else (p, False) for p in val.split(".")]
564
+ if len(parts) == 1:
565
+ return exp.Table(this=exp.Identifier(this=parts[0][0], quoted=parts[0][1]))
566
+ elif len(parts) == 2:
567
+ # db.table
568
+ return exp.Table(
569
+ this=exp.Identifier(this=parts[1][0], quoted=parts[1][1]),
570
+ db=exp.Identifier(this=parts[0][0], quoted=parts[0][1]),
571
+ )
572
+ elif len(parts) == 3:
573
+ # catalog.db.table
574
+ return exp.Table(
575
+ this=exp.Identifier(this=parts[2][0], quoted=parts[2][1]),
576
+ db=exp.Identifier(this=parts[1][0], quoted=parts[1][1]),
577
+ catalog=exp.Identifier(this=parts[0][0], quoted=parts[0][1]),
578
+ )
579
+ else:
580
+ # fallback: treat as a single identifier
581
+ return exp.Table(this=exp.Identifier(this=val, quoted=False))
556
582
  return expression
557
583
 
558
584
 
@@ -600,10 +626,10 @@ def information_schema_fs(expression: exp.Expression) -> exp.Expression:
600
626
 
601
627
  if (
602
628
  isinstance(expression, exp.Table)
603
- and expression.db.upper() == "INFORMATION_SCHEMA"
604
- and expression.name.upper() in {"COLUMNS", "TABLES", "VIEWS", "LOAD_HISTORY"}
629
+ and expression.db == "INFORMATION_SCHEMA"
630
+ and expression.name in {"COLUMNS", "TABLES", "VIEWS", "LOAD_HISTORY"}
605
631
  ):
606
- expression.set("this", exp.Identifier(this=f"_FS_{expression.name.upper()}", quoted=False))
632
+ expression.set("this", exp.Identifier(this=f"_FS_{expression.name}", quoted=False))
607
633
  expression.set("db", exp.Identifier(this="_FS_INFORMATION_SCHEMA", quoted=False))
608
634
 
609
635
  return expression
@@ -615,11 +641,8 @@ def information_schema_databases(
615
641
  ) -> exp.Expression:
616
642
  if (
617
643
  isinstance(expression, exp.Table)
618
- and (
619
- expression.db.upper() == "INFORMATION_SCHEMA"
620
- or (current_schema and current_schema.upper() == "INFORMATION_SCHEMA")
621
- )
622
- and expression.name.upper() == "DATABASES"
644
+ and (expression.db == "INFORMATION_SCHEMA" or (current_schema and current_schema == "INFORMATION_SCHEMA"))
645
+ and expression.name == "DATABASES"
623
646
  ):
624
647
  return exp.Table(
625
648
  this=exp.Identifier(this="DATABASES", quoted=False),
@@ -978,11 +1001,7 @@ def to_date(expression: exp.Expression) -> exp.Expression:
978
1001
  exp.Expression: The transformed expression.
979
1002
  """
980
1003
 
981
- if (
982
- isinstance(expression, exp.Anonymous)
983
- and isinstance(expression.this, str)
984
- and expression.this.upper() == "TO_DATE"
985
- ):
1004
+ if isinstance(expression, exp.Anonymous) and expression.name.upper() == "TO_DATE":
986
1005
  return exp.Cast(
987
1006
  this=expression.expressions[0],
988
1007
  to=exp.DataType(this=exp.DataType.Type.DATE, nested=False, prefix=False),
@@ -1066,11 +1085,7 @@ def to_decimal(expression: exp.Expression) -> exp.Expression:
1066
1085
  to=exp.DataType(this=exp.DataType.Type.DECIMAL, expressions=[precision, scale], nested=False, prefix=False),
1067
1086
  )
1068
1087
 
1069
- if (
1070
- isinstance(expression, exp.Anonymous)
1071
- and isinstance(expression.this, str)
1072
- and expression.this.upper() in ["TO_DECIMAL", "TO_NUMERIC"]
1073
- ):
1088
+ if isinstance(expression, exp.Anonymous) and expression.name.upper() in ["TO_DECIMAL", "TO_NUMERIC"]:
1074
1089
  return _to_decimal(expression, exp.Cast)
1075
1090
 
1076
1091
  return expression
@@ -1081,11 +1096,11 @@ def try_to_decimal(expression: exp.Expression) -> exp.Expression:
1081
1096
  See https://docs.snowflake.com/en/sql-reference/functions/try_to_decimal
1082
1097
  """
1083
1098
 
1084
- if (
1085
- isinstance(expression, exp.Anonymous)
1086
- and isinstance(expression.this, str)
1087
- and expression.this.upper() in ["TRY_TO_DECIMAL", "TRY_TO_NUMBER", "TRY_TO_NUMERIC"]
1088
- ):
1099
+ if isinstance(expression, exp.Anonymous) and expression.name.upper() in [
1100
+ "TRY_TO_DECIMAL",
1101
+ "TRY_TO_NUMBER",
1102
+ "TRY_TO_NUMERIC",
1103
+ ]:
1089
1104
  return _to_decimal(expression, exp.TryCast)
1090
1105
 
1091
1106
  return expression
@@ -1111,9 +1126,7 @@ def to_timestamp_ntz(expression: exp.Expression) -> exp.Expression:
1111
1126
  Because it's not yet supported by sqlglot, see https://github.com/tobymao/sqlglot/issues/2748
1112
1127
  """
1113
1128
 
1114
- if isinstance(expression, exp.Anonymous) and (
1115
- isinstance(expression.this, str) and expression.this.upper() == "TO_TIMESTAMP_NTZ"
1116
- ):
1129
+ if isinstance(expression, exp.Anonymous) and expression.name.upper() == "TO_TIMESTAMP_NTZ":
1117
1130
  return exp.StrToTime(
1118
1131
  this=expression.expressions[0],
1119
1132
  format=exp.Literal(this="%Y-%m-%d %H:%M:%S", is_string=True),
@@ -1164,11 +1177,7 @@ def try_parse_json(expression: exp.Expression) -> exp.Expression:
1164
1177
  exp.Expression: The transformed expression.
1165
1178
  """
1166
1179
 
1167
- if (
1168
- isinstance(expression, exp.Anonymous)
1169
- and isinstance(expression.this, str)
1170
- and expression.this.upper() == "TRY_PARSE_JSON"
1171
- ):
1180
+ if isinstance(expression, exp.Anonymous) and expression.name.upper() == "TRY_PARSE_JSON":
1172
1181
  expressions = expression.expressions
1173
1182
  return exp.TryCast(
1174
1183
  this=expressions[0],
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fakesnow
3
- Version: 0.9.41
3
+ Version: 0.9.42
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
@@ -5,8 +5,8 @@ fakesnow/checks.py,sha256=bOJPMp46AvjJV_bXXjx2njO2dXNjffLrznwRuKyYZ4g,2889
5
5
  fakesnow/cli.py,sha256=9qfI-Ssr6mo8UmIlXkUAOz2z2YPBgDsrEVaZv9FjGFs,2201
6
6
  fakesnow/conn.py,sha256=diCwcjaCBrlCn9PyjbScfIQTNQjqiPTkQanUTqcvblE,6009
7
7
  fakesnow/converter.py,sha256=wPOfsFXIUJNJSx5oFNAxh13udxmAVIIHsLK8BiGkXGA,1635
8
- fakesnow/copy_into.py,sha256=YIr5Bq3JwKOPYWm5t2QXUuFGxLL-1ioEXEumNjGbBvM,13648
9
- fakesnow/cursor.py,sha256=mOwyXnBFXp79nDh0vtbmxS_hmFNy4j7hPlskENWeIrI,23818
8
+ fakesnow/copy_into.py,sha256=utlV03RWHdWblIlKs92Q__9BWHi_gvqT76RV6tEMWTo,16006
9
+ fakesnow/cursor.py,sha256=T0aI_AC8U7_mRMccGUIOs8WFby4-8O3ETHpcxJLtM38,25432
10
10
  fakesnow/expr.py,sha256=CAxuYIUkwI339DQIBzvFF0F-m1tcVGKEPA5rDTzmH9A,892
11
11
  fakesnow/fakes.py,sha256=JQTiUkkwPeQrJ8FDWhPFPK6pGwd_aR2oiOrNzCWznlM,187
12
12
  fakesnow/fixtures.py,sha256=2rj0MTZlaZc4PNWhaqC5IiiLa7E9G0QZT3g45YawsL0,633
@@ -15,19 +15,20 @@ fakesnow/instance.py,sha256=OKoYXwaI6kL9HQpnHx44yzpON_xNfuIT_F4oJNF_XXQ,2114
15
15
  fakesnow/logger.py,sha256=U6EjUENQuTrDeNYqER2hxazoySmXzLmZJ-t-SDZgjkg,363
16
16
  fakesnow/macros.py,sha256=lxtznTCYryjecFkwswbqWMzCVamDLWyQZRKWtkWCWEk,1397
17
17
  fakesnow/pandas_tools.py,sha256=wI203UQHC8JvDzxE_VjE1NeV4rThek2P-u52oTg2foo,3481
18
+ fakesnow/params.py,sha256=Hp7tBiDycdOh8LuRoISsOZycD7DufDAMKyF_hqEjh6M,929
18
19
  fakesnow/py.typed,sha256=B-DLSjYBi7pkKjwxCSdpVj2J02wgfJr-E7B1wOUyxYU,80
19
20
  fakesnow/rowtype.py,sha256=QUp8EaXD5LT0Xv8BXk5ze4WseEn52xoJ6R05pJjs5mM,2729
20
- fakesnow/server.py,sha256=PGNuYEpmI0L0ZrwBCP1pDTc_lnFrtfSnlZ6zyVuCTqk,7173
21
+ fakesnow/server.py,sha256=anlieEI2WO81rDv7h5-M10CetYL_HJFXWZO9wScr0Dw,7196
21
22
  fakesnow/variables.py,sha256=BGnD4LAdVByfJ2GXL6qpGBaTF8ZJRjt3pdJsd9sIAcw,3134
22
- fakesnow/transforms/__init__.py,sha256=OE-dunCuum8lv832s2cjEzThgBnpKELo5aaTXD_bMNg,2807
23
+ fakesnow/transforms/__init__.py,sha256=3dpcXjeLkfc4e9rO0G8Mnv9zZgmwsKSwQ7UJeyLYnOg,2837
23
24
  fakesnow/transforms/merge.py,sha256=H2yYyzGsxjpggS6PY91rAUtYCFuOkBm8uGi0EO0r6T0,8375
24
- fakesnow/transforms/show.py,sha256=ejvs9S2l2Wcal4fhnNSVs3JkZwKsFxMEU35ufUV3-kg,20421
25
- fakesnow/transforms/stage.py,sha256=FSIyI5kpthD_pdbVfzYCrby5HaikMZUpdRr6oBSrgk4,6176
26
- fakesnow/transforms/transforms.py,sha256=t99pHmNm8aG89o738zVSXCG6a0dOXpnY1OqAz28EQHc,48072
27
- fakesnow-0.9.41.dist-info/licenses/LICENSE,sha256=kW-7NWIyaRMQiDpryfSmF2DObDZHGR1cJZ39s6B1Svg,11344
25
+ fakesnow/transforms/show.py,sha256=NIJDkf4-ns4VTw0DQphEn2DnwxQjXyTU2wS9VSUh6Wc,19919
26
+ fakesnow/transforms/stage.py,sha256=yBC0jMmUYhmHY5Of08lPxgH9R6arNUWAjL9M4ArDnjY,10344
27
+ fakesnow/transforms/transforms.py,sha256=ykzay7J28MsifhRKhm5qJJd__hMcLNVZTT14MSdWcJs,49102
28
+ fakesnow-0.9.42.dist-info/licenses/LICENSE,sha256=kW-7NWIyaRMQiDpryfSmF2DObDZHGR1cJZ39s6B1Svg,11344
28
29
  tools/decode.py,sha256=kC5kUvLQxdCkMRsnH6BqCajlKxKeN77w6rwCKsY6gqU,1781
29
- fakesnow-0.9.41.dist-info/METADATA,sha256=cOvEP3Mo_H0yn37XeRPozuvwY6OS7tqDnleGP8Vgplg,20680
30
- fakesnow-0.9.41.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
31
- fakesnow-0.9.41.dist-info/entry_points.txt,sha256=2riAUgu928ZIHawtO8EsfrMEJhi-EH-z_Vq7Q44xKPM,47
32
- fakesnow-0.9.41.dist-info/top_level.txt,sha256=Yos7YveA3f03xVYuURqnBsfMV2DePXfu_yGcsj3pPzI,30
33
- fakesnow-0.9.41.dist-info/RECORD,,
30
+ fakesnow-0.9.42.dist-info/METADATA,sha256=tYGXY_3aBbYYkLyFlajE1ebKbXT4xhyain0JLC9vfb0,20680
31
+ fakesnow-0.9.42.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
32
+ fakesnow-0.9.42.dist-info/entry_points.txt,sha256=2riAUgu928ZIHawtO8EsfrMEJhi-EH-z_Vq7Q44xKPM,47
33
+ fakesnow-0.9.42.dist-info/top_level.txt,sha256=Yos7YveA3f03xVYuURqnBsfMV2DePXfu_yGcsj3pPzI,30
34
+ fakesnow-0.9.42.dist-info/RECORD,,