fakesnow 0.9.17__tar.gz → 0.9.19__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 (34) hide show
  1. {fakesnow-0.9.17 → fakesnow-0.9.19}/PKG-INFO +1 -1
  2. {fakesnow-0.9.17 → fakesnow-0.9.19}/fakesnow/fakes.py +8 -1
  3. {fakesnow-0.9.17 → fakesnow-0.9.19}/fakesnow/transforms.py +14 -1
  4. fakesnow-0.9.19/fakesnow/variables.py +57 -0
  5. {fakesnow-0.9.17 → fakesnow-0.9.19}/fakesnow.egg-info/PKG-INFO +1 -1
  6. {fakesnow-0.9.17 → fakesnow-0.9.19}/fakesnow.egg-info/SOURCES.txt +1 -0
  7. {fakesnow-0.9.17 → fakesnow-0.9.19}/pyproject.toml +1 -1
  8. {fakesnow-0.9.17 → fakesnow-0.9.19}/tests/test_fakes.py +67 -4
  9. {fakesnow-0.9.17 → fakesnow-0.9.19}/tests/test_transforms.py +7 -0
  10. {fakesnow-0.9.17 → fakesnow-0.9.19}/LICENSE +0 -0
  11. {fakesnow-0.9.17 → fakesnow-0.9.19}/README.md +0 -0
  12. {fakesnow-0.9.17 → fakesnow-0.9.19}/fakesnow/__init__.py +0 -0
  13. {fakesnow-0.9.17 → fakesnow-0.9.19}/fakesnow/__main__.py +0 -0
  14. {fakesnow-0.9.17 → fakesnow-0.9.19}/fakesnow/checks.py +0 -0
  15. {fakesnow-0.9.17 → fakesnow-0.9.19}/fakesnow/cli.py +0 -0
  16. {fakesnow-0.9.17 → fakesnow-0.9.19}/fakesnow/expr.py +0 -0
  17. {fakesnow-0.9.17 → fakesnow-0.9.19}/fakesnow/fixtures.py +0 -0
  18. {fakesnow-0.9.17 → fakesnow-0.9.19}/fakesnow/global_database.py +0 -0
  19. {fakesnow-0.9.17 → fakesnow-0.9.19}/fakesnow/info_schema.py +0 -0
  20. {fakesnow-0.9.17 → fakesnow-0.9.19}/fakesnow/macros.py +0 -0
  21. {fakesnow-0.9.17 → fakesnow-0.9.19}/fakesnow/py.typed +0 -0
  22. {fakesnow-0.9.17 → fakesnow-0.9.19}/fakesnow.egg-info/dependency_links.txt +0 -0
  23. {fakesnow-0.9.17 → fakesnow-0.9.19}/fakesnow.egg-info/entry_points.txt +0 -0
  24. {fakesnow-0.9.17 → fakesnow-0.9.19}/fakesnow.egg-info/requires.txt +0 -0
  25. {fakesnow-0.9.17 → fakesnow-0.9.19}/fakesnow.egg-info/top_level.txt +0 -0
  26. {fakesnow-0.9.17 → fakesnow-0.9.19}/setup.cfg +0 -0
  27. {fakesnow-0.9.17 → fakesnow-0.9.19}/tests/test_checks.py +0 -0
  28. {fakesnow-0.9.17 → fakesnow-0.9.19}/tests/test_cli.py +0 -0
  29. {fakesnow-0.9.17 → fakesnow-0.9.19}/tests/test_expr.py +0 -0
  30. {fakesnow-0.9.17 → fakesnow-0.9.19}/tests/test_info_schema.py +0 -0
  31. {fakesnow-0.9.17 → fakesnow-0.9.19}/tests/test_patch.py +0 -0
  32. {fakesnow-0.9.17 → fakesnow-0.9.19}/tests/test_sqlalchemy.py +0 -0
  33. {fakesnow-0.9.17 → fakesnow-0.9.19}/tests/test_users.py +0 -0
  34. {fakesnow-0.9.17 → fakesnow-0.9.19}/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.17
3
+ Version: 0.9.19
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
@@ -33,6 +33,7 @@ import fakesnow.info_schema as info_schema
33
33
  import fakesnow.macros as macros
34
34
  import fakesnow.transforms as transforms
35
35
  from fakesnow.global_database import create_global_database
36
+ from fakesnow.variables import Variables
36
37
 
37
38
  SCHEMA_UNSET = "schema_unset"
38
39
  SQL_SUCCESS = "SELECT 'Statement executed successfully.' as 'status'"
@@ -134,6 +135,7 @@ class FakeSnowflakeCursor:
134
135
  if os.environ.get("FAKESNOW_DEBUG") == "snowflake":
135
136
  print(f"{command};{params=}" if params else f"{command};", file=sys.stderr)
136
137
 
138
+ command = self._inline_variables(command)
137
139
  command, params = self._rewrite_with_params(command, params)
138
140
  if self._conn.nop_regexes and any(re.match(p, command, re.IGNORECASE) for p in self._conn.nop_regexes):
139
141
  transformed = transforms.SUCCESS_NOP
@@ -148,6 +150,7 @@ class FakeSnowflakeCursor:
148
150
  def _transform(self, expression: exp.Expression) -> exp.Expression:
149
151
  return (
150
152
  expression.transform(transforms.upper_case_unquoted_identifiers)
153
+ .transform(transforms.update_variables, variables=self._conn.variables)
151
154
  .transform(transforms.set_schema, current_database=self._conn.database)
152
155
  .transform(transforms.create_database, db_path=self._conn.db_path)
153
156
  .transform(transforms.extract_comment_on_table)
@@ -501,6 +504,9 @@ class FakeSnowflakeCursor:
501
504
 
502
505
  return command, params
503
506
 
507
+ def _inline_variables(self, sql: str) -> str:
508
+ return self._conn.variables.inline_variables(sql)
509
+
504
510
 
505
511
  class FakeSnowflakeConnection:
506
512
  def __init__(
@@ -525,6 +531,7 @@ class FakeSnowflakeConnection:
525
531
  self.db_path = Path(db_path) if db_path else None
526
532
  self.nop_regexes = nop_regexes
527
533
  self._paramstyle = snowflake.connector.paramstyle
534
+ self.variables = Variables()
528
535
 
529
536
  create_global_database(duck_conn)
530
537
 
@@ -611,7 +618,7 @@ class FakeSnowflakeConnection:
611
618
  cursors = [
612
619
  self.cursor(cursor_class).execute(e.sql(dialect="snowflake"))
613
620
  for e in sqlglot.parse(sql_text, read="snowflake")
614
- if e
621
+ if e and not isinstance(e, exp.Semicolon) # ignore comments
615
622
  ]
616
623
  return cursors if return_cursors else []
617
624
 
@@ -8,6 +8,7 @@ import sqlglot
8
8
  from sqlglot import exp
9
9
 
10
10
  from fakesnow.global_database import USERS_TABLE_FQ_NAME
11
+ from fakesnow.variables import Variables
11
12
 
12
13
  MISSING_DATABASE = "missing_database"
13
14
  SUCCESS_NOP = sqlglot.parse_one("SELECT 'Statement executed successfully.'")
@@ -119,9 +120,11 @@ def create_database(expression: exp.Expression, db_path: Path | None = None) ->
119
120
  db_name = ident.this
120
121
  db_file = f"{db_path/db_name}.db" if db_path else ":memory:"
121
122
 
123
+ if_not_exists = "IF NOT EXISTS " if expression.args.get("exists") else ""
124
+
122
125
  return exp.Command(
123
126
  this="ATTACH",
124
- expression=exp.Literal(this=f"DATABASE '{db_file}' AS {db_name}", is_string=True),
127
+ expression=exp.Literal(this=f"{if_not_exists}DATABASE '{db_file}' AS {db_name}", is_string=True),
125
128
  create_db_name=db_name,
126
129
  )
127
130
 
@@ -1405,6 +1408,16 @@ def show_keys(
1405
1408
  return expression
1406
1409
 
1407
1410
 
1411
+ def update_variables(
1412
+ expression: exp.Expression,
1413
+ variables: Variables,
1414
+ ) -> exp.Expression:
1415
+ if Variables.is_variable_modifier(expression):
1416
+ variables.update_variables(expression)
1417
+ return SUCCESS_NOP # Nothing further to do if its a SET/UNSET operation.
1418
+ return expression
1419
+
1420
+
1408
1421
  class SHA256(exp.Func):
1409
1422
  _sql_names: ClassVar = ["SHA256"]
1410
1423
  arg_types: ClassVar = {"this": True}
@@ -0,0 +1,57 @@
1
+ import re
2
+
3
+ import snowflake.connector.errors
4
+ from sqlglot import exp
5
+
6
+
7
+ # Implements snowflake variables: https://docs.snowflake.com/en/sql-reference/session-variables#using-variables-in-sql
8
+ class Variables:
9
+ @classmethod
10
+ def is_variable_modifier(cls, expr: exp.Expression) -> bool:
11
+ return isinstance(expr, exp.Set) or cls._is_unset_expression(expr)
12
+
13
+ @classmethod
14
+ def _is_unset_expression(cls, expr: exp.Expression) -> bool:
15
+ if isinstance(expr, exp.Alias):
16
+ this_expr = expr.this.args.get("this")
17
+ return isinstance(this_expr, exp.Expression) and this_expr.this == "UNSET"
18
+ return False
19
+
20
+ def __init__(self) -> None:
21
+ self._variables = {}
22
+
23
+ def update_variables(self, expr: exp.Expression) -> None:
24
+ if isinstance(expr, exp.Set):
25
+ unset = expr.args.get("unset")
26
+ if not unset: # SET varname = value;
27
+ unset_expressions = expr.args.get("expressions")
28
+ assert unset_expressions, "SET without values in expression(s) is unexpected."
29
+ eq = unset_expressions[0].this
30
+ name = eq.this.sql()
31
+ value = eq.args.get("expression").sql()
32
+ self._set(name, value)
33
+ else:
34
+ # Haven't been able to produce this in tests yet due to UNSET being parsed as an Alias expression.
35
+ raise NotImplementedError("UNSET not supported yet")
36
+ elif self._is_unset_expression(expr): # Unfortunately UNSET varname; is parsed as an Alias expression :(
37
+ alias = expr.args.get("alias")
38
+ assert alias, "UNSET without value in alias attribute is unexpected."
39
+ name = alias.this
40
+ self._unset(name)
41
+
42
+ def _set(self, name: str, value: str) -> None:
43
+ self._variables[name] = value
44
+
45
+ def _unset(self, name: str) -> None:
46
+ self._variables.pop(name)
47
+
48
+ def inline_variables(self, sql: str) -> str:
49
+ for name, value in self._variables.items():
50
+ sql = re.sub(rf"\${name}", value, sql, flags=re.IGNORECASE)
51
+
52
+ remaining_variables = re.search(r"\$\w+", sql)
53
+ if remaining_variables:
54
+ raise snowflake.connector.errors.ProgrammingError(
55
+ msg=f"Session variable '{remaining_variables.group().upper()}' does not exist"
56
+ )
57
+ return sql
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fakesnow
3
- Version: 0.9.17
3
+ Version: 0.9.19
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
@@ -13,6 +13,7 @@ fakesnow/info_schema.py
13
13
  fakesnow/macros.py
14
14
  fakesnow/py.typed
15
15
  fakesnow/transforms.py
16
+ fakesnow/variables.py
16
17
  fakesnow.egg-info/PKG-INFO
17
18
  fakesnow.egg-info/SOURCES.txt
18
19
  fakesnow.egg-info/dependency_links.txt
@@ -1,7 +1,7 @@
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.17"
4
+ version = "0.9.19"
5
5
  readme = "README.md"
6
6
  license = { file = "LICENSE" }
7
7
  classifiers = ["License :: OSI Approved :: MIT License"]
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
  # pyright: reportOptionalMemberAccess=false
5
5
  import datetime
6
6
  import json
7
+ import re
7
8
  import tempfile
8
9
  from decimal import Decimal
9
10
 
@@ -15,6 +16,7 @@ import snowflake.connector.cursor
15
16
  import snowflake.connector.pandas_tools
16
17
  from pandas.testing import assert_frame_equal
17
18
  from snowflake.connector.cursor import ResultMetadata
19
+ from snowflake.connector.errors import ProgrammingError
18
20
 
19
21
  import fakesnow
20
22
  from tests.utils import dindent, indent
@@ -342,6 +344,17 @@ def test_connect_with_non_existent_db_or_schema(_fakesnow_no_auto_create: None):
342
344
  assert conn.schema == "JAFFLES"
343
345
 
344
346
 
347
+ def test_create_database_respects_if_not_exists() -> None:
348
+ with tempfile.TemporaryDirectory(prefix="fakesnow-test") as db_path, fakesnow.patch(db_path=db_path):
349
+ cursor = snowflake.connector.connect().cursor()
350
+ cursor.execute("CREATE DATABASE db2")
351
+
352
+ with pytest.raises(ProgrammingError, match='Database "DB2" is already attached with path'):
353
+ cursor.execute("CREATE DATABASE db2") # Fails as db already exists.
354
+
355
+ cursor.execute("CREATE DATABASE IF NOT EXISTS db2")
356
+
357
+
345
358
  def test_dateadd_date_cast(dcur: snowflake.connector.DictCursor):
346
359
  q = """
347
360
  SELECT
@@ -696,11 +709,14 @@ def test_executemany(cur: snowflake.connector.cursor.SnowflakeCursor):
696
709
 
697
710
 
698
711
  def test_execute_string(conn: snowflake.connector.SnowflakeConnection):
699
- [_, cur2] = conn.execute_string(
700
- """ create table customers (ID int, FIRST_NAME varchar, LAST_NAME varchar);
701
- select count(*) customers """
712
+ *_, cur = conn.execute_string(
713
+ """
714
+ create table customers (ID int, FIRST_NAME varchar, LAST_NAME varchar);
715
+ -- test comments are ignored
716
+ select count(*) customers
717
+ """
702
718
  )
703
- assert cur2.fetchall() == [(1,)]
719
+ assert cur.fetchall() == [(1,)]
704
720
 
705
721
 
706
722
  def test_fetchall(conn: snowflake.connector.SnowflakeConnection):
@@ -1542,6 +1558,53 @@ def test_use_invalid_schema(_fakesnow: None):
1542
1558
  )
1543
1559
 
1544
1560
 
1561
+ # Snowflake SQL variables: https://docs.snowflake.com/en/sql-reference/session-variables#using-variables-in-sql
1562
+ #
1563
+ # Variables are scoped to the session (Eg. The connection, not the cursor)
1564
+ # [x] Simple scalar variables: SET var1 = 1;
1565
+ # [x] Unset variables: UNSET var1;
1566
+ # [x] Simple SQL expression variables: SET INCREMENTAL_DATE = DATEADD( 'DAY', -7, CURRENT_DATE());
1567
+ # [x] Basic use of variables in SQL using $ syntax: SELECT $var1;
1568
+ # [ ] Multiple variables: SET (var1, var2) = (1, 'hello');
1569
+ # [ ] Variables set via 'properties' on the connection https://docs.snowflake.com/en/sql-reference/session-variables#setting-variables-on-connection
1570
+ # [ ] Using variables via the IDENTIFIER function: INSERT INTO IDENTIFIER($my_table_name) (i) VALUES (42);
1571
+ # [ ] Session variable functions: https://docs.snowflake.com/en/sql-reference/session-variables#session-variable-functions
1572
+ def test_variables(conn: snowflake.connector.SnowflakeConnection):
1573
+ with conn.cursor() as cur:
1574
+ cur.execute("SET var1 = 1;")
1575
+ cur.execute("SET var2 = 'hello';")
1576
+ cur.execute("SET var3 = DATEADD( 'DAY', -7, '2024-10-09');")
1577
+
1578
+ cur.execute("select $var1, $var2, $var3;")
1579
+ assert cur.fetchall() == [(1, "hello", datetime.datetime(2024, 10, 2, 0, 0))]
1580
+
1581
+ cur.execute("CREATE TABLE example (id int, name varchar);")
1582
+ cur.execute("INSERT INTO example VALUES (10, 'hello'), (20, 'world');")
1583
+ cur.execute("select id, name from example where name = $var2;")
1584
+ assert cur.fetchall() == [(10, "hello")]
1585
+
1586
+ cur.execute("UNSET var3;")
1587
+ with pytest.raises(
1588
+ snowflake.connector.errors.ProgrammingError, match=re.escape("Session variable '$VAR3' does not exist")
1589
+ ):
1590
+ cur.execute("select $var3;")
1591
+
1592
+ # variables are scoped to the session, so they should be available in a new cursor.
1593
+ with conn.cursor() as cur:
1594
+ cur.execute("select $var1, $var2")
1595
+ assert cur.fetchall() == [(1, "hello")]
1596
+
1597
+ # but not in a new connection.
1598
+ with (
1599
+ snowflake.connector.connect() as conn,
1600
+ conn.cursor() as cur,
1601
+ pytest.raises(
1602
+ snowflake.connector.errors.ProgrammingError, match=re.escape("Session variable '$VAR1' does not exist")
1603
+ ),
1604
+ ):
1605
+ cur.execute("select $var1;")
1606
+
1607
+
1545
1608
  def test_values(conn: snowflake.connector.SnowflakeConnection):
1546
1609
  with conn.cursor(snowflake.connector.cursor.DictCursor) as cur:
1547
1610
  cur.execute("select * from VALUES ('Amsterdam', 1), ('London', 2)")
@@ -144,6 +144,13 @@ def test_create_database() -> None:
144
144
  == "ATTACH DATABASE '.databases/foobar.db' AS foobar"
145
145
  )
146
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
+
147
154
 
148
155
  def test_describe_table() -> None:
149
156
  assert "SELECT" in sqlglot.parse_one("describe table db1.schema1.table1").transform(describe_table).sql()
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