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.
- {fakesnow-0.9.17 → fakesnow-0.9.19}/PKG-INFO +1 -1
- {fakesnow-0.9.17 → fakesnow-0.9.19}/fakesnow/fakes.py +8 -1
- {fakesnow-0.9.17 → fakesnow-0.9.19}/fakesnow/transforms.py +14 -1
- fakesnow-0.9.19/fakesnow/variables.py +57 -0
- {fakesnow-0.9.17 → fakesnow-0.9.19}/fakesnow.egg-info/PKG-INFO +1 -1
- {fakesnow-0.9.17 → fakesnow-0.9.19}/fakesnow.egg-info/SOURCES.txt +1 -0
- {fakesnow-0.9.17 → fakesnow-0.9.19}/pyproject.toml +1 -1
- {fakesnow-0.9.17 → fakesnow-0.9.19}/tests/test_fakes.py +67 -4
- {fakesnow-0.9.17 → fakesnow-0.9.19}/tests/test_transforms.py +7 -0
- {fakesnow-0.9.17 → fakesnow-0.9.19}/LICENSE +0 -0
- {fakesnow-0.9.17 → fakesnow-0.9.19}/README.md +0 -0
- {fakesnow-0.9.17 → fakesnow-0.9.19}/fakesnow/__init__.py +0 -0
- {fakesnow-0.9.17 → fakesnow-0.9.19}/fakesnow/__main__.py +0 -0
- {fakesnow-0.9.17 → fakesnow-0.9.19}/fakesnow/checks.py +0 -0
- {fakesnow-0.9.17 → fakesnow-0.9.19}/fakesnow/cli.py +0 -0
- {fakesnow-0.9.17 → fakesnow-0.9.19}/fakesnow/expr.py +0 -0
- {fakesnow-0.9.17 → fakesnow-0.9.19}/fakesnow/fixtures.py +0 -0
- {fakesnow-0.9.17 → fakesnow-0.9.19}/fakesnow/global_database.py +0 -0
- {fakesnow-0.9.17 → fakesnow-0.9.19}/fakesnow/info_schema.py +0 -0
- {fakesnow-0.9.17 → fakesnow-0.9.19}/fakesnow/macros.py +0 -0
- {fakesnow-0.9.17 → fakesnow-0.9.19}/fakesnow/py.typed +0 -0
- {fakesnow-0.9.17 → fakesnow-0.9.19}/fakesnow.egg-info/dependency_links.txt +0 -0
- {fakesnow-0.9.17 → fakesnow-0.9.19}/fakesnow.egg-info/entry_points.txt +0 -0
- {fakesnow-0.9.17 → fakesnow-0.9.19}/fakesnow.egg-info/requires.txt +0 -0
- {fakesnow-0.9.17 → fakesnow-0.9.19}/fakesnow.egg-info/top_level.txt +0 -0
- {fakesnow-0.9.17 → fakesnow-0.9.19}/setup.cfg +0 -0
- {fakesnow-0.9.17 → fakesnow-0.9.19}/tests/test_checks.py +0 -0
- {fakesnow-0.9.17 → fakesnow-0.9.19}/tests/test_cli.py +0 -0
- {fakesnow-0.9.17 → fakesnow-0.9.19}/tests/test_expr.py +0 -0
- {fakesnow-0.9.17 → fakesnow-0.9.19}/tests/test_info_schema.py +0 -0
- {fakesnow-0.9.17 → fakesnow-0.9.19}/tests/test_patch.py +0 -0
- {fakesnow-0.9.17 → fakesnow-0.9.19}/tests/test_sqlalchemy.py +0 -0
- {fakesnow-0.9.17 → fakesnow-0.9.19}/tests/test_users.py +0 -0
- {fakesnow-0.9.17 → fakesnow-0.9.19}/tests/test_write_pandas.py +0 -0
@@ -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,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.
|
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
|
-
|
700
|
-
"""
|
701
|
-
|
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
|
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
|
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
|
File without changes
|