fakesnow 0.8.0__tar.gz → 0.8.1__tar.gz
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-0.8.0/fakesnow.egg-info → fakesnow-0.8.1}/PKG-INFO +24 -3
- {fakesnow-0.8.0 → fakesnow-0.8.1}/README.md +22 -1
- fakesnow-0.8.1/fakesnow/__main__.py +4 -0
- fakesnow-0.8.1/fakesnow/cli.py +28 -0
- {fakesnow-0.8.0 → fakesnow-0.8.1}/fakesnow/fakes.py +77 -11
- {fakesnow-0.8.0 → fakesnow-0.8.1}/fakesnow/transforms.py +66 -5
- {fakesnow-0.8.0 → fakesnow-0.8.1/fakesnow.egg-info}/PKG-INFO +24 -3
- {fakesnow-0.8.0 → fakesnow-0.8.1}/fakesnow.egg-info/SOURCES.txt +4 -0
- fakesnow-0.8.1/fakesnow.egg-info/entry_points.txt +2 -0
- {fakesnow-0.8.0 → fakesnow-0.8.1}/fakesnow.egg-info/requires.txt +1 -1
- {fakesnow-0.8.0 → fakesnow-0.8.1}/fakesnow.egg-info/top_level.txt +1 -0
- {fakesnow-0.8.0 → fakesnow-0.8.1}/pyproject.toml +7 -2
- fakesnow-0.8.1/tests/test_cli.py +17 -0
- {fakesnow-0.8.0 → fakesnow-0.8.1}/tests/test_fakes.py +132 -39
- {fakesnow-0.8.0 → fakesnow-0.8.1}/tests/test_patch.py +1 -1
- {fakesnow-0.8.0 → fakesnow-0.8.1}/tests/test_transforms.py +35 -2
- {fakesnow-0.8.0 → fakesnow-0.8.1}/LICENSE +0 -0
- {fakesnow-0.8.0 → fakesnow-0.8.1}/MANIFEST.in +0 -0
- {fakesnow-0.8.0 → fakesnow-0.8.1}/fakesnow/__init__.py +0 -0
- {fakesnow-0.8.0 → fakesnow-0.8.1}/fakesnow/checks.py +0 -0
- {fakesnow-0.8.0 → fakesnow-0.8.1}/fakesnow/expr.py +0 -0
- {fakesnow-0.8.0 → fakesnow-0.8.1}/fakesnow/fixtures.py +0 -0
- {fakesnow-0.8.0 → fakesnow-0.8.1}/fakesnow/info_schema.py +2 -2
- {fakesnow-0.8.0 → fakesnow-0.8.1}/fakesnow/py.typed +0 -0
- {fakesnow-0.8.0 → fakesnow-0.8.1}/fakesnow.egg-info/dependency_links.txt +0 -0
- {fakesnow-0.8.0 → fakesnow-0.8.1}/setup.cfg +0 -0
- {fakesnow-0.8.0 → fakesnow-0.8.1}/tests/test_checks.py +0 -0
- {fakesnow-0.8.0 → fakesnow-0.8.1}/tests/test_expr.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: fakesnow
|
3
|
-
Version: 0.8.
|
3
|
+
Version: 0.8.1
|
4
4
|
Summary: Fake Snowflake Connector for Python. Run Snowflake DB locally.
|
5
5
|
License: MIT License
|
6
6
|
|
@@ -32,7 +32,7 @@ License-File: LICENSE
|
|
32
32
|
Requires-Dist: duckdb~=0.9.2
|
33
33
|
Requires-Dist: pyarrow
|
34
34
|
Requires-Dist: snowflake-connector-python
|
35
|
-
Requires-Dist: sqlglot~=
|
35
|
+
Requires-Dist: sqlglot~=20.5.0
|
36
36
|
Provides-Extra: dev
|
37
37
|
Requires-Dist: black~=23.9; extra == "dev"
|
38
38
|
Requires-Dist: build~=1.0; extra == "dev"
|
@@ -63,6 +63,24 @@ pip install fakesnow
|
|
63
63
|
|
64
64
|
## Usage
|
65
65
|
|
66
|
+
Run script.py with fakesnow:
|
67
|
+
|
68
|
+
```shell
|
69
|
+
fakesnow script.py
|
70
|
+
```
|
71
|
+
|
72
|
+
Or pytest
|
73
|
+
|
74
|
+
```shell
|
75
|
+
fakesnow -m pytest
|
76
|
+
```
|
77
|
+
|
78
|
+
`fakesnow` executes `fakesnow.patch` before running the script or module.
|
79
|
+
|
80
|
+
### fakesnow.patch
|
81
|
+
|
82
|
+
eg:
|
83
|
+
|
66
84
|
```python
|
67
85
|
import fakesnow
|
68
86
|
import snowflake.connector
|
@@ -91,6 +109,8 @@ with fakesnow.patch("mymodule.write_pandas"):
|
|
91
109
|
...
|
92
110
|
```
|
93
111
|
|
112
|
+
### pytest fixtures
|
113
|
+
|
94
114
|
pytest [fixtures](fakesnow/fixtures.py) are provided for testing. Example _conftest.py_:
|
95
115
|
|
96
116
|
```python
|
@@ -146,7 +166,8 @@ For more detail see [tests/test_fakes.py](tests/test_fakes.py)
|
|
146
166
|
|
147
167
|
## Caveats
|
148
168
|
|
149
|
-
-
|
169
|
+
- The order of rows is non deterministic and may not match Snowflake unless ORDER BY is fully specified.
|
170
|
+
- VARCHAR field sizes are not enforced. Unlike Snowflake which errors with "User character length limit (xxx) exceeded by string" when an inserted string the exceeds the column limit.
|
150
171
|
|
151
172
|
## Contributing
|
152
173
|
|
@@ -14,6 +14,24 @@ pip install fakesnow
|
|
14
14
|
|
15
15
|
## Usage
|
16
16
|
|
17
|
+
Run script.py with fakesnow:
|
18
|
+
|
19
|
+
```shell
|
20
|
+
fakesnow script.py
|
21
|
+
```
|
22
|
+
|
23
|
+
Or pytest
|
24
|
+
|
25
|
+
```shell
|
26
|
+
fakesnow -m pytest
|
27
|
+
```
|
28
|
+
|
29
|
+
`fakesnow` executes `fakesnow.patch` before running the script or module.
|
30
|
+
|
31
|
+
### fakesnow.patch
|
32
|
+
|
33
|
+
eg:
|
34
|
+
|
17
35
|
```python
|
18
36
|
import fakesnow
|
19
37
|
import snowflake.connector
|
@@ -42,6 +60,8 @@ with fakesnow.patch("mymodule.write_pandas"):
|
|
42
60
|
...
|
43
61
|
```
|
44
62
|
|
63
|
+
### pytest fixtures
|
64
|
+
|
45
65
|
pytest [fixtures](fakesnow/fixtures.py) are provided for testing. Example _conftest.py_:
|
46
66
|
|
47
67
|
```python
|
@@ -97,7 +117,8 @@ For more detail see [tests/test_fakes.py](tests/test_fakes.py)
|
|
97
117
|
|
98
118
|
## Caveats
|
99
119
|
|
100
|
-
-
|
120
|
+
- The order of rows is non deterministic and may not match Snowflake unless ORDER BY is fully specified.
|
121
|
+
- VARCHAR field sizes are not enforced. Unlike Snowflake which errors with "User character length limit (xxx) exceeded by string" when an inserted string the exceeds the column limit.
|
101
122
|
|
102
123
|
## Contributing
|
103
124
|
|
@@ -0,0 +1,28 @@
|
|
1
|
+
import runpy
|
2
|
+
import sys
|
3
|
+
from collections.abc import Sequence
|
4
|
+
|
5
|
+
import fakesnow
|
6
|
+
|
7
|
+
USAGE = "Usage: fakesnow <path> | -m <module> [<arg>]..."
|
8
|
+
|
9
|
+
|
10
|
+
def main(args: Sequence[str] = sys.argv) -> int:
|
11
|
+
if len(args) < 2 or (len(args) == 2 and args[1] == "-m"):
|
12
|
+
print(USAGE, file=sys.stderr)
|
13
|
+
return 42
|
14
|
+
|
15
|
+
with fakesnow.patch():
|
16
|
+
if args[1] == "-m":
|
17
|
+
module = args[2]
|
18
|
+
sys.argv = args[2:]
|
19
|
+
|
20
|
+
# add current directory to path to mimic python -m
|
21
|
+
sys.path.insert(0, "")
|
22
|
+
runpy.run_module(module, run_name="__main__", alter_sys=True)
|
23
|
+
else:
|
24
|
+
path = args[1]
|
25
|
+
sys.argv = args[1:]
|
26
|
+
runpy.run_path(path, run_name="__main__")
|
27
|
+
|
28
|
+
return 0
|
@@ -1,9 +1,12 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
+
import os
|
3
4
|
import re
|
5
|
+
import sys
|
4
6
|
from collections.abc import Iterable, Iterator, Sequence
|
7
|
+
from string import Template
|
5
8
|
from types import TracebackType
|
6
|
-
from typing import TYPE_CHECKING, Any, Literal, Optional,
|
9
|
+
from typing import TYPE_CHECKING, Any, Literal, Optional, cast
|
7
10
|
|
8
11
|
import duckdb
|
9
12
|
|
@@ -26,6 +29,12 @@ import fakesnow.info_schema as info_schema
|
|
26
29
|
import fakesnow.transforms as transforms
|
27
30
|
|
28
31
|
SCHEMA_UNSET = "schema_unset"
|
32
|
+
SUCCESS_SQL = "SELECT 'Statement executed successfully.' as status"
|
33
|
+
DATABASE_CREATED_SQL = Template("SELECT 'Database ${name} successfully created.' as status")
|
34
|
+
TABLE_CREATED_SQL = Template("SELECT 'Table ${name} successfully created.' as status")
|
35
|
+
DROPPED_SQL = Template("SELECT '${name} successfully dropped.' as status")
|
36
|
+
SCHEMA_CREATED_SQL = Template("SELECT 'Schema ${name} successfully created.' as status")
|
37
|
+
INSERTED_SQL = Template("SELECT ${count} as 'number of rows inserted'")
|
29
38
|
|
30
39
|
|
31
40
|
class FakeSnowflakeCursor:
|
@@ -49,6 +58,7 @@ class FakeSnowflakeCursor:
|
|
49
58
|
self._last_sql = None
|
50
59
|
self._last_params = None
|
51
60
|
self._sqlstate = None
|
61
|
+
self._arraysize = 1
|
52
62
|
self._converter = snowflake.connector.converter.SnowflakeConverter()
|
53
63
|
|
54
64
|
def __enter__(self) -> Self:
|
@@ -62,6 +72,19 @@ class FakeSnowflakeCursor:
|
|
62
72
|
) -> bool:
|
63
73
|
return False
|
64
74
|
|
75
|
+
@property
|
76
|
+
def arraysize(self) -> int:
|
77
|
+
return self._arraysize
|
78
|
+
|
79
|
+
@arraysize.setter
|
80
|
+
def arraysize(self, value: int) -> None:
|
81
|
+
self._arraysize = value
|
82
|
+
|
83
|
+
def close(self) -> bool:
|
84
|
+
self._last_sql = None
|
85
|
+
self._last_params = None
|
86
|
+
return True
|
87
|
+
|
65
88
|
def describe(self, command: str, *args: Any, **kwargs: Any) -> list[ResultMetadata]:
|
66
89
|
"""Return the schema of the result without executing the query.
|
67
90
|
|
@@ -79,8 +102,9 @@ class FakeSnowflakeCursor:
|
|
79
102
|
def description(self) -> list[ResultMetadata]:
|
80
103
|
# use a cursor to avoid destroying an unfetched result on the main connection
|
81
104
|
with self._duck_conn.cursor() as cur:
|
82
|
-
|
83
|
-
assert self._conn.
|
105
|
+
# TODO: allow sql alchemy connection with no database or schema
|
106
|
+
assert self._conn.database, ".description not implemented when database is None"
|
107
|
+
assert self._conn.schema, ".description not implemented when schema is None"
|
84
108
|
|
85
109
|
# match database and schema used on the main connection
|
86
110
|
cur.execute(f"SET SCHEMA = '{self._conn.database}.{self._conn.schema}'")
|
@@ -145,6 +169,8 @@ class FakeSnowflakeCursor:
|
|
145
169
|
.transform(transforms.parse_json)
|
146
170
|
# indices_to_json_extract must be before regex_substr
|
147
171
|
.transform(transforms.indices_to_json_extract)
|
172
|
+
.transform(transforms.json_extract_as_varchar)
|
173
|
+
.transform(transforms.flatten)
|
148
174
|
.transform(transforms.regex_replace)
|
149
175
|
.transform(transforms.regex_substr)
|
150
176
|
.transform(transforms.values_columns)
|
@@ -162,7 +188,8 @@ class FakeSnowflakeCursor:
|
|
162
188
|
try:
|
163
189
|
self._last_sql = sql
|
164
190
|
self._last_params = params
|
165
|
-
|
191
|
+
if os.environ.get("FAKESNOW_DEBUG"):
|
192
|
+
print(f"{sql};", file=sys.stderr)
|
166
193
|
self._duck_conn.execute(sql, params)
|
167
194
|
except duckdb.BinderException as e:
|
168
195
|
msg = e.args[0]
|
@@ -171,6 +198,15 @@ class FakeSnowflakeCursor:
|
|
171
198
|
# minimal processing to make it look like a snowflake exception, message content may differ
|
172
199
|
msg = cast(str, e.args[0]).split("\n")[0]
|
173
200
|
raise snowflake.connector.errors.ProgrammingError(msg=msg, errno=2003, sqlstate="42S02") from None
|
201
|
+
except duckdb.TransactionException as e:
|
202
|
+
if "cannot rollback - no transaction is active" in str(
|
203
|
+
e
|
204
|
+
) or "cannot commit - no transaction is active" in str(e):
|
205
|
+
# snowflake doesn't error on rollback or commit outside a tx
|
206
|
+
self._duck_conn.execute(SUCCESS_SQL)
|
207
|
+
self._last_sql = SUCCESS_SQL
|
208
|
+
else:
|
209
|
+
raise e
|
174
210
|
|
175
211
|
if cmd == "USE DATABASE" and (ident := expression.find(exp.Identifier)) and isinstance(ident.this, str):
|
176
212
|
self._conn.database = ident.this.upper()
|
@@ -183,6 +219,33 @@ class FakeSnowflakeCursor:
|
|
183
219
|
if create_db_name := transformed.args.get("create_db_name"):
|
184
220
|
# we created a new database, so create the info schema extensions
|
185
221
|
self._duck_conn.execute(info_schema.creation_sql(create_db_name))
|
222
|
+
created_sql = DATABASE_CREATED_SQL.substitute(name=create_db_name)
|
223
|
+
self._duck_conn.execute(created_sql)
|
224
|
+
self._last_sql = created_sql
|
225
|
+
|
226
|
+
if cmd == "CREATE SCHEMA" and (ident := expression.find(exp.Identifier)) and isinstance(ident.this, str):
|
227
|
+
name = ident.this if ident.quoted else ident.this.upper()
|
228
|
+
created_sql = SCHEMA_CREATED_SQL.substitute(name=name)
|
229
|
+
self._duck_conn.execute(created_sql)
|
230
|
+
self._last_sql = created_sql
|
231
|
+
|
232
|
+
if cmd == "CREATE TABLE" and (ident := expression.find(exp.Identifier)) and isinstance(ident.this, str):
|
233
|
+
name = ident.this if ident.quoted else ident.this.upper()
|
234
|
+
created_sql = TABLE_CREATED_SQL.substitute(name=name)
|
235
|
+
self._duck_conn.execute(created_sql)
|
236
|
+
self._last_sql = created_sql
|
237
|
+
|
238
|
+
if cmd.startswith("DROP") and (ident := expression.find(exp.Identifier)) and isinstance(ident.this, str):
|
239
|
+
name = ident.this if ident.quoted else ident.this.upper()
|
240
|
+
dropped_sql = DROPPED_SQL.substitute(name=name)
|
241
|
+
self._duck_conn.execute(dropped_sql)
|
242
|
+
self._last_sql = dropped_sql
|
243
|
+
|
244
|
+
if cmd == "INSERT":
|
245
|
+
(count,) = self._duck_conn.fetchall()[0]
|
246
|
+
inserted_sql = INSERTED_SQL.substitute(count=count)
|
247
|
+
self._duck_conn.execute(inserted_sql)
|
248
|
+
self._last_sql = inserted_sql
|
186
249
|
|
187
250
|
if table_comment := cast(tuple[exp.Table, str], transformed.args.get("table_comment")):
|
188
251
|
# record table comment
|
@@ -232,19 +295,22 @@ class FakeSnowflakeCursor:
|
|
232
295
|
return self._duck_conn.fetch_df()
|
233
296
|
|
234
297
|
def fetchone(self) -> dict | tuple | None:
|
298
|
+
result = self.fetchmany(1)
|
299
|
+
return result[0] if result else None
|
300
|
+
|
301
|
+
def fetchmany(self, size: int | None = None) -> list[tuple] | list[dict]:
|
302
|
+
# https://peps.python.org/pep-0249/#fetchmany
|
303
|
+
size = size or self._arraysize
|
235
304
|
if not self._use_dict_result:
|
236
|
-
return cast(
|
305
|
+
return cast(list[tuple], self._duck_conn.fetchmany(size))
|
237
306
|
|
238
307
|
if not self._arrow_table:
|
239
308
|
self._arrow_table = self._duck_conn.fetch_arrow_table()
|
240
|
-
self.
|
309
|
+
self._arrow_table_fetch_index = -size
|
241
310
|
|
242
|
-
self.
|
311
|
+
self._arrow_table_fetch_index += size
|
243
312
|
|
244
|
-
|
245
|
-
return self._arrow_table.take([self._arrow_table_fetch_one_index]).to_pylist()[0]
|
246
|
-
except pyarrow.lib.ArrowIndexError:
|
247
|
-
return None
|
313
|
+
return self._arrow_table.slice(offset=self._arrow_table_fetch_index, length=size).to_pylist()
|
248
314
|
|
249
315
|
def get_result_batches(self) -> list[ResultBatch] | None:
|
250
316
|
# rows_per_batch is approximate
|
@@ -155,6 +155,49 @@ def extract_text_length(expression: exp.Expression) -> exp.Expression:
|
|
155
155
|
return expression
|
156
156
|
|
157
157
|
|
158
|
+
def flatten(expression: exp.Expression) -> exp.Expression:
|
159
|
+
"""Flatten an array.
|
160
|
+
|
161
|
+
See https://docs.snowflake.com/en/sql-reference/functions/flatten
|
162
|
+
|
163
|
+
TODO: return index.
|
164
|
+
TODO: support objects.
|
165
|
+
"""
|
166
|
+
if (
|
167
|
+
isinstance(expression, exp.Lateral)
|
168
|
+
and isinstance(expression.this, exp.Explode)
|
169
|
+
and (alias := expression.args.get("alias"))
|
170
|
+
# always true; when no explicit alias provided this will be _flattened
|
171
|
+
and isinstance(alias, exp.TableAlias)
|
172
|
+
):
|
173
|
+
explode_expression = expression.this.this.expression
|
174
|
+
|
175
|
+
return exp.Lateral(
|
176
|
+
this=exp.Unnest(
|
177
|
+
expressions=[
|
178
|
+
exp.Anonymous(
|
179
|
+
# duckdb unnests in reserve, so we reverse the list to match
|
180
|
+
# the order of the original array (and snowflake)
|
181
|
+
this="list_reverse",
|
182
|
+
expressions=[
|
183
|
+
exp.Cast(
|
184
|
+
this=explode_expression,
|
185
|
+
to=exp.DataType(
|
186
|
+
this=exp.DataType.Type.ARRAY,
|
187
|
+
expressions=[exp.DataType(this=exp.DataType.Type.JSON, nested=False, prefix=False)],
|
188
|
+
nested=True,
|
189
|
+
),
|
190
|
+
)
|
191
|
+
],
|
192
|
+
)
|
193
|
+
]
|
194
|
+
),
|
195
|
+
alias=exp.TableAlias(this=alias.this, columns=[exp.Identifier(this="VALUE", quoted=False)]),
|
196
|
+
)
|
197
|
+
|
198
|
+
return expression
|
199
|
+
|
200
|
+
|
158
201
|
def float_to_double(expression: exp.Expression) -> exp.Expression:
|
159
202
|
"""Convert float to double for 64 bit precision.
|
160
203
|
|
@@ -176,10 +219,10 @@ def indices_to_json_extract(expression: exp.Expression) -> exp.Expression:
|
|
176
219
|
and object indices, see
|
177
220
|
https://docs.snowflake.com/en/sql-reference/data-types-semistructured#accessing-elements-of-an-object-by-key
|
178
221
|
|
179
|
-
Duckdb uses the -> operator,
|
222
|
+
Duckdb uses the -> operator, aka the json_extract function, see
|
180
223
|
https://duckdb.org/docs/extensions/json#json-extraction-functions
|
181
224
|
|
182
|
-
This works for Snowflake arrays too because we convert them to JSON
|
225
|
+
This works for Snowflake arrays too because we convert them to JSON in duckdb.
|
183
226
|
"""
|
184
227
|
if (
|
185
228
|
isinstance(expression, exp.Bracket)
|
@@ -259,6 +302,20 @@ def integer_precision(expression: exp.Expression) -> exp.Expression:
|
|
259
302
|
return expression
|
260
303
|
|
261
304
|
|
305
|
+
def json_extract_as_varchar(expression: exp.Expression) -> exp.Expression:
|
306
|
+
"""Return raw unquoted string when casting json extraction to varchar.
|
307
|
+
|
308
|
+
This must run after indices_to_json_extract.
|
309
|
+
|
310
|
+
Duckdb uses the ->> operator, aka the json_extract_string function, see
|
311
|
+
https://duckdb.org/docs/extensions/json#json-extraction-functions
|
312
|
+
"""
|
313
|
+
if isinstance(expression, exp.Cast) and (extract := expression.this) and isinstance(extract, exp.JSONExtract):
|
314
|
+
return exp.JSONExtractScalar(this=extract.this, expression=extract.expression)
|
315
|
+
|
316
|
+
return expression
|
317
|
+
|
318
|
+
|
262
319
|
def object_construct(expression: exp.Expression) -> exp.Expression:
|
263
320
|
"""Convert object_construct to return a json string
|
264
321
|
|
@@ -355,9 +412,13 @@ def regex_substr(expression: exp.Expression) -> exp.Expression:
|
|
355
412
|
|
356
413
|
# which occurrence of the pattern to match
|
357
414
|
try:
|
358
|
-
occurrence = expression.args["occurrence"]
|
415
|
+
occurrence = int(expression.args["occurrence"].this)
|
359
416
|
except KeyError:
|
360
|
-
occurrence =
|
417
|
+
occurrence = 1
|
418
|
+
|
419
|
+
# the duckdb dialect increments bracket (ie: index) expressions by 1 because duckdb is 1-indexed,
|
420
|
+
# so we need to compensate by subtracting 1
|
421
|
+
occurrence = exp.Literal(this=str(occurrence - 1), is_string=False)
|
361
422
|
|
362
423
|
try:
|
363
424
|
regex_parameters_value = str(expression.args["parameters"].this)
|
@@ -428,7 +489,7 @@ def set_schema(expression: exp.Expression, current_database: str | None) -> exp.
|
|
428
489
|
name = f"{expression.this.name}.main"
|
429
490
|
else:
|
430
491
|
# SCHEMA
|
431
|
-
if db := expression.this.args.get("db"):
|
492
|
+
if db := expression.this.args.get("db"): # noqa: SIM108
|
432
493
|
db_name = db.name
|
433
494
|
else:
|
434
495
|
# isn't qualified with a database
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: fakesnow
|
3
|
-
Version: 0.8.
|
3
|
+
Version: 0.8.1
|
4
4
|
Summary: Fake Snowflake Connector for Python. Run Snowflake DB locally.
|
5
5
|
License: MIT License
|
6
6
|
|
@@ -32,7 +32,7 @@ License-File: LICENSE
|
|
32
32
|
Requires-Dist: duckdb~=0.9.2
|
33
33
|
Requires-Dist: pyarrow
|
34
34
|
Requires-Dist: snowflake-connector-python
|
35
|
-
Requires-Dist: sqlglot~=
|
35
|
+
Requires-Dist: sqlglot~=20.5.0
|
36
36
|
Provides-Extra: dev
|
37
37
|
Requires-Dist: black~=23.9; extra == "dev"
|
38
38
|
Requires-Dist: build~=1.0; extra == "dev"
|
@@ -63,6 +63,24 @@ pip install fakesnow
|
|
63
63
|
|
64
64
|
## Usage
|
65
65
|
|
66
|
+
Run script.py with fakesnow:
|
67
|
+
|
68
|
+
```shell
|
69
|
+
fakesnow script.py
|
70
|
+
```
|
71
|
+
|
72
|
+
Or pytest
|
73
|
+
|
74
|
+
```shell
|
75
|
+
fakesnow -m pytest
|
76
|
+
```
|
77
|
+
|
78
|
+
`fakesnow` executes `fakesnow.patch` before running the script or module.
|
79
|
+
|
80
|
+
### fakesnow.patch
|
81
|
+
|
82
|
+
eg:
|
83
|
+
|
66
84
|
```python
|
67
85
|
import fakesnow
|
68
86
|
import snowflake.connector
|
@@ -91,6 +109,8 @@ with fakesnow.patch("mymodule.write_pandas"):
|
|
91
109
|
...
|
92
110
|
```
|
93
111
|
|
112
|
+
### pytest fixtures
|
113
|
+
|
94
114
|
pytest [fixtures](fakesnow/fixtures.py) are provided for testing. Example _conftest.py_:
|
95
115
|
|
96
116
|
```python
|
@@ -146,7 +166,8 @@ For more detail see [tests/test_fakes.py](tests/test_fakes.py)
|
|
146
166
|
|
147
167
|
## Caveats
|
148
168
|
|
149
|
-
-
|
169
|
+
- The order of rows is non deterministic and may not match Snowflake unless ORDER BY is fully specified.
|
170
|
+
- VARCHAR field sizes are not enforced. Unlike Snowflake which errors with "User character length limit (xxx) exceeded by string" when an inserted string the exceeds the column limit.
|
150
171
|
|
151
172
|
## Contributing
|
152
173
|
|
@@ -3,7 +3,9 @@ MANIFEST.in
|
|
3
3
|
README.md
|
4
4
|
pyproject.toml
|
5
5
|
fakesnow/__init__.py
|
6
|
+
fakesnow/__main__.py
|
6
7
|
fakesnow/checks.py
|
8
|
+
fakesnow/cli.py
|
7
9
|
fakesnow/expr.py
|
8
10
|
fakesnow/fakes.py
|
9
11
|
fakesnow/fixtures.py
|
@@ -13,9 +15,11 @@ fakesnow/transforms.py
|
|
13
15
|
fakesnow.egg-info/PKG-INFO
|
14
16
|
fakesnow.egg-info/SOURCES.txt
|
15
17
|
fakesnow.egg-info/dependency_links.txt
|
18
|
+
fakesnow.egg-info/entry_points.txt
|
16
19
|
fakesnow.egg-info/requires.txt
|
17
20
|
fakesnow.egg-info/top_level.txt
|
18
21
|
tests/test_checks.py
|
22
|
+
tests/test_cli.py
|
19
23
|
tests/test_expr.py
|
20
24
|
tests/test_fakes.py
|
21
25
|
tests/test_patch.py
|
@@ -1,7 +1,7 @@
|
|
1
1
|
[project]
|
2
2
|
name = "fakesnow"
|
3
3
|
description = "Fake Snowflake Connector for Python. Run Snowflake DB locally."
|
4
|
-
version = "0.8.
|
4
|
+
version = "0.8.1"
|
5
5
|
readme = "README.md"
|
6
6
|
license = { file = "LICENSE" }
|
7
7
|
classifiers = ["License :: OSI Approved :: MIT License"]
|
@@ -11,12 +11,15 @@ dependencies = [
|
|
11
11
|
"duckdb~=0.9.2",
|
12
12
|
"pyarrow",
|
13
13
|
"snowflake-connector-python",
|
14
|
-
"sqlglot~=
|
14
|
+
"sqlglot~=20.5.0",
|
15
15
|
]
|
16
16
|
|
17
17
|
[project.urls]
|
18
18
|
homepage = "https://github.com/tekumara/fakesnow"
|
19
19
|
|
20
|
+
[project.scripts]
|
21
|
+
fakesnow = "fakesnow.cli:main"
|
22
|
+
|
20
23
|
[project.optional-dependencies]
|
21
24
|
dev = [
|
22
25
|
"black~=23.9",
|
@@ -81,6 +84,8 @@ select = [
|
|
81
84
|
"PERF",
|
82
85
|
# ruff-specific
|
83
86
|
"RUF",
|
87
|
+
# flake8-simplify
|
88
|
+
"SIM"
|
84
89
|
]
|
85
90
|
ignore = [
|
86
91
|
# allow untyped self and cls args, and no return type from dunder methods
|
@@ -0,0 +1,17 @@
|
|
1
|
+
from pytest import CaptureFixture
|
2
|
+
|
3
|
+
import fakesnow.cli
|
4
|
+
|
5
|
+
|
6
|
+
def test_run_module(capsys: CaptureFixture) -> None:
|
7
|
+
fakesnow.cli.main(["pytest", "-m", "tests.hello", "frobnitz"])
|
8
|
+
|
9
|
+
captured = capsys.readouterr()
|
10
|
+
assert captured.out == "('Hello fake frobnitz!',)\n"
|
11
|
+
|
12
|
+
|
13
|
+
def test_run_path(capsys: CaptureFixture) -> None:
|
14
|
+
fakesnow.cli.main(["pytest", "tests/hello.py", "frobnitz"])
|
15
|
+
|
16
|
+
captured = capsys.readouterr()
|
17
|
+
assert captured.out == "('Hello fake frobnitz!',)\n"
|
@@ -50,6 +50,10 @@ def test_binding_qmark(conn: snowflake.connector.SnowflakeConnection):
|
|
50
50
|
assert cur.fetchall() == [(1, "Jenny", True)]
|
51
51
|
|
52
52
|
|
53
|
+
def test_close(cur: snowflake.connector.cursor.SnowflakeCursor):
|
54
|
+
assert cur.close() is True
|
55
|
+
|
56
|
+
|
53
57
|
def test_connect_auto_create(_fakesnow: None):
|
54
58
|
with snowflake.connector.connect(database="db1", schema="schema1"):
|
55
59
|
# creates db1 and schema1
|
@@ -278,6 +282,45 @@ def test_describe_info_schema_columns(cur: snowflake.connector.cursor.SnowflakeC
|
|
278
282
|
assert cur.description == expected_metadata
|
279
283
|
|
280
284
|
|
285
|
+
## descriptions are needed for ipython-sql/jupysql which describes every statement
|
286
|
+
|
287
|
+
|
288
|
+
def test_description_create_database(dcur: snowflake.connector.cursor.DictCursor):
|
289
|
+
dcur.execute("create database example")
|
290
|
+
assert dcur.fetchall() == [{"status": "Database EXAMPLE successfully created."}]
|
291
|
+
assert dcur.description == [ResultMetadata(name='status', type_code=2, display_size=None, internal_size=16777216, precision=None, scale=None, is_nullable=True)] # fmt: skip
|
292
|
+
# TODO: support drop database
|
293
|
+
# dcur.execute("drop database example")
|
294
|
+
# assert dcur.fetchall() == [{"status": "EXAMPLE successfully dropped."}]
|
295
|
+
# assert dcur.description == [ResultMetadata(name='status', type_code=2, display_size=None, internal_size=16777216, precision=None, scale=None, is_nullable=True)] # fmt: skip
|
296
|
+
|
297
|
+
|
298
|
+
def test_description_create_drop_schema(dcur: snowflake.connector.cursor.DictCursor):
|
299
|
+
dcur.execute("create schema example")
|
300
|
+
assert dcur.fetchall() == [{"status": "Schema EXAMPLE successfully created."}]
|
301
|
+
assert dcur.description == [ResultMetadata(name='status', type_code=2, display_size=None, internal_size=16777216, precision=None, scale=None, is_nullable=True)] # fmt: skip
|
302
|
+
dcur.execute("drop schema example")
|
303
|
+
assert dcur.fetchall() == [{"status": "EXAMPLE successfully dropped."}]
|
304
|
+
assert dcur.description == [ResultMetadata(name='status', type_code=2, display_size=None, internal_size=16777216, precision=None, scale=None, is_nullable=True)] # fmt: skip
|
305
|
+
|
306
|
+
|
307
|
+
def test_description_create_drop_table(dcur: snowflake.connector.cursor.DictCursor):
|
308
|
+
dcur.execute("create table example (X int)")
|
309
|
+
assert dcur.fetchall() == [{"status": "Table EXAMPLE successfully created."}]
|
310
|
+
assert dcur.description == [ResultMetadata(name='status', type_code=2, display_size=None, internal_size=16777216, precision=None, scale=None, is_nullable=True)] # fmt: skip
|
311
|
+
dcur.execute("drop table example")
|
312
|
+
assert dcur.fetchall() == [{"status": "EXAMPLE successfully dropped."}]
|
313
|
+
assert dcur.description == [ResultMetadata(name='status', type_code=2, display_size=None, internal_size=16777216, precision=None, scale=None, is_nullable=True)] # fmt: skip
|
314
|
+
|
315
|
+
|
316
|
+
def test_description_insert(dcur: snowflake.connector.cursor.DictCursor):
|
317
|
+
dcur.execute("create table example (X int)")
|
318
|
+
dcur.execute("insert into example values (1), (2)")
|
319
|
+
assert dcur.fetchall() == [{"number of rows inserted": 2}]
|
320
|
+
# TODO: Snowflake is actually precision=19, is_nullable=False
|
321
|
+
assert dcur.description == [ResultMetadata(name='number of rows inserted', type_code=0, display_size=None, internal_size=None, precision=38, scale=0, is_nullable=True)] # fmt: skip
|
322
|
+
|
323
|
+
|
281
324
|
def test_executemany(cur: snowflake.connector.cursor.SnowflakeCursor):
|
282
325
|
cur.execute("create table customers (ID int, FIRST_NAME varchar, LAST_NAME varchar)")
|
283
326
|
|
@@ -296,49 +339,65 @@ def test_execute_string(conn: snowflake.connector.SnowflakeConnection):
|
|
296
339
|
assert [(1,)] == cur2.fetchall()
|
297
340
|
|
298
341
|
|
299
|
-
def test_fetchall(
|
300
|
-
|
301
|
-
cur.execute("insert into customers values (1, 'Jenny', 'P')")
|
302
|
-
cur.execute("insert into customers values (2, 'Jasper', 'M')")
|
303
|
-
cur.execute("select id, first_name, last_name from customers")
|
304
|
-
|
305
|
-
assert cur.fetchall() == [(1, "Jenny", "P"), (2, "Jasper", "M")]
|
306
|
-
|
307
|
-
|
308
|
-
def test_fetchall_dict_cursor(conn: snowflake.connector.SnowflakeConnection):
|
309
|
-
with conn.cursor(snowflake.connector.cursor.DictCursor) as cur:
|
342
|
+
def test_fetchall(conn: snowflake.connector.SnowflakeConnection):
|
343
|
+
with conn.cursor() as cur:
|
310
344
|
cur.execute("create table customers (ID int, FIRST_NAME varchar, LAST_NAME varchar)")
|
311
345
|
cur.execute("insert into customers values (1, 'Jenny', 'P')")
|
312
346
|
cur.execute("insert into customers values (2, 'Jasper', 'M')")
|
313
347
|
cur.execute("select id, first_name, last_name from customers")
|
314
348
|
|
349
|
+
assert cur.fetchall() == [(1, "Jenny", "P"), (2, "Jasper", "M")]
|
350
|
+
|
351
|
+
with conn.cursor(snowflake.connector.cursor.DictCursor) as cur:
|
352
|
+
cur.execute("select id, first_name, last_name from customers")
|
353
|
+
|
315
354
|
assert cur.fetchall() == [
|
316
355
|
{"ID": 1, "FIRST_NAME": "Jenny", "LAST_NAME": "P"},
|
317
356
|
{"ID": 2, "FIRST_NAME": "Jasper", "LAST_NAME": "M"},
|
318
357
|
]
|
319
358
|
|
320
359
|
|
321
|
-
def test_fetchone(
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
assert cur.fetchone() == (1, "Jenny", "P")
|
328
|
-
assert cur.fetchone() == (2, "Jasper", "M")
|
329
|
-
assert not cur.fetchone()
|
360
|
+
def test_fetchone(conn: snowflake.connector.SnowflakeConnection):
|
361
|
+
with conn.cursor() as cur:
|
362
|
+
cur.execute("create table customers (ID int, FIRST_NAME varchar, LAST_NAME varchar)")
|
363
|
+
cur.execute("insert into customers values (1, 'Jenny', 'P')")
|
364
|
+
cur.execute("insert into customers values (2, 'Jasper', 'M')")
|
365
|
+
cur.execute("select id, first_name, last_name from customers")
|
330
366
|
|
367
|
+
assert cur.fetchone() == (1, "Jenny", "P")
|
368
|
+
assert cur.fetchone() == (2, "Jasper", "M")
|
369
|
+
assert cur.fetchone() is None
|
331
370
|
|
332
|
-
def test_fetchone_dict_cursor(conn: snowflake.connector.SnowflakeConnection):
|
333
371
|
with conn.cursor(snowflake.connector.cursor.DictCursor) as cur:
|
372
|
+
cur.execute("select id, first_name, last_name from customers")
|
373
|
+
|
374
|
+
assert cur.fetchone() == {"ID": 1, "FIRST_NAME": "Jenny", "LAST_NAME": "P"}
|
375
|
+
assert cur.fetchone() == {"ID": 2, "FIRST_NAME": "Jasper", "LAST_NAME": "M"}
|
376
|
+
assert cur.fetchone() is None
|
377
|
+
|
378
|
+
|
379
|
+
def test_fetchmany(conn: snowflake.connector.SnowflakeConnection):
|
380
|
+
with conn.cursor() as cur:
|
334
381
|
cur.execute("create table customers (ID int, FIRST_NAME varchar, LAST_NAME varchar)")
|
335
382
|
cur.execute("insert into customers values (1, 'Jenny', 'P')")
|
336
383
|
cur.execute("insert into customers values (2, 'Jasper', 'M')")
|
384
|
+
cur.execute("insert into customers values (3, 'Jeremy', 'K')")
|
337
385
|
cur.execute("select id, first_name, last_name from customers")
|
338
386
|
|
339
|
-
assert cur.
|
340
|
-
assert cur.
|
341
|
-
assert
|
387
|
+
assert cur.fetchmany(2) == [(1, "Jenny", "P"), (2, "Jasper", "M")]
|
388
|
+
assert cur.fetchmany(2) == [(3, "Jeremy", "K")]
|
389
|
+
assert cur.fetchmany(2) == []
|
390
|
+
|
391
|
+
with conn.cursor(snowflake.connector.cursor.DictCursor) as cur:
|
392
|
+
cur.execute("select id, first_name, last_name from customers")
|
393
|
+
assert cur.fetchmany(2) == [
|
394
|
+
{"ID": 1, "FIRST_NAME": "Jenny", "LAST_NAME": "P"},
|
395
|
+
{"ID": 2, "FIRST_NAME": "Jasper", "LAST_NAME": "M"},
|
396
|
+
]
|
397
|
+
assert cur.fetchmany(2) == [
|
398
|
+
{"ID": 3, "FIRST_NAME": "Jeremy", "LAST_NAME": "K"},
|
399
|
+
]
|
400
|
+
assert cur.fetchmany(2) == []
|
342
401
|
|
343
402
|
|
344
403
|
def test_fetch_pandas_all(cur: snowflake.connector.cursor.SnowflakeCursor):
|
@@ -357,6 +416,23 @@ def test_fetch_pandas_all(cur: snowflake.connector.cursor.SnowflakeCursor):
|
|
357
416
|
assert_frame_equal(cur.fetch_pandas_all(), expected_df)
|
358
417
|
|
359
418
|
|
419
|
+
def test_flatten(cur: snowflake.connector.cursor.SnowflakeCursor):
|
420
|
+
cur.execute(
|
421
|
+
"""
|
422
|
+
select t.id, flat.value:fruit from
|
423
|
+
(
|
424
|
+
select 1, parse_json('[{"fruit":"banana"}]')
|
425
|
+
union
|
426
|
+
select 2, parse_json('[{"fruit":"coconut"}, {"fruit":"durian"}]')
|
427
|
+
) as t(id, fruits), lateral flatten(input => t.fruits) AS flat
|
428
|
+
order by id
|
429
|
+
"""
|
430
|
+
# duckdb lateral join order is non-deterministic so order by id
|
431
|
+
# within an id the order of fruits should match the json array
|
432
|
+
)
|
433
|
+
assert cur.fetchall() == [(1, '"banana"'), (2, '"coconut"'), (2, '"durian"')]
|
434
|
+
|
435
|
+
|
360
436
|
def test_floats_are_64bit(cur: snowflake.connector.cursor.SnowflakeCursor):
|
361
437
|
cur.execute("create or replace table example (f float, f4 float4, f8 float8, d double, r real)")
|
362
438
|
cur.execute("insert into example values (1.23, 1.23, 1.23, 1.23, 1.23)")
|
@@ -365,6 +441,12 @@ def test_floats_are_64bit(cur: snowflake.connector.cursor.SnowflakeCursor):
|
|
365
441
|
assert cur.fetchall() == [(1.23, 1.23, 1.23, 1.23, 1.23)]
|
366
442
|
|
367
443
|
|
444
|
+
def test_get_path_as_varchar(cur: snowflake.connector.cursor.SnowflakeCursor):
|
445
|
+
cur.execute("""select parse_json('{"fruit":"banana"}'):fruit::varchar""")
|
446
|
+
# converting json to varchar returns unquoted string
|
447
|
+
assert cur.fetchall() == [("banana",)]
|
448
|
+
|
449
|
+
|
368
450
|
def test_get_result_batches(cur: snowflake.connector.cursor.SnowflakeCursor):
|
369
451
|
cur.execute("create table customers (ID int, FIRST_NAME varchar, LAST_NAME varchar)")
|
370
452
|
cur.execute("insert into customers values (1, 'Jenny', 'P')")
|
@@ -657,21 +739,6 @@ def test_to_decimal(cur: snowflake.connector.cursor.SnowflakeCursor):
|
|
657
739
|
]
|
658
740
|
|
659
741
|
|
660
|
-
def test_write_pandas_timestamp_ntz(conn: snowflake.connector.SnowflakeConnection):
|
661
|
-
# compensate for https://github.com/duckdb/duckdb/issues/7980
|
662
|
-
with conn.cursor() as cur:
|
663
|
-
cur.execute("create table example (UPDATE_AT_NTZ timestamp_ntz(9))")
|
664
|
-
# cur.execute("create table example (UPDATE_AT_NTZ timestamp)")
|
665
|
-
|
666
|
-
now_utc = datetime.datetime.now(pytz.utc)
|
667
|
-
df = pd.DataFrame([(now_utc,)], columns=["UPDATE_AT_NTZ"])
|
668
|
-
snowflake.connector.pandas_tools.write_pandas(conn, df, "EXAMPLE")
|
669
|
-
|
670
|
-
cur.execute("select * from example")
|
671
|
-
|
672
|
-
assert cur.fetchall() == [(now_utc.replace(tzinfo=None),)]
|
673
|
-
|
674
|
-
|
675
742
|
def test_transactions(conn: snowflake.connector.SnowflakeConnection):
|
676
743
|
conn.execute_string(
|
677
744
|
"""CREATE TABLE table1 (i int);
|
@@ -688,6 +755,17 @@ def test_transactions(conn: snowflake.connector.SnowflakeConnection):
|
|
688
755
|
cur.execute("SELECT * FROM table1")
|
689
756
|
assert cur.fetchall() == [(2,)]
|
690
757
|
|
758
|
+
# check rollback and commit without transaction is a success (to mimic snowflake)
|
759
|
+
# also check description can be retrieved, needed for ipython-sql/jupysql which runs description implicitly
|
760
|
+
with conn.cursor() as cur:
|
761
|
+
cur.execute("COMMIT")
|
762
|
+
assert cur.description == [ResultMetadata(name='status', type_code=2, display_size=None, internal_size=16777216, precision=None, scale=None, is_nullable=True)] # fmt: skip
|
763
|
+
assert cur.fetchall() == [("Statement executed successfully.",)]
|
764
|
+
|
765
|
+
cur.execute("ROLLBACK")
|
766
|
+
assert cur.description == [ResultMetadata(name='status', type_code=2, display_size=None, internal_size=16777216, precision=None, scale=None, is_nullable=True)] # fmt: skip
|
767
|
+
assert cur.fetchall() == [("Statement executed successfully.",)]
|
768
|
+
|
691
769
|
|
692
770
|
def test_unquoted_identifiers_are_upper_cased(conn: snowflake.connector.SnowflakeConnection):
|
693
771
|
with conn.cursor(snowflake.connector.cursor.DictCursor) as cur:
|
@@ -768,6 +846,21 @@ def test_write_pandas(conn: snowflake.connector.SnowflakeConnection):
|
|
768
846
|
assert cur.fetchall() == [(1, "Jenny", "P"), (2, "Jasper", "M")]
|
769
847
|
|
770
848
|
|
849
|
+
def test_write_pandas_timestamp_ntz(conn: snowflake.connector.SnowflakeConnection):
|
850
|
+
# compensate for https://github.com/duckdb/duckdb/issues/7980
|
851
|
+
with conn.cursor() as cur:
|
852
|
+
cur.execute("create table example (UPDATE_AT_NTZ timestamp_ntz(9))")
|
853
|
+
# cur.execute("create table example (UPDATE_AT_NTZ timestamp)")
|
854
|
+
|
855
|
+
now_utc = datetime.datetime.now(pytz.utc)
|
856
|
+
df = pd.DataFrame([(now_utc,)], columns=["UPDATE_AT_NTZ"])
|
857
|
+
snowflake.connector.pandas_tools.write_pandas(conn, df, "EXAMPLE")
|
858
|
+
|
859
|
+
cur.execute("select * from example")
|
860
|
+
|
861
|
+
assert cur.fetchall() == [(now_utc.replace(tzinfo=None),)]
|
862
|
+
|
863
|
+
|
771
864
|
def test_write_pandas_partial_columns(conn: snowflake.connector.SnowflakeConnection):
|
772
865
|
with conn.cursor() as cur:
|
773
866
|
cur.execute("create table customers (ID int, FIRST_NAME varchar, LAST_NAME varchar)")
|
@@ -39,7 +39,7 @@ def test_patch_other_unloaded_module() -> None:
|
|
39
39
|
def test_cannot_patch_twice(_fakesnow_no_auto_create: None) -> None:
|
40
40
|
# _fakesnow is the first patch
|
41
41
|
|
42
|
-
with pytest.raises(AssertionError) as excinfo:
|
42
|
+
with pytest.raises(AssertionError) as excinfo: # noqa: SIM117
|
43
43
|
# second patch will fail
|
44
44
|
with fakesnow.patch():
|
45
45
|
pass
|
@@ -7,11 +7,13 @@ from fakesnow.transforms import (
|
|
7
7
|
drop_schema_cascade,
|
8
8
|
extract_comment,
|
9
9
|
extract_text_length,
|
10
|
+
flatten,
|
10
11
|
float_to_double,
|
11
12
|
indices_to_json_extract,
|
12
13
|
information_schema_columns_snowflake,
|
13
14
|
information_schema_tables_ext,
|
14
15
|
integer_precision,
|
16
|
+
json_extract_as_varchar,
|
15
17
|
object_construct,
|
16
18
|
parse_json,
|
17
19
|
regex_replace,
|
@@ -67,6 +69,25 @@ def test_extract_text_length() -> None:
|
|
67
69
|
assert e.args["text_lengths"] == [("t1", 16777216), ("t2", 10), ("t3", 20)]
|
68
70
|
|
69
71
|
|
72
|
+
def test_flatten() -> None:
|
73
|
+
assert (
|
74
|
+
sqlglot.parse_one(
|
75
|
+
"""
|
76
|
+
select t.id, flat.value:fruit from
|
77
|
+
(
|
78
|
+
select 1, parse_json('[{"fruit":"banana"}]')
|
79
|
+
union
|
80
|
+
select 2, parse_json('[{"fruit":"coconut"}, {"fruit":"durian"}]')
|
81
|
+
) as t(id, fruits), lateral flatten(input => t.fruits) AS flat
|
82
|
+
""",
|
83
|
+
read="snowflake",
|
84
|
+
).transform(flatten)
|
85
|
+
# needed to transform flat.value:fruit to flat.value -> '$.fruit'
|
86
|
+
.transform(indices_to_json_extract).sql(dialect="duckdb")
|
87
|
+
== """SELECT t.id, flat.value -> '$.fruit' FROM (SELECT 1, JSON('[{"fruit":"banana"}]') UNION SELECT 2, JSON('[{"fruit":"coconut"}, {"fruit":"durian"}]')) AS t(id, fruits), LATERAL UNNEST(LIST_REVERSE(CAST(t.fruits AS JSON[]))) AS flat(VALUE)""" # noqa: E501
|
88
|
+
)
|
89
|
+
|
90
|
+
|
70
91
|
def test_float_to_double() -> None:
|
71
92
|
assert (
|
72
93
|
sqlglot.parse_one("create table example (f float, f4 float4, f8 float8, d double, r real)")
|
@@ -121,6 +142,18 @@ def test_information_schema_tables_ext() -> None:
|
|
121
142
|
)
|
122
143
|
|
123
144
|
|
145
|
+
def test_json_extract_as_varchar() -> None:
|
146
|
+
assert (
|
147
|
+
sqlglot.parse_one(
|
148
|
+
"""select parse_json('{"fruit":"banana"}'):fruit::varchar""",
|
149
|
+
read="snowflake",
|
150
|
+
)
|
151
|
+
# needed first
|
152
|
+
.transform(indices_to_json_extract).transform(json_extract_as_varchar).sql(dialect="duckdb")
|
153
|
+
== """SELECT JSON('{"fruit":"banana"}') ->> '$.fruit'"""
|
154
|
+
)
|
155
|
+
|
156
|
+
|
124
157
|
def test_object_construct() -> None:
|
125
158
|
assert (
|
126
159
|
sqlglot.parse_one("SELECT OBJECT_CONSTRUCT('a',1,'b','BBBB', 'c',null)", read="snowflake")
|
@@ -150,7 +183,7 @@ def test_regex_substr() -> None:
|
|
150
183
|
assert (
|
151
184
|
sqlglot.parse_one("SELECT regexp_substr(string1, 'the\\\\W+\\\\w+')", read="snowflake")
|
152
185
|
.transform(regex_substr)
|
153
|
-
.sql()
|
186
|
+
.sql(dialect="duckdb")
|
154
187
|
== "SELECT REGEXP_EXTRACT_ALL(string1[1 : ], 'the\\W+\\w+', 0, '')[1]"
|
155
188
|
)
|
156
189
|
|
@@ -188,7 +221,7 @@ def test_timestamp_ntz_ns() -> None:
|
|
188
221
|
def test_to_date() -> None:
|
189
222
|
assert (
|
190
223
|
sqlglot.parse_one("SELECT to_date(to_timestamp(0))").transform(to_date).sql()
|
191
|
-
== "SELECT CAST(DATE_TRUNC('
|
224
|
+
== "SELECT CAST(DATE_TRUNC('DAY', TO_TIMESTAMP(0)) AS DATE)"
|
192
225
|
)
|
193
226
|
|
194
227
|
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
@@ -1,7 +1,7 @@
|
|
1
|
-
from string import Template
|
2
|
-
|
3
1
|
"""Info schema extension tables/views used for storing snowflake metadata not captured by duckdb."""
|
4
2
|
|
3
|
+
from string import Template
|
4
|
+
|
5
5
|
# use ext prefix in columns to disambiguate when joining with information_schema.tables
|
6
6
|
SQL_CREATE_INFORMATION_SCHEMA_TABLES_EXT = Template(
|
7
7
|
"""
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|