fakesnow 0.9.16__tar.gz → 0.9.18__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.
Files changed (33) hide show
  1. {fakesnow-0.9.16 → fakesnow-0.9.18}/PKG-INFO +3 -3
  2. {fakesnow-0.9.16 → fakesnow-0.9.18}/fakesnow/fakes.py +4 -2
  3. {fakesnow-0.9.16 → fakesnow-0.9.18}/fakesnow/transforms.py +38 -4
  4. {fakesnow-0.9.16 → fakesnow-0.9.18}/fakesnow.egg-info/PKG-INFO +3 -3
  5. {fakesnow-0.9.16 → fakesnow-0.9.18}/fakesnow.egg-info/requires.txt +2 -2
  6. {fakesnow-0.9.16 → fakesnow-0.9.18}/pyproject.toml +3 -3
  7. {fakesnow-0.9.16 → fakesnow-0.9.18}/tests/test_fakes.py +54 -5
  8. {fakesnow-0.9.16 → fakesnow-0.9.18}/tests/test_transforms.py +48 -0
  9. {fakesnow-0.9.16 → fakesnow-0.9.18}/LICENSE +0 -0
  10. {fakesnow-0.9.16 → fakesnow-0.9.18}/README.md +0 -0
  11. {fakesnow-0.9.16 → fakesnow-0.9.18}/fakesnow/__init__.py +0 -0
  12. {fakesnow-0.9.16 → fakesnow-0.9.18}/fakesnow/__main__.py +0 -0
  13. {fakesnow-0.9.16 → fakesnow-0.9.18}/fakesnow/checks.py +0 -0
  14. {fakesnow-0.9.16 → fakesnow-0.9.18}/fakesnow/cli.py +0 -0
  15. {fakesnow-0.9.16 → fakesnow-0.9.18}/fakesnow/expr.py +0 -0
  16. {fakesnow-0.9.16 → fakesnow-0.9.18}/fakesnow/fixtures.py +0 -0
  17. {fakesnow-0.9.16 → fakesnow-0.9.18}/fakesnow/global_database.py +0 -0
  18. {fakesnow-0.9.16 → fakesnow-0.9.18}/fakesnow/info_schema.py +0 -0
  19. {fakesnow-0.9.16 → fakesnow-0.9.18}/fakesnow/macros.py +0 -0
  20. {fakesnow-0.9.16 → fakesnow-0.9.18}/fakesnow/py.typed +0 -0
  21. {fakesnow-0.9.16 → fakesnow-0.9.18}/fakesnow.egg-info/SOURCES.txt +0 -0
  22. {fakesnow-0.9.16 → fakesnow-0.9.18}/fakesnow.egg-info/dependency_links.txt +0 -0
  23. {fakesnow-0.9.16 → fakesnow-0.9.18}/fakesnow.egg-info/entry_points.txt +0 -0
  24. {fakesnow-0.9.16 → fakesnow-0.9.18}/fakesnow.egg-info/top_level.txt +0 -0
  25. {fakesnow-0.9.16 → fakesnow-0.9.18}/setup.cfg +0 -0
  26. {fakesnow-0.9.16 → fakesnow-0.9.18}/tests/test_checks.py +0 -0
  27. {fakesnow-0.9.16 → fakesnow-0.9.18}/tests/test_cli.py +0 -0
  28. {fakesnow-0.9.16 → fakesnow-0.9.18}/tests/test_expr.py +0 -0
  29. {fakesnow-0.9.16 → fakesnow-0.9.18}/tests/test_info_schema.py +0 -0
  30. {fakesnow-0.9.16 → fakesnow-0.9.18}/tests/test_patch.py +0 -0
  31. {fakesnow-0.9.16 → fakesnow-0.9.18}/tests/test_sqlalchemy.py +0 -0
  32. {fakesnow-0.9.16 → fakesnow-0.9.18}/tests/test_users.py +0 -0
  33. {fakesnow-0.9.16 → fakesnow-0.9.18}/tests/test_write_pandas.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fakesnow
3
- Version: 0.9.16
3
+ Version: 0.9.18
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,10 +210,10 @@ 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~=0.10.3
213
+ Requires-Dist: duckdb~=1.0.0
214
214
  Requires-Dist: pyarrow
215
215
  Requires-Dist: snowflake-connector-python
216
- Requires-Dist: sqlglot~=24.1.0
216
+ Requires-Dist: sqlglot~=25.3.0
217
217
  Provides-Extra: dev
218
218
  Requires-Dist: build~=1.0; extra == "dev"
219
219
  Requires-Dist: pandas-stubs; extra == "dev"
@@ -158,6 +158,7 @@ class FakeSnowflakeCursor:
158
158
  .transform(transforms.tag)
159
159
  .transform(transforms.semi_structured_types)
160
160
  .transform(transforms.try_parse_json)
161
+ .transform(transforms.split)
161
162
  # NOTE: trim_cast_varchar must be before json_extract_cast_as_varchar
162
163
  .transform(transforms.trim_cast_varchar)
163
164
  # indices_to_json_extract must be before regex_substr
@@ -165,6 +166,7 @@ class FakeSnowflakeCursor:
165
166
  .transform(transforms.json_extract_cast_as_varchar)
166
167
  .transform(transforms.json_extract_cased_as_varchar)
167
168
  .transform(transforms.json_extract_precedence)
169
+ .transform(transforms.flatten_value_cast_as_varchar)
168
170
  .transform(transforms.flatten)
169
171
  .transform(transforms.regex_replace)
170
172
  .transform(transforms.regex_substr)
@@ -184,7 +186,7 @@ class FakeSnowflakeCursor:
184
186
  .transform(transforms.random)
185
187
  .transform(transforms.identifier)
186
188
  .transform(transforms.array_agg_within_group)
187
- .transform(transforms.array_agg_to_json)
189
+ .transform(transforms.array_agg)
188
190
  .transform(transforms.dateadd_date_cast)
189
191
  .transform(transforms.dateadd_string_literal_timestamp_cast)
190
192
  .transform(transforms.datediff_string_literal_timestamp_cast)
@@ -609,7 +611,7 @@ class FakeSnowflakeConnection:
609
611
  cursors = [
610
612
  self.cursor(cursor_class).execute(e.sql(dialect="snowflake"))
611
613
  for e in sqlglot.parse(sql_text, read="snowflake")
612
- if e
614
+ if e and not isinstance(e, exp.Semicolon) # ignore comments
613
615
  ]
614
616
  return cursors if return_cursors else []
615
617
 
@@ -41,8 +41,11 @@ def array_size(expression: exp.Expression) -> exp.Expression:
41
41
  return expression
42
42
 
43
43
 
44
- def array_agg_to_json(expression: exp.Expression) -> exp.Expression:
45
- if isinstance(expression, exp.ArrayAgg):
44
+ def array_agg(expression: exp.Expression) -> exp.Expression:
45
+ if isinstance(expression, exp.ArrayAgg) and not isinstance(expression.parent, exp.Window):
46
+ return exp.Anonymous(this="TO_JSON", expressions=[expression])
47
+
48
+ if isinstance(expression, exp.Window) and isinstance(expression.this, exp.ArrayAgg):
46
49
  return exp.Anonymous(this="TO_JSON", expressions=[expression])
47
50
 
48
51
  return expression
@@ -116,9 +119,11 @@ def create_database(expression: exp.Expression, db_path: Path | None = None) ->
116
119
  db_name = ident.this
117
120
  db_file = f"{db_path/db_name}.db" if db_path else ":memory:"
118
121
 
122
+ if_not_exists = "IF NOT EXISTS " if expression.args.get("exists") else ""
123
+
119
124
  return exp.Command(
120
125
  this="ATTACH",
121
- expression=exp.Literal(this=f"DATABASE '{db_file}' AS {db_name}", is_string=True),
126
+ expression=exp.Literal(this=f"{if_not_exists}DATABASE '{db_file}' AS {db_name}", is_string=True),
122
127
  create_db_name=db_name,
123
128
  )
124
129
 
@@ -436,7 +441,7 @@ def flatten(expression: exp.Expression) -> exp.Expression:
436
441
  isinstance(expression, exp.Lateral)
437
442
  and isinstance(expression.this, exp.Explode)
438
443
  and (alias := expression.args.get("alias"))
439
- # always true; when no explicit alias provided this will be _flattened
444
+ # always true; when no explicit alias provided this will be flattened
440
445
  and isinstance(alias, exp.TableAlias)
441
446
  ):
442
447
  explode_expression = expression.this.this.expression
@@ -460,6 +465,25 @@ def flatten(expression: exp.Expression) -> exp.Expression:
460
465
  return expression
461
466
 
462
467
 
468
+ def flatten_value_cast_as_varchar(expression: exp.Expression) -> exp.Expression:
469
+ """Return raw unquoted string when flatten VALUE is cast to varchar.
470
+
471
+ Returns a raw string using the Duckdb ->> operator, aka the json_extract_string function, see
472
+ https://duckdb.org/docs/extensions/json#json-extraction-functions
473
+ """
474
+ if (
475
+ isinstance(expression, exp.Cast)
476
+ and isinstance(expression.this, exp.Column)
477
+ and expression.this.name.upper() == "VALUE"
478
+ and expression.to.this in [exp.DataType.Type.VARCHAR, exp.DataType.Type.TEXT]
479
+ and (select := expression.find_ancestor(exp.Select))
480
+ and select.find(exp.Explode)
481
+ ):
482
+ return exp.JSONExtractScalar(this=expression.this, expression=exp.JSONPath(expressions=[exp.JSONPathRoot()]))
483
+
484
+ return expression
485
+
486
+
463
487
  def float_to_double(expression: exp.Expression) -> exp.Expression:
464
488
  """Convert float to double for 64 bit precision.
465
489
 
@@ -931,6 +955,16 @@ def show_schemas(expression: exp.Expression, current_database: str | None = None
931
955
  return expression
932
956
 
933
957
 
958
+ def split(expression: exp.Expression) -> exp.Expression:
959
+ """
960
+ Convert output of duckdb str_split from varchar[] to JSON array to match Snowflake.
961
+ """
962
+ if isinstance(expression, exp.Split):
963
+ return exp.Anonymous(this="to_json", expressions=[expression])
964
+
965
+ return expression
966
+
967
+
934
968
  def tag(expression: exp.Expression) -> exp.Expression:
935
969
  """Handle tags. Transfer tags into upserts of the tag table.
936
970
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fakesnow
3
- Version: 0.9.16
3
+ Version: 0.9.18
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,10 +210,10 @@ 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~=0.10.3
213
+ Requires-Dist: duckdb~=1.0.0
214
214
  Requires-Dist: pyarrow
215
215
  Requires-Dist: snowflake-connector-python
216
- Requires-Dist: sqlglot~=24.1.0
216
+ Requires-Dist: sqlglot~=25.3.0
217
217
  Provides-Extra: dev
218
218
  Requires-Dist: build~=1.0; extra == "dev"
219
219
  Requires-Dist: pandas-stubs; extra == "dev"
@@ -1,7 +1,7 @@
1
- duckdb~=0.10.3
1
+ duckdb~=1.0.0
2
2
  pyarrow
3
3
  snowflake-connector-python
4
- sqlglot~=24.1.0
4
+ sqlglot~=25.3.0
5
5
 
6
6
  [dev]
7
7
  build~=1.0
@@ -1,17 +1,17 @@
1
1
  [project]
2
2
  name = "fakesnow"
3
3
  description = "Fake Snowflake Connector for Python. Run, mock and test Snowflake DB locally."
4
- version = "0.9.16"
4
+ version = "0.9.18"
5
5
  readme = "README.md"
6
6
  license = { file = "LICENSE" }
7
7
  classifiers = ["License :: OSI Approved :: MIT License"]
8
8
  keywords = ["snowflake", "snowflakedb", "fake", "local", "mock", "testing"]
9
9
  requires-python = ">=3.9"
10
10
  dependencies = [
11
- "duckdb~=0.10.3",
11
+ "duckdb~=1.0.0",
12
12
  "pyarrow",
13
13
  "snowflake-connector-python",
14
- "sqlglot~=24.1.0",
14
+ "sqlglot~=25.3.0",
15
15
  ]
16
16
 
17
17
  [project.urls]
@@ -15,6 +15,7 @@ import snowflake.connector.cursor
15
15
  import snowflake.connector.pandas_tools
16
16
  from pandas.testing import assert_frame_equal
17
17
  from snowflake.connector.cursor import ResultMetadata
18
+ from snowflake.connector.errors import ProgrammingError
18
19
 
19
20
  import fakesnow
20
21
  from tests.utils import dindent, indent
@@ -54,7 +55,7 @@ def test_array_size(cur: snowflake.connector.cursor.SnowflakeCursor):
54
55
  assert cur.fetchall() == [(None,)]
55
56
 
56
57
 
57
- def test_array_agg_to_json(dcur: snowflake.connector.cursor.DictCursor):
58
+ def test_array_agg(dcur: snowflake.connector.cursor.DictCursor):
58
59
  dcur.execute("create table table1 (id number, name varchar)")
59
60
  values = [(1, "foo"), (2, "bar"), (1, "baz"), (2, "qux")]
60
61
 
@@ -63,6 +64,24 @@ def test_array_agg_to_json(dcur: snowflake.connector.cursor.DictCursor):
63
64
  dcur.execute("select array_agg(name) as names from table1")
64
65
  assert dindent(dcur.fetchall()) == [{"NAMES": '[\n "foo",\n "bar",\n "baz",\n "qux"\n]'}]
65
66
 
67
+ # using over
68
+
69
+ dcur.execute(
70
+ """
71
+ SELECT DISTINCT
72
+ ID
73
+ , ANOTHER
74
+ , ARRAY_AGG(DISTINCT COL) OVER(PARTITION BY ID) AS COLS
75
+ FROM (select column1 as ID, column2 as COL, column3 as ANOTHER from
76
+ (VALUES (1, 's1', 'c1'),(1, 's2', 'c1'),(1, 's3', 'c1'),(2, 's1', 'c2'), (2,'s2','c2')))
77
+ ORDER BY ID
78
+ """
79
+ )
80
+ assert dindent(dcur.fetchall()) == [
81
+ {"ID": 1, "ANOTHER": "c1", "COLS": '[\n "s1",\n "s2",\n "s3"\n]'},
82
+ {"ID": 2, "ANOTHER": "c2", "COLS": '[\n "s1",\n "s2"\n]'},
83
+ ]
84
+
66
85
 
67
86
  def test_array_agg_within_group(dcur: snowflake.connector.cursor.DictCursor):
68
87
  dcur.execute("CREATE TABLE table1 (ID INT, amount INT)")
@@ -324,6 +343,17 @@ def test_connect_with_non_existent_db_or_schema(_fakesnow_no_auto_create: None):
324
343
  assert conn.schema == "JAFFLES"
325
344
 
326
345
 
346
+ def test_create_database_respects_if_not_exists() -> None:
347
+ with tempfile.TemporaryDirectory(prefix="fakesnow-test") as db_path, fakesnow.patch(db_path=db_path):
348
+ cursor = snowflake.connector.connect().cursor()
349
+ cursor.execute("CREATE DATABASE db2")
350
+
351
+ with pytest.raises(ProgrammingError, match='Database "DB2" is already attached with path'):
352
+ cursor.execute("CREATE DATABASE db2") # Fails as db already exists.
353
+
354
+ cursor.execute("CREATE DATABASE IF NOT EXISTS db2")
355
+
356
+
327
357
  def test_dateadd_date_cast(dcur: snowflake.connector.DictCursor):
328
358
  q = """
329
359
  SELECT
@@ -678,11 +708,14 @@ def test_executemany(cur: snowflake.connector.cursor.SnowflakeCursor):
678
708
 
679
709
 
680
710
  def test_execute_string(conn: snowflake.connector.SnowflakeConnection):
681
- [_, cur2] = conn.execute_string(
682
- """ create table customers (ID int, FIRST_NAME varchar, LAST_NAME varchar);
683
- select count(*) customers """
711
+ *_, cur = conn.execute_string(
712
+ """
713
+ create table customers (ID int, FIRST_NAME varchar, LAST_NAME varchar);
714
+ -- test comments are ignored
715
+ select count(*) customers
716
+ """
684
717
  )
685
- assert cur2.fetchall() == [(1,)]
718
+ assert cur.fetchall() == [(1,)]
686
719
 
687
720
 
688
721
  def test_fetchall(conn: snowflake.connector.SnowflakeConnection):
@@ -796,6 +829,18 @@ def test_flatten(cur: snowflake.connector.cursor.SnowflakeCursor):
796
829
  assert cur.fetchall() == [(1, '"banana"'), (2, '"coconut"'), (2, '"durian"')]
797
830
 
798
831
 
832
+ def test_flatten_value_cast_as_varchar(cur: snowflake.connector.cursor.SnowflakeCursor):
833
+ cur.execute(
834
+ """
835
+ select id, f.value::varchar as v
836
+ from (select column1 as id, column2 as col from (values (1, 's1,s2,s3'), (2, 's1,s2'))) as t
837
+ , lateral flatten(input => split(t.col, ',')) as f order by id
838
+ """
839
+ )
840
+ # should be raw string not json string with double quotes
841
+ assert cur.fetchall() == [(1, "s1"), (1, "s2"), (1, "s3"), (2, "s1"), (2, "s2")]
842
+
843
+
799
844
  def test_floats_are_64bit(cur: snowflake.connector.cursor.SnowflakeCursor):
800
845
  cur.execute("create or replace table example (f float, f4 float4, f8 float8, d double, r real)")
801
846
  cur.execute("insert into example values (1.23, 1.23, 1.23, 1.23, 1.23)")
@@ -1329,6 +1374,10 @@ def test_show_primary_keys(dcur: snowflake.connector.cursor.SnowflakeCursor):
1329
1374
  assert result3 == []
1330
1375
 
1331
1376
 
1377
+ def test_split(cur: snowflake.connector.cursor.SnowflakeCursor):
1378
+ assert indent(cur.execute("select split('a,b,c', ',')").fetchall()) == [('[\n "a",\n "b",\n "c"\n]',)]
1379
+
1380
+
1332
1381
  def test_sqlglot_regression(cur: snowflake.connector.cursor.SnowflakeCursor):
1333
1382
  assert cur.execute(
1334
1383
  """with SOURCE_TABLE AS (SELECT '2024-01-01' AS start_date)
@@ -8,6 +8,7 @@ from fakesnow.transforms import (
8
8
  SUCCESS_NOP,
9
9
  _get_to_number_args,
10
10
  alias_in_join,
11
+ array_agg,
11
12
  array_agg_within_group,
12
13
  array_size,
13
14
  create_clone,
@@ -21,6 +22,7 @@ from fakesnow.transforms import (
21
22
  extract_comment_on_table,
22
23
  extract_text_length,
23
24
  flatten,
25
+ flatten_value_cast_as_varchar,
24
26
  float_to_double,
25
27
  identifier,
26
28
  indices_to_json_extract,
@@ -40,6 +42,7 @@ from fakesnow.transforms import (
40
42
  sha256,
41
43
  show_objects_tables,
42
44
  show_schemas,
45
+ split,
43
46
  tag,
44
47
  timestamp_ntz,
45
48
  to_date,
@@ -78,6 +81,22 @@ def test_array_size() -> None:
78
81
  )
79
82
 
80
83
 
84
+ def test_array_agg() -> None:
85
+ assert (
86
+ sqlglot.parse_one("SELECT ARRAY_AGG(name) AS names FROM table1").transform(array_agg).sql(dialect="duckdb")
87
+ == "SELECT TO_JSON(ARRAY_AGG(name)) AS names FROM table1"
88
+ )
89
+
90
+ assert (
91
+ sqlglot.parse_one(
92
+ "SELECT DISTINCT ID, ANOTHER, ARRAY_AGG(DISTINCT COL) OVER(PARTITION BY ID) AS COLS FROM TEST"
93
+ )
94
+ .transform(array_agg)
95
+ .sql(dialect="duckdb")
96
+ == "SELECT DISTINCT ID, ANOTHER, TO_JSON(ARRAY_AGG(DISTINCT COL) OVER (PARTITION BY ID)) AS COLS FROM TEST"
97
+ )
98
+
99
+
81
100
  def test_array_agg_within_group() -> None:
82
101
  assert (
83
102
  sqlglot.parse_one(
@@ -125,6 +144,13 @@ def test_create_database() -> None:
125
144
  == "ATTACH DATABASE '.databases/foobar.db' AS foobar"
126
145
  )
127
146
 
147
+ assert (
148
+ sqlglot.parse_one("create database if not exists foobar")
149
+ .transform(create_database, db_path=Path(".databases/"))
150
+ .sql()
151
+ == "ATTACH IF NOT EXISTS DATABASE '.databases/foobar.db' AS foobar"
152
+ )
153
+
128
154
 
129
155
  def test_describe_table() -> None:
130
156
  assert "SELECT" in sqlglot.parse_one("describe table db1.schema1.table1").transform(describe_table).sql()
@@ -400,6 +426,22 @@ def test_flatten() -> None:
400
426
  )
401
427
 
402
428
 
429
+ def test_flatten_value_cast_as_varchar() -> None:
430
+ assert (
431
+ sqlglot.parse_one(
432
+ """
433
+ SELECT ID , F.VALUE::varchar as V
434
+ FROM TEST AS T
435
+ , LATERAL FLATTEN(input => SPLIT(T.COL, ',')) AS F;
436
+ """,
437
+ read="snowflake",
438
+ )
439
+ .transform(flatten_value_cast_as_varchar)
440
+ .sql(dialect="duckdb")
441
+ == """SELECT ID, F.VALUE ->> '$' AS V FROM TEST AS T, LATERAL UNNEST(input => STR_SPLIT(T.COL, ',')) AS F(SEQ, KEY, PATH, INDEX, VALUE, THIS)""" # noqa: E501
442
+ )
443
+
444
+
403
445
  def test_float_to_double() -> None:
404
446
  assert (
405
447
  sqlglot.parse_one("create table example (f float, f4 float4, f8 float8, d double, r real)")
@@ -610,6 +652,12 @@ def test_show_schemas() -> None:
610
652
  )
611
653
 
612
654
 
655
+ def test_split() -> None:
656
+ assert (
657
+ sqlglot.parse_one("SELECT split('a,b,c', ',')").transform(split).sql() == "SELECT TO_JSON(SPLIT('a,b,c', ','))"
658
+ )
659
+
660
+
613
661
  def test_tag() -> None:
614
662
  assert sqlglot.parse_one("ALTER TABLE table1 SET TAG foo='bar'", read="snowflake").transform(tag) == SUCCESS_NOP
615
663
  assert (
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes