fakesnow 0.9.21__py3-none-any.whl → 0.9.23__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/info_schema.py CHANGED
@@ -78,6 +78,7 @@ LEFT JOIN duckdb_columns ddb_columns
78
78
  """
79
79
  )
80
80
 
81
+
81
82
  # replicates https://docs.snowflake.com/sql-reference/info-schema/databases
82
83
  SQL_CREATE_INFORMATION_SCHEMA_DATABASES_VIEW = Template(
83
84
  """
@@ -0,0 +1,77 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Sequence
4
+ from typing import TYPE_CHECKING, Any, Literal, Optional
5
+
6
+ import numpy as np
7
+
8
+ if TYPE_CHECKING:
9
+ import pandas as pd
10
+
11
+ from fakesnow.conn import FakeSnowflakeConnection
12
+
13
+ CopyResult = tuple[
14
+ str,
15
+ str,
16
+ int,
17
+ int,
18
+ int,
19
+ int,
20
+ Optional[str],
21
+ Optional[int],
22
+ Optional[int],
23
+ Optional[str],
24
+ ]
25
+
26
+ WritePandasResult = tuple[
27
+ bool,
28
+ int,
29
+ int,
30
+ Sequence[CopyResult],
31
+ ]
32
+
33
+
34
+ def sql_type(dtype: np.dtype) -> str:
35
+ if str(dtype) == "int64":
36
+ return "NUMBER"
37
+ elif str(dtype) == "object":
38
+ return "VARCHAR"
39
+ else:
40
+ raise NotImplementedError(f"sql_type {dtype=}")
41
+
42
+
43
+ def write_pandas(
44
+ conn: FakeSnowflakeConnection,
45
+ df: pd.DataFrame,
46
+ table_name: str,
47
+ database: str | None = None,
48
+ schema: str | None = None,
49
+ chunk_size: int | None = None,
50
+ compression: str = "gzip",
51
+ on_error: str = "abort_statement",
52
+ parallel: int = 4,
53
+ quote_identifiers: bool = True,
54
+ auto_create_table: bool = False,
55
+ create_temp_table: bool = False,
56
+ overwrite: bool = False,
57
+ table_type: Literal["", "temp", "temporary", "transient"] = "",
58
+ **kwargs: Any,
59
+ ) -> WritePandasResult:
60
+ name = table_name
61
+ if schema:
62
+ name = f"{schema}.{name}"
63
+ if database:
64
+ name = f"{database}.{name}"
65
+
66
+ if auto_create_table:
67
+ cols = [f"{c} {sql_type(t)}" for c, t in df.dtypes.to_dict().items()]
68
+
69
+ conn.cursor().execute(f"CREATE TABLE IF NOT EXISTS {name} ({','.join(cols)})")
70
+
71
+ count = conn._insert_df(df, name) # noqa: SLF001
72
+
73
+ # mocks https://docs.snowflake.com/en/sql-reference/sql/copy-into-table.html#output
74
+ mock_copy_results = [("fakesnow/file0.txt", "LOADED", count, count, 1, 0, None, None, None, None)]
75
+
76
+ # return success
77
+ return (True, len(mock_copy_results), count, mock_copy_results)
fakesnow/server.py CHANGED
@@ -12,7 +12,7 @@ from starlette.requests import Request
12
12
  from starlette.responses import JSONResponse
13
13
  from starlette.routing import Route
14
14
 
15
- from fakesnow.arrow import to_ipc
15
+ from fakesnow.arrow import to_ipc, to_rowtype, with_sf_metadata
16
16
  from fakesnow.fakes import FakeSnowflakeConnection
17
17
  from fakesnow.instance import FakeSnow
18
18
 
@@ -52,19 +52,13 @@ async def query_request(request: Request) -> JSONResponse:
52
52
  batch_bytes = to_ipc(cur._arrow_table) # noqa: SLF001
53
53
  rowset_b64 = b64encode(batch_bytes).decode("utf-8")
54
54
 
55
+ # TODO: avoid calling with_sf_metadata twice
56
+ rowtype = to_rowtype(with_sf_metadata(cur._arrow_table.schema)) # noqa: SLF001
57
+
55
58
  return JSONResponse(
56
59
  {
57
60
  "data": {
58
- "rowtype": [
59
- {
60
- "name": "'HELLO WORLD'",
61
- "nullable": False,
62
- "type": "text",
63
- "length": 11,
64
- "scale": None,
65
- "precision": None,
66
- }
67
- ],
61
+ "rowtype": rowtype,
68
62
  "rowsetBase64": rowset_b64,
69
63
  "total": 1,
70
64
  "queryResultFormat": "arrow",
@@ -103,6 +97,7 @@ routes = [
103
97
  query_request,
104
98
  methods=["POST"],
105
99
  ),
100
+ Route("/queries/v1/abort-request", lambda _: JSONResponse({"success": True}), methods=["POST"]),
106
101
  ]
107
102
 
108
103
  app = Starlette(debug=True, routes=routes)
fakesnow/transforms.py CHANGED
@@ -159,22 +159,41 @@ SELECT
159
159
  column_default AS "default",
160
160
  'N' AS "primary key",
161
161
  'N' AS "unique key",
162
- NULL AS "check",
163
- NULL AS "expression",
164
- NULL AS "comment",
165
- NULL AS "policy name",
166
- NULL AS "privacy domain",
162
+ NULL::VARCHAR AS "check",
163
+ NULL::VARCHAR AS "expression",
164
+ NULL::VARCHAR AS "comment",
165
+ NULL::VARCHAR AS "policy name",
166
+ NULL::JSON AS "privacy domain",
167
167
  FROM information_schema._fs_columns_snowflake
168
168
  WHERE table_catalog = '${catalog}' AND table_schema = '${schema}' AND table_name = '${table}'
169
169
  ORDER BY ordinal_position
170
170
  """
171
171
  )
172
172
 
173
+ SQL_DESCRIBE_INFO_SCHEMA = Template(
174
+ """
175
+ SELECT
176
+ column_name AS "name",
177
+ column_type as "type",
178
+ 'COLUMN' AS "kind",
179
+ CASE WHEN "null" = 'YES' THEN 'Y' ELSE 'N' END AS "null?",
180
+ NULL::VARCHAR AS "default",
181
+ 'N' AS "primary key",
182
+ 'N' AS "unique key",
183
+ NULL::VARCHAR AS "check",
184
+ NULL::VARCHAR AS "expression",
185
+ NULL::VARCHAR AS "comment",
186
+ NULL::VARCHAR AS "policy name",
187
+ NULL::JSON AS "privacy domain",
188
+ FROM (DESCRIBE information_schema.${view})
189
+ """
190
+ )
191
+
173
192
 
174
193
  def describe_table(
175
194
  expression: exp.Expression, current_database: str | None = None, current_schema: str | None = None
176
195
  ) -> exp.Expression:
177
- """Redirect to the information_schema._fs_describe_table to match snowflake.
196
+ """Redirect to the information_schema._fs_columns_snowflake to match snowflake.
178
197
 
179
198
  See https://docs.snowflake.com/en/sql-reference/sql/desc-table
180
199
  """
@@ -183,12 +202,16 @@ def describe_table(
183
202
  isinstance(expression, exp.Describe)
184
203
  and (kind := expression.args.get("kind"))
185
204
  and isinstance(kind, str)
186
- and kind.upper() == "TABLE"
205
+ and kind.upper() in ("TABLE", "VIEW")
187
206
  and (table := expression.find(exp.Table))
188
207
  ):
189
208
  catalog = table.catalog or current_database
190
209
  schema = table.db or current_schema
191
210
 
211
+ if schema and schema.upper() == "INFORMATION_SCHEMA":
212
+ # information schema views don't exist in _fs_columns_snowflake
213
+ return sqlglot.parse_one(SQL_DESCRIBE_INFO_SCHEMA.substitute(view=table.name), read="duckdb")
214
+
192
215
  return sqlglot.parse_one(
193
216
  SQL_DESCRIBE_TABLE.substitute(catalog=catalog, schema=schema, table=table.name),
194
217
  read="duckdb",
fakesnow/types.py ADDED
@@ -0,0 +1,89 @@
1
+ import re
2
+ from typing import Optional, TypedDict
3
+
4
+ from snowflake.connector.cursor import ResultMetadata
5
+
6
+
7
+ class ColumnInfo(TypedDict):
8
+ name: str
9
+ database: str
10
+ schema: str
11
+ table: str
12
+ nullable: bool
13
+ type: str
14
+ byteLength: Optional[int]
15
+ length: Optional[int]
16
+ scale: Optional[int]
17
+ precision: Optional[int]
18
+ collation: Optional[str]
19
+
20
+
21
+ duckdb_to_sf_type = {
22
+ "BIGINT": "fixed",
23
+ "BLOB": "binary",
24
+ "BOOLEAN": "boolean",
25
+ "DATE": "date",
26
+ "DECIMAL": "fixed",
27
+ "DOUBLE": "real",
28
+ "INTEGER": "fixed",
29
+ "JSON": "variant",
30
+ "TIME": "time",
31
+ "TIMESTAMP WITH TIME ZONE": "timestamp_tz",
32
+ "TIMESTAMP_NS": "timestamp_ntz",
33
+ "TIMESTAMP": "timestamp_ntz",
34
+ "VARCHAR": "text",
35
+ }
36
+
37
+
38
+ def describe_as_rowtype(describe_results: list) -> list[ColumnInfo]:
39
+ """Convert duckdb column type to snowflake rowtype returned by the API."""
40
+
41
+ def as_column_info(column_name: str, column_type: str) -> ColumnInfo:
42
+ if not (sf_type := duckdb_to_sf_type.get("DECIMAL" if column_type.startswith("DECIMAL") else column_type)):
43
+ raise NotImplementedError(f"for column type {column_type}")
44
+
45
+ info: ColumnInfo = {
46
+ "name": column_name,
47
+ # TODO
48
+ "database": "",
49
+ "schema": "",
50
+ "table": "",
51
+ # TODO
52
+ "nullable": True,
53
+ "type": sf_type,
54
+ "byteLength": None,
55
+ "length": None,
56
+ "scale": None,
57
+ "precision": None,
58
+ "collation": None,
59
+ }
60
+
61
+ if column_type.startswith("DECIMAL"):
62
+ match = re.search(r"\((\d+),(\d+)\)", column_type)
63
+ info["precision"] = int(match[1]) if match else 38
64
+ info["scale"] = int(match[2]) if match else 0
65
+ elif sf_type == "fixed":
66
+ info["precision"] = 38
67
+ info["scale"] = 0
68
+ elif sf_type == "text":
69
+ # TODO: fetch actual varchar size
70
+ info["byteLength"] = 16777216
71
+ info["length"] = 16777216
72
+ elif sf_type.startswith("time"):
73
+ info["precision"] = 0
74
+ info["scale"] = 9
75
+ elif sf_type == "binary":
76
+ info["byteLength"] = 8388608
77
+ info["length"] = 8388608
78
+
79
+ return info
80
+
81
+ column_infos = [
82
+ as_column_info(column_name, column_type)
83
+ for (column_name, column_type, _null, _key, _default, _extra) in describe_results
84
+ ]
85
+ return column_infos
86
+
87
+
88
+ def describe_as_result_metadata(describe_results: list) -> list[ResultMetadata]:
89
+ return [ResultMetadata.from_column(c) for c in describe_as_rowtype(describe_results)] # pyright: ignore[reportArgumentType]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fakesnow
3
- Version: 0.9.21
3
+ Version: 0.9.23
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
@@ -210,28 +210,29 @@ Classifier: License :: OSI Approved :: MIT License
210
210
  Requires-Python: >=3.9
211
211
  Description-Content-Type: text/markdown
212
212
  License-File: LICENSE
213
- Requires-Dist: duckdb ~=1.0.0
213
+ Requires-Dist: duckdb~=1.0.0
214
214
  Requires-Dist: pyarrow
215
215
  Requires-Dist: snowflake-connector-python
216
- Requires-Dist: sqlglot ~=25.5.1
216
+ Requires-Dist: sqlglot~=25.9.0
217
217
  Provides-Extra: dev
218
- Requires-Dist: build ~=1.0 ; extra == 'dev'
219
- Requires-Dist: pandas-stubs ; extra == 'dev'
220
- Requires-Dist: snowflake-connector-python[pandas,secure-local-storage] ; extra == 'dev'
221
- Requires-Dist: pre-commit ~=3.4 ; extra == 'dev'
222
- Requires-Dist: pyarrow-stubs ; extra == 'dev'
223
- Requires-Dist: pytest ~=8.0 ; extra == 'dev'
224
- Requires-Dist: pytest-asyncio ; extra == 'dev'
225
- Requires-Dist: ruff ~=0.5.1 ; extra == 'dev'
226
- Requires-Dist: twine ~=5.0 ; extra == 'dev'
227
- Requires-Dist: snowflake-sqlalchemy ~=1.5.0 ; extra == 'dev'
218
+ Requires-Dist: build~=1.0; extra == "dev"
219
+ Requires-Dist: dirty-equals; extra == "dev"
220
+ Requires-Dist: pandas-stubs; extra == "dev"
221
+ Requires-Dist: snowflake-connector-python[pandas,secure-local-storage]; extra == "dev"
222
+ Requires-Dist: pre-commit~=3.4; extra == "dev"
223
+ Requires-Dist: pyarrow-stubs; extra == "dev"
224
+ Requires-Dist: pytest~=8.0; extra == "dev"
225
+ Requires-Dist: pytest-asyncio; extra == "dev"
226
+ Requires-Dist: ruff~=0.5.1; extra == "dev"
227
+ Requires-Dist: twine~=5.0; extra == "dev"
228
+ Requires-Dist: snowflake-sqlalchemy~=1.5.0; extra == "dev"
228
229
  Provides-Extra: notebook
229
- Requires-Dist: duckdb-engine ; extra == 'notebook'
230
- Requires-Dist: ipykernel ; extra == 'notebook'
231
- Requires-Dist: jupysql ; extra == 'notebook'
230
+ Requires-Dist: duckdb-engine; extra == "notebook"
231
+ Requires-Dist: ipykernel; extra == "notebook"
232
+ Requires-Dist: jupysql; extra == "notebook"
232
233
  Provides-Extra: server
233
- Requires-Dist: starlette ; extra == 'server'
234
- Requires-Dist: uvicorn ; extra == 'server'
234
+ Requires-Dist: starlette; extra == "server"
235
+ Requires-Dist: uvicorn; extra == "server"
235
236
 
236
237
  # fakesnow ❄️
237
238
 
@@ -0,0 +1,25 @@
1
+ fakesnow/__init__.py,sha256=9tFJJKvowKNW3vfnlmza6hOLN1I52DwChgNc5Ew6CcA,3499
2
+ fakesnow/__main__.py,sha256=GDrGyNTvBFuqn_UfDjKs7b3LPtU6gDv1KwosVDrukIM,76
3
+ fakesnow/arrow.py,sha256=WLkr1nEiNxUcPdzadKSM33sRAiQJsN6LvuzTVIsi3D0,2766
4
+ fakesnow/checks.py,sha256=-QMvdcrRbhN60rnzxLBJ0IkUBWyLR8gGGKKmCS0w9mA,2383
5
+ fakesnow/cli.py,sha256=9qfI-Ssr6mo8UmIlXkUAOz2z2YPBgDsrEVaZv9FjGFs,2201
6
+ fakesnow/conn.py,sha256=yR9SMGSKkLvdPfi5fDwj9PQggOrPcKkdBBnQy2y_Bak,6921
7
+ fakesnow/cursor.py,sha256=lITuMMy_hA9_riC121Sv9bFDFbU9BP9NlOLdHlP0ahY,19371
8
+ fakesnow/expr.py,sha256=CAxuYIUkwI339DQIBzvFF0F-m1tcVGKEPA5rDTzmH9A,892
9
+ fakesnow/fakes.py,sha256=JQTiUkkwPeQrJ8FDWhPFPK6pGwd_aR2oiOrNzCWznlM,187
10
+ fakesnow/fixtures.py,sha256=G-NkVeruSQAJ7fvSS2fR2oysUn0Yra1pohHlOvacKEk,455
11
+ fakesnow/info_schema.py,sha256=DObVOrhzppAFHsdtj4YI9oRISn9SkJUG6ONjVleQQ_Y,6303
12
+ fakesnow/instance.py,sha256=3cJvPRuFy19dMKXbtBLl6imzO48pEw8uTYhZyFDuwhk,3133
13
+ fakesnow/macros.py,sha256=pX1YJDnQOkFJSHYUjQ6ErEkYIKvFI6Ncz_au0vv1csA,265
14
+ fakesnow/pandas_tools.py,sha256=ecL0kxIVU5o2--P3bRLWWVhxXqq6Km4trFr36txukMg,1897
15
+ fakesnow/py.typed,sha256=B-DLSjYBi7pkKjwxCSdpVj2J02wgfJr-E7B1wOUyxYU,80
16
+ fakesnow/server.py,sha256=8dzaLUUXPzCMm6-ESn0CBws6XSwwOpnUuHQAZJ-4SwU,3011
17
+ fakesnow/transforms.py,sha256=ellcY5OBc7mqgL9ChNolrqcCLWXF9RH21Jt88FcFl-I,54419
18
+ fakesnow/types.py,sha256=9Tt83Z7ctc9_v6SYyayXYz4MEI4RZo4zq_uqdj4g3Dk,2681
19
+ fakesnow/variables.py,sha256=WXyPnkeNwD08gy52yF66CVe2twiYC50tztNfgXV4q1k,3032
20
+ fakesnow-0.9.23.dist-info/LICENSE,sha256=kW-7NWIyaRMQiDpryfSmF2DObDZHGR1cJZ39s6B1Svg,11344
21
+ fakesnow-0.9.23.dist-info/METADATA,sha256=90vwAf40lg9ipw2urbu9nMq7cEARbtENE0aKd7qSpjo,18064
22
+ fakesnow-0.9.23.dist-info/WHEEL,sha256=eOLhNAGa2EW3wWl_TU484h7q1UNgy0JXjjoqKoxAAQc,92
23
+ fakesnow-0.9.23.dist-info/entry_points.txt,sha256=2riAUgu928ZIHawtO8EsfrMEJhi-EH-z_Vq7Q44xKPM,47
24
+ fakesnow-0.9.23.dist-info/top_level.txt,sha256=500evXI1IFX9so82cizGIEMHAb_dJNPaZvd2H9dcKTA,24
25
+ fakesnow-0.9.23.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.43.0)
2
+ Generator: bdist_wheel (0.44.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,21 +0,0 @@
1
- fakesnow/__init__.py,sha256=9tFJJKvowKNW3vfnlmza6hOLN1I52DwChgNc5Ew6CcA,3499
2
- fakesnow/__main__.py,sha256=GDrGyNTvBFuqn_UfDjKs7b3LPtU6gDv1KwosVDrukIM,76
3
- fakesnow/arrow.py,sha256=1ypCsf-r2Ven6CuSm-bTLoeq1G31kBD6JnaLvDxpwhU,1218
4
- fakesnow/checks.py,sha256=-QMvdcrRbhN60rnzxLBJ0IkUBWyLR8gGGKKmCS0w9mA,2383
5
- fakesnow/cli.py,sha256=9qfI-Ssr6mo8UmIlXkUAOz2z2YPBgDsrEVaZv9FjGFs,2201
6
- fakesnow/expr.py,sha256=CAxuYIUkwI339DQIBzvFF0F-m1tcVGKEPA5rDTzmH9A,892
7
- fakesnow/fakes.py,sha256=wLSjKrNI8wxe3MuUAa97jpUHd5vZTzvrlF1-Hf0FC0M,31208
8
- fakesnow/fixtures.py,sha256=G-NkVeruSQAJ7fvSS2fR2oysUn0Yra1pohHlOvacKEk,455
9
- fakesnow/info_schema.py,sha256=LjS_-8YXBtCSvkdU5uL0aJdFcZEsBa6o5zf-Q_aV9i0,6302
10
- fakesnow/instance.py,sha256=3cJvPRuFy19dMKXbtBLl6imzO48pEw8uTYhZyFDuwhk,3133
11
- fakesnow/macros.py,sha256=pX1YJDnQOkFJSHYUjQ6ErEkYIKvFI6Ncz_au0vv1csA,265
12
- fakesnow/py.typed,sha256=B-DLSjYBi7pkKjwxCSdpVj2J02wgfJr-E7B1wOUyxYU,80
13
- fakesnow/server.py,sha256=HwCAZ5AhU4nRbFGIqDBs2rdwoK70dYZDyw1XfE1cHqU,3082
14
- fakesnow/transforms.py,sha256=GCpczoFdvnffQMvSG59PNiiTVTld9kROnnR5dWNXQvY,53624
15
- fakesnow/variables.py,sha256=WXyPnkeNwD08gy52yF66CVe2twiYC50tztNfgXV4q1k,3032
16
- fakesnow-0.9.21.dist-info/LICENSE,sha256=kW-7NWIyaRMQiDpryfSmF2DObDZHGR1cJZ39s6B1Svg,11344
17
- fakesnow-0.9.21.dist-info/METADATA,sha256=6EzLZbGBTS0VEYCHdG4sNNZT-w5SiespKcitL_XEtM4,18043
18
- fakesnow-0.9.21.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
19
- fakesnow-0.9.21.dist-info/entry_points.txt,sha256=2riAUgu928ZIHawtO8EsfrMEJhi-EH-z_Vq7Q44xKPM,47
20
- fakesnow-0.9.21.dist-info/top_level.txt,sha256=500evXI1IFX9so82cizGIEMHAb_dJNPaZvd2H9dcKTA,24
21
- fakesnow-0.9.21.dist-info/RECORD,,