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 +5 -6
- fakesnow/fakes.py +32 -18
- fakesnow/info_schema.py +21 -0
- fakesnow/transforms.py +28 -17
- {fakesnow-0.5.1.dist-info → fakesnow-0.7.0.dist-info}/METADATA +14 -10
- fakesnow-0.7.0.dist-info/RECORD +13 -0
- {fakesnow-0.5.1.dist-info → fakesnow-0.7.0.dist-info}/WHEEL +1 -1
- fakesnow-0.5.1.dist-info/RECORD +0 -13
- {fakesnow-0.5.1.dist-info → fakesnow-0.7.0.dist-info}/LICENSE +0 -0
- {fakesnow-0.5.1.dist-info → fakesnow-0.7.0.dist-info}/top_level.txt +0 -0
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[
|
34
|
-
|
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
|
69
|
-
|
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 =
|
73
|
+
describe = f"DESCRIBE {command}"
|
72
74
|
self.execute(describe, *args, **kwargs)
|
73
|
-
return FakeSnowflakeCursor._describe_as_result_metadata(self._duck_conn.fetchall())
|
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())
|
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
|
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
|
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
|
-
|
113
|
-
|
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
|
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
|
-
#
|
345
|
-
|
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.
|
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
|
32
|
+
Requires-Dist: duckdb ~=0.8.0
|
33
33
|
Requires-Dist: pyarrow
|
34
34
|
Requires-Dist: snowflake-connector-python
|
35
|
-
Requires-Dist: sqlglot
|
35
|
+
Requires-Dist: sqlglot ~=16.8.1
|
36
36
|
Provides-Extra: dev
|
37
|
-
Requires-Dist: black
|
38
|
-
Requires-Dist: build
|
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
|
41
|
-
Requires-Dist: pytest
|
42
|
-
Requires-Dist: ruff
|
43
|
-
Requires-Dist: twine
|
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] [
|
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,,
|
fakesnow-0.5.1.dist-info/RECORD
DELETED
@@ -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,,
|
File without changes
|
File without changes
|