fakesnow 0.5.1__py3-none-any.whl → 0.7.0__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/__init__.py CHANGED
@@ -30,8 +30,9 @@ def patch(
30
30
  - snowflake.connector.pandas_tools.write_pandas
31
31
 
32
32
  Args:
33
- extra_targets (Sequence[types.ModuleType], optional): Extra targets to patch. Defaults to [].
34
- auto_create_database
33
+ extra_targets (str | Sequence[str], optional): Extra targets to patch. Defaults to [].
34
+ create_database_on_connect (bool, optional): Create database if provided in connection. Defaults to True.
35
+ create_schema_on_connect (bool, optional): Create schema if provided in connection. Defaults to True.
35
36
 
36
37
  Allows extra targets beyond the standard snowflake.connector targets to be patched. Needed because we cannot
37
38
  patch definitions, only usages, see https://docs.python.org/3/library/unittest.mock.html#where-to-patch
@@ -65,10 +66,8 @@ def patch(
65
66
  for im in std_targets + list([extra_targets] if isinstance(extra_targets, str) else extra_targets):
66
67
  module_name = ".".join(im.split(".")[:-1])
67
68
  fn_name = im.split(".")[-1]
68
- module = sys.modules.get(module_name)
69
- if not module:
70
- # module may not be loaded yet, try to import it
71
- module = importlib.import_module(module_name)
69
+ # get module or try to import it if not loaded yet
70
+ module = sys.modules.get(module_name) or importlib.import_module(module_name)
72
71
  fn = module.__dict__.get(fn_name)
73
72
  assert fn, f"No module var {im}"
74
73
 
fakesnow/fakes.py CHANGED
@@ -10,6 +10,7 @@ if TYPE_CHECKING:
10
10
  import pandas as pd
11
11
  import pyarrow.lib
12
12
  import pyarrow
13
+ import snowflake.connector.converter
13
14
  import snowflake.connector.errors
14
15
  import sqlglot
15
16
  from duckdb import DuckDBPyConnection
@@ -47,6 +48,7 @@ class FakeSnowflakeCursor:
47
48
  self._last_sql = None
48
49
  self._last_params = None
49
50
  self._sqlstate = None
51
+ self._converter = snowflake.connector.converter.SnowflakeConverter()
50
52
 
51
53
  def __enter__(self) -> Self:
52
54
  return self
@@ -68,9 +70,9 @@ class FakeSnowflakeCursor:
68
70
  list[ResultMetadata]: _description_
69
71
  """
70
72
 
71
- describe = transforms.as_describe(parse_one(command, read="snowflake"))
73
+ describe = f"DESCRIBE {command}"
72
74
  self.execute(describe, *args, **kwargs)
73
- return FakeSnowflakeCursor._describe_as_result_metadata(self._duck_conn.fetchall()) # noqa: SLF001
75
+ return FakeSnowflakeCursor._describe_as_result_metadata(self._duck_conn.fetchall())
74
76
 
75
77
  @property
76
78
  def description(self) -> list[ResultMetadata]:
@@ -82,13 +84,13 @@ class FakeSnowflakeCursor:
82
84
  # match database and schema used on the main connection
83
85
  cur.execute(f"SET SCHEMA = '{self._conn.database}.{self._conn.schema}'")
84
86
  cur.execute(f"DESCRIBE {self._last_sql}", self._last_params)
85
- meta = FakeSnowflakeCursor._describe_as_result_metadata(cur.fetchall()) # noqa: SLF001
87
+ meta = FakeSnowflakeCursor._describe_as_result_metadata(cur.fetchall())
86
88
 
87
89
  return meta # type: ignore see https://github.com/duckdb/duckdb/issues/7816
88
90
 
89
91
  def execute(
90
92
  self,
91
- command: str | exp.Expression,
93
+ command: str,
92
94
  params: Sequence[Any] | dict[Any, Any] | None = None,
93
95
  *args: Any,
94
96
  **kwargs: Any,
@@ -102,17 +104,15 @@ class FakeSnowflakeCursor:
102
104
 
103
105
  def _execute(
104
106
  self,
105
- command: str | exp.Expression,
107
+ command: str,
106
108
  params: Sequence[Any] | dict[Any, Any] | None = None,
107
109
  *args: Any,
108
110
  **kwargs: Any,
109
111
  ) -> FakeSnowflakeCursor:
110
112
  self._arrow_table = None
111
113
 
112
- if isinstance(command, exp.Expression):
113
- expression = command
114
- else:
115
- expression = parse_one(self._rewrite_params(command, params), read="snowflake")
114
+ command, params = self._rewrite_with_params(command, params)
115
+ expression = parse_one(command, read="snowflake")
116
116
 
117
117
  cmd = expr.key_command(expression)
118
118
 
@@ -148,6 +148,7 @@ class FakeSnowflakeCursor:
148
148
  .transform(transforms.regex_substr)
149
149
  .transform(transforms.values_columns)
150
150
  .transform(transforms.to_date)
151
+ .transform(transforms.to_decimal)
151
152
  .transform(transforms.object_construct)
152
153
  .transform(transforms.timestamp_ntz_ns)
153
154
  .transform(transforms.float_to_double)
@@ -319,6 +320,14 @@ class FakeSnowflakeCursor:
319
320
  return ResultMetadata(
320
321
  name=column_name, type_code=12, display_size=None, internal_size=None, precision=0, scale=9, is_nullable=True # noqa: E501
321
322
  )
323
+ elif column_type == "JSON[]":
324
+ return ResultMetadata(
325
+ name=column_name, type_code=10, display_size=None, internal_size=None, precision=None, scale=None, is_nullable=True # noqa: E501
326
+ )
327
+ elif column_type == "JSON":
328
+ return ResultMetadata(
329
+ name=column_name, type_code=9, display_size=None, internal_size=None, precision=None, scale=None, is_nullable=True # noqa: E501
330
+ )
322
331
  else:
323
332
  # TODO handle more types
324
333
  raise NotImplementedError(f"for column type {column_type}")
@@ -331,20 +340,25 @@ class FakeSnowflakeCursor:
331
340
  ]
332
341
  return meta
333
342
 
334
- def _rewrite_params(
343
+ def _rewrite_with_params(
335
344
  self,
336
345
  command: str,
337
346
  params: Sequence[Any] | dict[Any, Any] | None = None,
338
- ) -> str:
339
- if isinstance(params, dict):
340
- # see https://docs.snowflake.com/en/developer-guide/python-connector/python-connector-api
341
- raise NotImplementedError("dict params not supported yet")
342
-
347
+ ) -> tuple[str, Sequence[Any] | dict[Any, Any] | None]:
343
348
  if params and self._conn._paramstyle in ("pyformat", "format"): # noqa: SLF001
344
- # duckdb uses question mark style params
345
- return command.replace("%s", "?")
349
+ # handle client-side in the same manner as the snowflake python connector
350
+
351
+ def convert(param: Any) -> Any: # noqa: ANN401
352
+ return self._converter.quote(self._converter.escape(self._converter.to_snowflake(param)))
353
+
354
+ if isinstance(params, dict):
355
+ params = {k: convert(v) for k, v in params.items()}
356
+ else:
357
+ params = tuple(convert(v) for v in params)
358
+
359
+ return command % params, None
346
360
 
347
- return command
361
+ return command, params
348
362
 
349
363
 
350
364
  class FakeSnowflakeConnection:
fakesnow/info_schema.py CHANGED
@@ -40,6 +40,8 @@ case when starts_with(data_type, 'DECIMAL') or data_type='BIGINT' then 'NUMBER'
40
40
  when data_type='DOUBLE' then 'FLOAT'
41
41
  when data_type='BLOB' then 'BINARY'
42
42
  when data_type='TIMESTAMP' then 'TIMESTAMP_NTZ'
43
+ when data_type='JSON[]' then 'ARRAY'
44
+ when data_type='JSON' then 'OBJECT'
43
45
  else data_type end as data_type,
44
46
  ext_character_maximum_length as character_maximum_length, ext_character_octet_length as character_octet_length,
45
47
  case when data_type='BIGINT' then 38
@@ -57,12 +59,31 @@ AND ext_table_name = table_name AND ext_column_name = column_name
57
59
  """
58
60
  )
59
61
 
62
+ # replicates https://docs.snowflake.com/sql-reference/info-schema/databases
63
+ SQL_CREATE_INFORMATION_SCHEMA_DATABASES_VIEW = Template(
64
+ """
65
+ create view ${catalog}.information_schema.databases AS
66
+ select
67
+ catalog_name as database_name,
68
+ 'SYSADMIN' as database_owner,
69
+ 'NO' as is_transient,
70
+ null as comment,
71
+ to_timestamp(0)::timestamptz as created,
72
+ to_timestamp(0)::timestamptz as last_altered,
73
+ 1 as retention_time,
74
+ 'STANDARD' as type
75
+ from information_schema.schemata
76
+ where catalog_name not in ('memory', 'system', 'temp') and schema_name = 'information_schema'
77
+ """
78
+ )
79
+
60
80
 
61
81
  def creation_sql(catalog: str) -> str:
62
82
  return f"""
63
83
  {SQL_CREATE_INFORMATION_SCHEMA_TABLES_EXT.substitute(catalog=catalog)};
64
84
  {SQL_CREATE_INFORMATION_SCHEMA_COLUMNS_EXT.substitute(catalog=catalog)};
65
85
  {SQL_CREATE_INFORMATION_SCHEMA_COLUMNS_VIEW.substitute(catalog=catalog)};
86
+ {SQL_CREATE_INFORMATION_SCHEMA_DATABASES_VIEW.substitute(catalog=catalog)};
66
87
  """
67
88
 
68
89
 
fakesnow/transforms.py CHANGED
@@ -10,23 +10,6 @@ MISSING_DATABASE = "missing_database"
10
10
  SUCCESS_NOP = sqlglot.parse_one("SELECT 'Statement executed successfully.'")
11
11
 
12
12
 
13
- def as_describe(expression: exp.Expression) -> exp.Expression:
14
- """Prepend describe to the expression.
15
-
16
- Example:
17
- >>> import sqlglot
18
- >>> sqlglot.parse_one("SELECT name FROM CUSTOMERS").transform(as_describe).sql()
19
- 'describe SELECT name FROM CUSTOMERS'
20
- Args:
21
- expression (exp.Expression): the expression that will be transformed.
22
-
23
- Returns:
24
- exp.Expression: The transformed expression.
25
- """
26
-
27
- return exp.Describe(this=expression)
28
-
29
-
30
13
  # TODO: move this into a Dialect as a transpilation
31
14
  def create_database(expression: exp.Expression) -> exp.Expression:
32
15
  """Transform create database to attach database.
@@ -535,6 +518,34 @@ def to_date(expression: exp.Expression) -> exp.Expression:
535
518
  return expression
536
519
 
537
520
 
521
+ def to_decimal(expression: exp.Expression) -> exp.Expression:
522
+ """Transform to_decimal, to_number, to_numeric expressions from snowflake to duckdb.
523
+
524
+ See https://docs.snowflake.com/en/sql-reference/functions/to_decimal
525
+ """
526
+
527
+ if (
528
+ isinstance(expression, exp.Anonymous)
529
+ and isinstance(expression.this, str)
530
+ and expression.this.upper() in ["TO_DECIMAL", "TO_NUMBER", "TO_NUMERIC"]
531
+ ):
532
+ expressions: list[exp.Expression] = expression.expressions
533
+
534
+ if len(expressions) > 1 and expressions[1].is_string:
535
+ # see https://docs.snowflake.com/en/sql-reference/functions/to_decimal#arguments
536
+ raise NotImplementedError(f"{expression.this} with format argument")
537
+
538
+ precision = expressions[1] if len(expressions) > 1 else exp.Literal(this="38", is_string=False)
539
+ scale = expressions[2] if len(expressions) > 2 else exp.Literal(this="0", is_string=False)
540
+
541
+ return exp.Cast(
542
+ this=expressions[0],
543
+ to=exp.DataType(this=exp.DataType.Type.DECIMAL, expressions=[precision, scale], nested=False, prefix=False),
544
+ )
545
+
546
+ return expression
547
+
548
+
538
549
  def timestamp_ntz_ns(expression: exp.Expression) -> exp.Expression:
539
550
  """Convert timestamp_ntz(9) to timestamp_ntz.
540
551
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fakesnow
3
- Version: 0.5.1
3
+ Version: 0.7.0
4
4
  Summary: Fake Snowflake Connector for Python. Run Snowflake DB locally.
5
5
  License: MIT License
6
6
 
@@ -29,18 +29,18 @@ Classifier: License :: OSI Approved :: MIT License
29
29
  Requires-Python: >=3.9
30
30
  Description-Content-Type: text/markdown
31
31
  License-File: LICENSE
32
- Requires-Dist: duckdb (~=0.8.0)
32
+ Requires-Dist: duckdb ~=0.8.0
33
33
  Requires-Dist: pyarrow
34
34
  Requires-Dist: snowflake-connector-python
35
- Requires-Dist: sqlglot (~=16.8.1)
35
+ Requires-Dist: sqlglot ~=16.8.1
36
36
  Provides-Extra: dev
37
- Requires-Dist: black (~=23.3) ; extra == 'dev'
38
- Requires-Dist: build (~=0.10) ; extra == 'dev'
37
+ Requires-Dist: black ~=23.3 ; extra == 'dev'
38
+ Requires-Dist: build ~=0.10 ; extra == 'dev'
39
39
  Requires-Dist: snowflake-connector-python[pandas,secure-local-storage] ; extra == 'dev'
40
- Requires-Dist: pre-commit (~=3.2) ; extra == 'dev'
41
- Requires-Dist: pytest (~=7.3) ; extra == 'dev'
42
- Requires-Dist: ruff (~=0.0.263) ; extra == 'dev'
43
- Requires-Dist: twine (~=4.0) ; extra == 'dev'
40
+ Requires-Dist: pre-commit ~=3.2 ; extra == 'dev'
41
+ Requires-Dist: pytest ~=7.3 ; extra == 'dev'
42
+ Requires-Dist: ruff ~=0.0.285 ; extra == 'dev'
43
+ Requires-Dist: twine ~=4.0 ; extra == 'dev'
44
44
  Provides-Extra: notebook
45
45
  Requires-Dist: duckdb-engine ; extra == 'notebook'
46
46
  Requires-Dist: ipykernel ; extra == 'notebook'
@@ -128,7 +128,7 @@ def _fakesnow_session() -> Iterator[None]:
128
128
  - [x] [get_result_batches()](https://docs.snowflake.com/en/user-guide/python-connector-api#get_result_batches)
129
129
  - [x] information schema
130
130
  - [x] multiple databases
131
- - [x] [qmark binding](https://docs.snowflake.com/en/user-guide/python-connector-example#binding-data)
131
+ - [x] [parameter binding](https://docs.snowflake.com/en/user-guide/python-connector-example#binding-data)
132
132
  - [x] table comments
133
133
  - [x] [write_pandas(..)](https://docs.snowflake.com/en/user-guide/python-connector-api#write_pandas)
134
134
  - [ ] [access control](https://docs.snowflake.com/en/user-guide/security-access-control-overview)
@@ -144,6 +144,10 @@ Partial support
144
144
 
145
145
  For more detail see [tests/test_fakes.py](tests/test_fakes.py)
146
146
 
147
+ ## Caveats
148
+
149
+ - VARCHAR field sizes are not enforced unlike Snowflake which will error with "User character length limit (xxx) exceeded by string" when you try to insert a string longer than the column limit.
150
+
147
151
  ## Contributing
148
152
 
149
153
  See [CONTRIBUTING.md](CONTRIBUTING.md) to get started and develop in this repo.
@@ -0,0 +1,13 @@
1
+ fakesnow/__init__.py,sha256=mjtMo58BOxp9LU6pFPyVgl6GYtoF_tnaX5UqmPINVlU,3137
2
+ fakesnow/checks.py,sha256=1qVLR0ZB3z3UPij3Hm8hqlkcNLH2QJnwe8OqkoFCwv8,2356
3
+ fakesnow/expr.py,sha256=CAxuYIUkwI339DQIBzvFF0F-m1tcVGKEPA5rDTzmH9A,892
4
+ fakesnow/fakes.py,sha256=xlhuB0teuW3ftNUra93h1HiiRwyupDnoqQYW8RosGwA,21508
5
+ fakesnow/fixtures.py,sha256=LANb4LuiUjKbTZRHmgnAi50xC1rs1xF8SHLoBikB88c,509
6
+ fakesnow/info_schema.py,sha256=3lkpRI_ByXbA1PwZ3hh4PtB9aLAsRbkI4MM_hTaOce8,4544
7
+ fakesnow/py.typed,sha256=B-DLSjYBi7pkKjwxCSdpVj2J02wgfJr-E7B1wOUyxYU,80
8
+ fakesnow/transforms.py,sha256=v7-Erd5cyK4dqw5jXkkQKIWp9j7lYeWaGhFeAnjGQsY,22962
9
+ fakesnow-0.7.0.dist-info/LICENSE,sha256=BL6v_VTnU7xdsocviIQJMFr3stX_-uRfTyByo3gRu4M,1071
10
+ fakesnow-0.7.0.dist-info/METADATA,sha256=UMNSLxz4REM3ha9JGd-dh_y06fv6C8EczHyI_SFo1bo,5421
11
+ fakesnow-0.7.0.dist-info/WHEEL,sha256=yQN5g4mg4AybRjkgi-9yy4iQEFibGQmlz78Pik5Or-A,92
12
+ fakesnow-0.7.0.dist-info/top_level.txt,sha256=x8S-sMmvfgNm2_1w0zlIF5YlDs2hR7eNQdVA6TgmPZE,14
13
+ fakesnow-0.7.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.41.0)
2
+ Generator: bdist_wheel (0.41.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,13 +0,0 @@
1
- fakesnow/__init__.py,sha256=ogZl2Z61FIiU2fg8WiM1FhXYLpG-Phvg855nmuMhdqQ,2992
2
- fakesnow/checks.py,sha256=1qVLR0ZB3z3UPij3Hm8hqlkcNLH2QJnwe8OqkoFCwv8,2356
3
- fakesnow/expr.py,sha256=CAxuYIUkwI339DQIBzvFF0F-m1tcVGKEPA5rDTzmH9A,892
4
- fakesnow/fakes.py,sha256=v6t0sLy9KsiiUW-KyRggamt7K7ADEw8uSXdQt6HmQN4,20760
5
- fakesnow/fixtures.py,sha256=LANb4LuiUjKbTZRHmgnAi50xC1rs1xF8SHLoBikB88c,509
6
- fakesnow/info_schema.py,sha256=0eJgDJ0sbEUVNcfGBb5hdCR2SiKJYmO0MA-ZnnsUhbo,3781
7
- fakesnow/py.typed,sha256=B-DLSjYBi7pkKjwxCSdpVj2J02wgfJr-E7B1wOUyxYU,80
8
- fakesnow/transforms.py,sha256=PTK3RA1SEiCT9urw97tLyhzXZuWZx5hCUWcvPO2DIwI,22305
9
- fakesnow-0.5.1.dist-info/LICENSE,sha256=BL6v_VTnU7xdsocviIQJMFr3stX_-uRfTyByo3gRu4M,1071
10
- fakesnow-0.5.1.dist-info/METADATA,sha256=H2Lo5Okt1MPsbpBW2CfeDYX9Yti-d8sTUOWFNAaHW8k,5225
11
- fakesnow-0.5.1.dist-info/WHEEL,sha256=AtBG6SXL3KF_v0NxLf0ehyVOh0cold-JbJYXNGorC6Q,92
12
- fakesnow-0.5.1.dist-info/top_level.txt,sha256=x8S-sMmvfgNm2_1w0zlIF5YlDs2hR7eNQdVA6TgmPZE,14
13
- fakesnow-0.5.1.dist-info/RECORD,,