fakesnow 0.9.18__tar.gz → 0.9.20__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.18 → fakesnow-0.9.20}/PKG-INFO +1 -1
- {fakesnow-0.9.18 → fakesnow-0.9.20}/fakesnow/__init__.py +2 -0
- {fakesnow-0.9.18 → fakesnow-0.9.20}/fakesnow/fakes.py +7 -3
- {fakesnow-0.9.18 → fakesnow-0.9.20}/fakesnow/transforms.py +16 -0
- fakesnow-0.9.20/fakesnow/variables.py +56 -0
- {fakesnow-0.9.18 → fakesnow-0.9.20}/fakesnow.egg-info/PKG-INFO +1 -1
- {fakesnow-0.9.18 → fakesnow-0.9.20}/fakesnow.egg-info/SOURCES.txt +2 -0
- {fakesnow-0.9.18 → fakesnow-0.9.20}/pyproject.toml +1 -1
- fakesnow-0.9.20/tests/test_connect.py +199 -0
- {fakesnow-0.9.18 → fakesnow-0.9.20}/tests/test_fakes.py +54 -172
- {fakesnow-0.9.18 → fakesnow-0.9.20}/tests/test_info_schema.py +18 -0
- {fakesnow-0.9.18 → fakesnow-0.9.20}/LICENSE +0 -0
- {fakesnow-0.9.18 → fakesnow-0.9.20}/README.md +0 -0
- {fakesnow-0.9.18 → fakesnow-0.9.20}/fakesnow/__main__.py +0 -0
- {fakesnow-0.9.18 → fakesnow-0.9.20}/fakesnow/checks.py +0 -0
- {fakesnow-0.9.18 → fakesnow-0.9.20}/fakesnow/cli.py +0 -0
- {fakesnow-0.9.18 → fakesnow-0.9.20}/fakesnow/expr.py +0 -0
- {fakesnow-0.9.18 → fakesnow-0.9.20}/fakesnow/fixtures.py +0 -0
- {fakesnow-0.9.18 → fakesnow-0.9.20}/fakesnow/global_database.py +0 -0
- {fakesnow-0.9.18 → fakesnow-0.9.20}/fakesnow/info_schema.py +0 -0
- {fakesnow-0.9.18 → fakesnow-0.9.20}/fakesnow/macros.py +0 -0
- {fakesnow-0.9.18 → fakesnow-0.9.20}/fakesnow/py.typed +0 -0
- {fakesnow-0.9.18 → fakesnow-0.9.20}/fakesnow.egg-info/dependency_links.txt +0 -0
- {fakesnow-0.9.18 → fakesnow-0.9.20}/fakesnow.egg-info/entry_points.txt +0 -0
- {fakesnow-0.9.18 → fakesnow-0.9.20}/fakesnow.egg-info/requires.txt +0 -0
- {fakesnow-0.9.18 → fakesnow-0.9.20}/fakesnow.egg-info/top_level.txt +0 -0
- {fakesnow-0.9.18 → fakesnow-0.9.20}/setup.cfg +0 -0
- {fakesnow-0.9.18 → fakesnow-0.9.20}/tests/test_checks.py +0 -0
- {fakesnow-0.9.18 → fakesnow-0.9.20}/tests/test_cli.py +0 -0
- {fakesnow-0.9.18 → fakesnow-0.9.20}/tests/test_expr.py +0 -0
- {fakesnow-0.9.18 → fakesnow-0.9.20}/tests/test_patch.py +0 -0
- {fakesnow-0.9.18 → fakesnow-0.9.20}/tests/test_sqlalchemy.py +0 -0
- {fakesnow-0.9.18 → fakesnow-0.9.20}/tests/test_transforms.py +0 -0
- {fakesnow-0.9.18 → fakesnow-0.9.20}/tests/test_users.py +0 -0
- {fakesnow-0.9.18 → fakesnow-0.9.20}/tests/test_write_pandas.py +0 -0
@@ -13,6 +13,7 @@ import snowflake.connector
|
|
13
13
|
import snowflake.connector.pandas_tools
|
14
14
|
|
15
15
|
import fakesnow.fakes as fakes
|
16
|
+
from fakesnow.global_database import create_global_database
|
16
17
|
|
17
18
|
|
18
19
|
@contextmanager
|
@@ -52,6 +53,7 @@ def patch(
|
|
52
53
|
assert not isinstance(snowflake.connector.connect, mock.MagicMock), "Snowflake connector is already patched"
|
53
54
|
|
54
55
|
duck_conn = duckdb.connect(database=":memory:")
|
56
|
+
create_global_database(duck_conn)
|
55
57
|
|
56
58
|
fake_fns = {
|
57
59
|
# every time we connect, create a new cursor (ie: connection) so we can isolate each connection's
|
@@ -32,7 +32,7 @@ import fakesnow.expr as expr
|
|
32
32
|
import fakesnow.info_schema as info_schema
|
33
33
|
import fakesnow.macros as macros
|
34
34
|
import fakesnow.transforms as transforms
|
35
|
-
from fakesnow.
|
35
|
+
from fakesnow.variables import Variables
|
36
36
|
|
37
37
|
SCHEMA_UNSET = "schema_unset"
|
38
38
|
SQL_SUCCESS = "SELECT 'Statement executed successfully.' as 'status'"
|
@@ -134,6 +134,7 @@ class FakeSnowflakeCursor:
|
|
134
134
|
if os.environ.get("FAKESNOW_DEBUG") == "snowflake":
|
135
135
|
print(f"{command};{params=}" if params else f"{command};", file=sys.stderr)
|
136
136
|
|
137
|
+
command = self._inline_variables(command)
|
137
138
|
command, params = self._rewrite_with_params(command, params)
|
138
139
|
if self._conn.nop_regexes and any(re.match(p, command, re.IGNORECASE) for p in self._conn.nop_regexes):
|
139
140
|
transformed = transforms.SUCCESS_NOP
|
@@ -148,6 +149,7 @@ class FakeSnowflakeCursor:
|
|
148
149
|
def _transform(self, expression: exp.Expression) -> exp.Expression:
|
149
150
|
return (
|
150
151
|
expression.transform(transforms.upper_case_unquoted_identifiers)
|
152
|
+
.transform(transforms.update_variables, variables=self._conn.variables)
|
151
153
|
.transform(transforms.set_schema, current_database=self._conn.database)
|
152
154
|
.transform(transforms.create_database, db_path=self._conn.db_path)
|
153
155
|
.transform(transforms.extract_comment_on_table)
|
@@ -501,6 +503,9 @@ class FakeSnowflakeCursor:
|
|
501
503
|
|
502
504
|
return command, params
|
503
505
|
|
506
|
+
def _inline_variables(self, sql: str) -> str:
|
507
|
+
return self._conn.variables.inline_variables(sql)
|
508
|
+
|
504
509
|
|
505
510
|
class FakeSnowflakeConnection:
|
506
511
|
def __init__(
|
@@ -525,8 +530,7 @@ class FakeSnowflakeConnection:
|
|
525
530
|
self.db_path = Path(db_path) if db_path else None
|
526
531
|
self.nop_regexes = nop_regexes
|
527
532
|
self._paramstyle = snowflake.connector.paramstyle
|
528
|
-
|
529
|
-
create_global_database(duck_conn)
|
533
|
+
self.variables = Variables()
|
530
534
|
|
531
535
|
# create database if needed
|
532
536
|
if (
|
@@ -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.'")
|
@@ -1401,12 +1402,27 @@ def show_keys(
|
|
1401
1402
|
|
1402
1403
|
if schema:
|
1403
1404
|
statement += f"AND schema_name = '{schema}' "
|
1405
|
+
elif scope_kind == "TABLE":
|
1406
|
+
if not table:
|
1407
|
+
raise ValueError(f"SHOW PRIMARY KEYS with {scope_kind} scope requires a table")
|
1408
|
+
|
1409
|
+
statement += f"AND table_name = '{table.name}' "
|
1404
1410
|
else:
|
1405
1411
|
raise NotImplementedError(f"SHOW PRIMARY KEYS with {scope_kind} not yet supported")
|
1406
1412
|
return sqlglot.parse_one(statement)
|
1407
1413
|
return expression
|
1408
1414
|
|
1409
1415
|
|
1416
|
+
def update_variables(
|
1417
|
+
expression: exp.Expression,
|
1418
|
+
variables: Variables,
|
1419
|
+
) -> exp.Expression:
|
1420
|
+
if Variables.is_variable_modifier(expression):
|
1421
|
+
variables.update_variables(expression)
|
1422
|
+
return SUCCESS_NOP # Nothing further to do if its a SET/UNSET operation.
|
1423
|
+
return expression
|
1424
|
+
|
1425
|
+
|
1410
1426
|
class SHA256(exp.Func):
|
1411
1427
|
_sql_names: ClassVar = ["SHA256"]
|
1412
1428
|
arg_types: ClassVar = {"this": True}
|
@@ -0,0 +1,56 @@
|
|
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
|
+
if remaining_variables := re.search(r"(?<!\$)\$\w+", sql):
|
53
|
+
raise snowflake.connector.errors.ProgrammingError(
|
54
|
+
msg=f"Session variable '{remaining_variables.group().upper()}' does not exist"
|
55
|
+
)
|
56
|
+
return sql
|
@@ -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
|
@@ -21,6 +22,7 @@ fakesnow.egg-info/requires.txt
|
|
21
22
|
fakesnow.egg-info/top_level.txt
|
22
23
|
tests/test_checks.py
|
23
24
|
tests/test_cli.py
|
25
|
+
tests/test_connect.py
|
24
26
|
tests/test_expr.py
|
25
27
|
tests/test_fakes.py
|
26
28
|
tests/test_info_schema.py
|
@@ -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.20"
|
5
5
|
readme = "README.md"
|
6
6
|
license = { file = "LICENSE" }
|
7
7
|
classifiers = ["License :: OSI Approved :: MIT License"]
|
@@ -0,0 +1,199 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import concurrent.futures
|
4
|
+
|
5
|
+
# ruff: noqa: E501
|
6
|
+
# pyright: reportOptionalMemberAccess=false
|
7
|
+
import tempfile
|
8
|
+
|
9
|
+
import pytest
|
10
|
+
import snowflake.connector
|
11
|
+
import snowflake.connector.cursor
|
12
|
+
import snowflake.connector.pandas_tools
|
13
|
+
|
14
|
+
import fakesnow
|
15
|
+
|
16
|
+
|
17
|
+
def test_connect_auto_create(_fakesnow: None):
|
18
|
+
with snowflake.connector.connect(database="db1", schema="schema1"):
|
19
|
+
# creates db1 and schema1
|
20
|
+
pass
|
21
|
+
|
22
|
+
with snowflake.connector.connect(database="db1", schema="schema1"):
|
23
|
+
# connects again and reuses db1 and schema1
|
24
|
+
pass
|
25
|
+
|
26
|
+
|
27
|
+
def test_connect_different_sessions_use_database(_fakesnow_no_auto_create: None):
|
28
|
+
# connect without default database and schema
|
29
|
+
with snowflake.connector.connect() as conn1, conn1.cursor() as cur:
|
30
|
+
# use the table's fully qualified name
|
31
|
+
cur.execute("create database marts")
|
32
|
+
cur.execute("create schema marts.jaffles")
|
33
|
+
cur.execute("create table marts.jaffles.customers (ID int, FIRST_NAME varchar, LAST_NAME varchar)")
|
34
|
+
cur.execute("insert into marts.jaffles.customers values (1, 'Jenny', 'P')")
|
35
|
+
|
36
|
+
# use database and schema
|
37
|
+
cur.execute("use database marts")
|
38
|
+
cur.execute("use schema jaffles")
|
39
|
+
cur.execute("insert into customers values (2, 'Jasper', 'M')")
|
40
|
+
|
41
|
+
# in a separate connection, connect using the database and schema from above
|
42
|
+
with snowflake.connector.connect(database="marts", schema="jaffles") as conn2, conn2.cursor() as cur:
|
43
|
+
cur.execute("select id, first_name, last_name from customers")
|
44
|
+
assert cur.fetchall() == [(1, "Jenny", "P"), (2, "Jasper", "M")]
|
45
|
+
|
46
|
+
|
47
|
+
def test_connect_concurrently(_fakesnow: None) -> None:
|
48
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor:
|
49
|
+
future_a = executor.submit(snowflake.connector.connect)
|
50
|
+
future_b = executor.submit(snowflake.connector.connect)
|
51
|
+
|
52
|
+
futures = [future_a, future_b]
|
53
|
+
|
54
|
+
for future in concurrent.futures.as_completed(futures):
|
55
|
+
# exceptions if any will be raised here. we want to avoid
|
56
|
+
# duckdb.duckdb.TransactionException: TransactionContext Error: Catalog write-write conflict
|
57
|
+
_ = future.result()
|
58
|
+
|
59
|
+
|
60
|
+
def test_connect_db_path_can_create_database() -> None:
|
61
|
+
with tempfile.TemporaryDirectory(prefix="fakesnow-test") as db_path, fakesnow.patch(db_path=db_path):
|
62
|
+
cursor = snowflake.connector.connect().cursor()
|
63
|
+
cursor.execute("CREATE DATABASE db2")
|
64
|
+
|
65
|
+
|
66
|
+
def test_connect_db_path_reuse():
|
67
|
+
with tempfile.TemporaryDirectory(prefix="fakesnow-test") as db_path:
|
68
|
+
with (
|
69
|
+
fakesnow.patch(db_path=db_path),
|
70
|
+
snowflake.connector.connect(database="db1", schema="schema1") as conn,
|
71
|
+
conn.cursor() as cur,
|
72
|
+
):
|
73
|
+
# creates db1.schema1.example
|
74
|
+
cur.execute("create table example (x int)")
|
75
|
+
cur.execute("insert into example values (420)")
|
76
|
+
|
77
|
+
# reconnect
|
78
|
+
with (
|
79
|
+
fakesnow.patch(db_path=db_path),
|
80
|
+
snowflake.connector.connect(database="db1", schema="schema1") as conn,
|
81
|
+
conn.cursor() as cur,
|
82
|
+
):
|
83
|
+
assert cur.execute("select * from example").fetchall() == [(420,)]
|
84
|
+
|
85
|
+
|
86
|
+
def test_connect_without_database(_fakesnow_no_auto_create: None):
|
87
|
+
with snowflake.connector.connect() as conn, conn.cursor() as cur:
|
88
|
+
with pytest.raises(snowflake.connector.errors.ProgrammingError) as excinfo:
|
89
|
+
cur.execute("select * from customers")
|
90
|
+
|
91
|
+
# actual snowflake error message is:
|
92
|
+
#
|
93
|
+
# 002003 (42S02): SQL compilation error:
|
94
|
+
# Object 'CUSTOMERS' does not exist or not authorized.
|
95
|
+
# assert (
|
96
|
+
# "002003 (42S02): Catalog Error: Table with name customers does not exist!"
|
97
|
+
# in str(excinfo.value)
|
98
|
+
# )
|
99
|
+
|
100
|
+
with pytest.raises(snowflake.connector.errors.ProgrammingError) as excinfo:
|
101
|
+
cur.execute("select * from jaffles.customers")
|
102
|
+
|
103
|
+
assert (
|
104
|
+
"090105 (22000): Cannot perform SELECT. This session does not have a current database. Call 'USE DATABASE', or use a qualified name."
|
105
|
+
in str(excinfo.value)
|
106
|
+
)
|
107
|
+
|
108
|
+
with pytest.raises(snowflake.connector.errors.ProgrammingError) as excinfo:
|
109
|
+
cur.execute("create schema jaffles")
|
110
|
+
|
111
|
+
assert (
|
112
|
+
"090105 (22000): Cannot perform CREATE SCHEMA. This session does not have a current database. Call 'USE DATABASE', or use a qualified name."
|
113
|
+
in str(excinfo.value)
|
114
|
+
)
|
115
|
+
|
116
|
+
with pytest.raises(snowflake.connector.errors.ProgrammingError) as excinfo:
|
117
|
+
cur.execute("use schema jaffles")
|
118
|
+
|
119
|
+
# assert (
|
120
|
+
# "002043 (02000): SQL compilation error:\nObject does not exist, or operation cannot be performed."
|
121
|
+
# in str(excinfo.value)
|
122
|
+
# )
|
123
|
+
|
124
|
+
with pytest.raises(snowflake.connector.errors.ProgrammingError) as excinfo:
|
125
|
+
cur.execute("create table customers (ID int, FIRST_NAME varchar, LAST_NAME varchar)")
|
126
|
+
|
127
|
+
assert (
|
128
|
+
"090105 (22000): Cannot perform CREATE TABLE. This session does not have a current database. Call 'USE DATABASE', or use a qualified name."
|
129
|
+
in str(excinfo.value)
|
130
|
+
)
|
131
|
+
|
132
|
+
# test description works without database
|
133
|
+
assert cur.execute("SELECT 1").fetchall() == [(1,)]
|
134
|
+
assert cur.description
|
135
|
+
|
136
|
+
|
137
|
+
def test_connect_without_schema(_fakesnow: None):
|
138
|
+
# database will be created but not schema
|
139
|
+
with snowflake.connector.connect(database="marts") as conn, conn.cursor() as cur:
|
140
|
+
assert not conn.schema
|
141
|
+
|
142
|
+
with pytest.raises(snowflake.connector.errors.ProgrammingError) as excinfo:
|
143
|
+
cur.execute("select * from customers")
|
144
|
+
|
145
|
+
# actual snowflake error message is:
|
146
|
+
#
|
147
|
+
# 002003 (42S02): SQL compilation error:
|
148
|
+
# Object 'CUSTOMERS' does not exist or not authorized.
|
149
|
+
# assert (
|
150
|
+
# "002003 (42S02): Catalog Error: Table with name customers does not exist!"
|
151
|
+
# in str(excinfo.value)
|
152
|
+
# )
|
153
|
+
|
154
|
+
with pytest.raises(snowflake.connector.errors.ProgrammingError) as excinfo:
|
155
|
+
cur.execute("create table customers (ID int, FIRST_NAME varchar, LAST_NAME varchar)")
|
156
|
+
|
157
|
+
assert (
|
158
|
+
"090106 (22000): Cannot perform CREATE TABLE. This session does not have a current schema. Call 'USE SCHEMA', or use a qualified name."
|
159
|
+
in str(excinfo.value)
|
160
|
+
)
|
161
|
+
|
162
|
+
# test description works without schema
|
163
|
+
assert cur.execute("SELECT 1").fetchall() == [(1,)]
|
164
|
+
assert cur.description
|
165
|
+
|
166
|
+
conn.execute_string("CREATE SCHEMA schema1; USE SCHEMA schema1;")
|
167
|
+
assert conn.schema == "SCHEMA1"
|
168
|
+
|
169
|
+
|
170
|
+
def test_connect_with_non_existent_db_or_schema(_fakesnow_no_auto_create: None):
|
171
|
+
# can connect with db that doesn't exist
|
172
|
+
with snowflake.connector.connect(database="marts") as conn, conn.cursor() as cur:
|
173
|
+
# but no valid database set
|
174
|
+
with pytest.raises(snowflake.connector.errors.ProgrammingError) as excinfo:
|
175
|
+
cur.execute("create table foobar (i int)")
|
176
|
+
|
177
|
+
assert (
|
178
|
+
"090105 (22000): Cannot perform CREATE TABLE. This session does not have a current database. Call 'USE DATABASE', or use a qualified name."
|
179
|
+
in str(excinfo.value)
|
180
|
+
)
|
181
|
+
|
182
|
+
# database still present on connection
|
183
|
+
assert conn.database == "MARTS"
|
184
|
+
|
185
|
+
cur.execute("CREATE database marts")
|
186
|
+
|
187
|
+
# can connect with schema that doesn't exist
|
188
|
+
with snowflake.connector.connect(database="marts", schema="jaffles") as conn, conn.cursor() as cur:
|
189
|
+
# but no valid schema set
|
190
|
+
with pytest.raises(snowflake.connector.errors.ProgrammingError) as excinfo:
|
191
|
+
cur.execute("create table foobar (i int)")
|
192
|
+
|
193
|
+
assert (
|
194
|
+
"090106 (22000): Cannot perform CREATE TABLE. This session does not have a current schema. Call 'USE SCHEMA', or use a qualified name."
|
195
|
+
in str(excinfo.value)
|
196
|
+
)
|
197
|
+
|
198
|
+
# schema still present on connection
|
199
|
+
assert conn.schema == "JAFFLES"
|
@@ -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
|
|
@@ -171,178 +172,6 @@ def test_close_cur(conn: snowflake.connector.SnowflakeConnection, cur: snowflake
|
|
171
172
|
assert cur.close() is True
|
172
173
|
|
173
174
|
|
174
|
-
def test_connect_auto_create(_fakesnow: None):
|
175
|
-
with snowflake.connector.connect(database="db1", schema="schema1"):
|
176
|
-
# creates db1 and schema1
|
177
|
-
pass
|
178
|
-
|
179
|
-
with snowflake.connector.connect(database="db1", schema="schema1"):
|
180
|
-
# connects again and reuses db1 and schema1
|
181
|
-
pass
|
182
|
-
|
183
|
-
|
184
|
-
def test_connect_different_sessions_use_database(_fakesnow_no_auto_create: None):
|
185
|
-
# connect without default database and schema
|
186
|
-
with snowflake.connector.connect() as conn1, conn1.cursor() as cur:
|
187
|
-
# use the table's fully qualified name
|
188
|
-
cur.execute("create database marts")
|
189
|
-
cur.execute("create schema marts.jaffles")
|
190
|
-
cur.execute("create table marts.jaffles.customers (ID int, FIRST_NAME varchar, LAST_NAME varchar)")
|
191
|
-
cur.execute("insert into marts.jaffles.customers values (1, 'Jenny', 'P')")
|
192
|
-
|
193
|
-
# use database and schema
|
194
|
-
cur.execute("use database marts")
|
195
|
-
cur.execute("use schema jaffles")
|
196
|
-
cur.execute("insert into customers values (2, 'Jasper', 'M')")
|
197
|
-
|
198
|
-
# in a separate connection, connect using the database and schema from above
|
199
|
-
with snowflake.connector.connect(database="marts", schema="jaffles") as conn2, conn2.cursor() as cur:
|
200
|
-
cur.execute("select id, first_name, last_name from customers")
|
201
|
-
assert cur.fetchall() == [(1, "Jenny", "P"), (2, "Jasper", "M")]
|
202
|
-
|
203
|
-
|
204
|
-
def test_connect_reuse_db():
|
205
|
-
with tempfile.TemporaryDirectory(prefix="fakesnow-test") as db_path:
|
206
|
-
with (
|
207
|
-
fakesnow.patch(db_path=db_path),
|
208
|
-
snowflake.connector.connect(database="db1", schema="schema1") as conn,
|
209
|
-
conn.cursor() as cur,
|
210
|
-
):
|
211
|
-
# creates db1.schema1.example
|
212
|
-
cur.execute("create table example (x int)")
|
213
|
-
cur.execute("insert into example values (420)")
|
214
|
-
|
215
|
-
# reconnect
|
216
|
-
with (
|
217
|
-
fakesnow.patch(db_path=db_path),
|
218
|
-
snowflake.connector.connect(database="db1", schema="schema1") as conn,
|
219
|
-
conn.cursor() as cur,
|
220
|
-
):
|
221
|
-
assert cur.execute("select * from example").fetchall() == [(420,)]
|
222
|
-
|
223
|
-
|
224
|
-
def test_connect_db_path_can_create_database() -> None:
|
225
|
-
with tempfile.TemporaryDirectory(prefix="fakesnow-test") as db_path, fakesnow.patch(db_path=db_path):
|
226
|
-
cursor = snowflake.connector.connect().cursor()
|
227
|
-
cursor.execute("CREATE DATABASE db2")
|
228
|
-
|
229
|
-
|
230
|
-
def test_connect_without_database(_fakesnow_no_auto_create: None):
|
231
|
-
with snowflake.connector.connect() as conn, conn.cursor() as cur:
|
232
|
-
with pytest.raises(snowflake.connector.errors.ProgrammingError) as excinfo:
|
233
|
-
cur.execute("select * from customers")
|
234
|
-
|
235
|
-
# actual snowflake error message is:
|
236
|
-
#
|
237
|
-
# 002003 (42S02): SQL compilation error:
|
238
|
-
# Object 'CUSTOMERS' does not exist or not authorized.
|
239
|
-
# assert (
|
240
|
-
# "002003 (42S02): Catalog Error: Table with name customers does not exist!"
|
241
|
-
# in str(excinfo.value)
|
242
|
-
# )
|
243
|
-
|
244
|
-
with pytest.raises(snowflake.connector.errors.ProgrammingError) as excinfo:
|
245
|
-
cur.execute("select * from jaffles.customers")
|
246
|
-
|
247
|
-
assert (
|
248
|
-
"090105 (22000): Cannot perform SELECT. This session does not have a current database. Call 'USE DATABASE', or use a qualified name."
|
249
|
-
in str(excinfo.value)
|
250
|
-
)
|
251
|
-
|
252
|
-
with pytest.raises(snowflake.connector.errors.ProgrammingError) as excinfo:
|
253
|
-
cur.execute("create schema jaffles")
|
254
|
-
|
255
|
-
assert (
|
256
|
-
"090105 (22000): Cannot perform CREATE SCHEMA. This session does not have a current database. Call 'USE DATABASE', or use a qualified name."
|
257
|
-
in str(excinfo.value)
|
258
|
-
)
|
259
|
-
|
260
|
-
with pytest.raises(snowflake.connector.errors.ProgrammingError) as excinfo:
|
261
|
-
cur.execute("use schema jaffles")
|
262
|
-
|
263
|
-
# assert (
|
264
|
-
# "002043 (02000): SQL compilation error:\nObject does not exist, or operation cannot be performed."
|
265
|
-
# in str(excinfo.value)
|
266
|
-
# )
|
267
|
-
|
268
|
-
with pytest.raises(snowflake.connector.errors.ProgrammingError) as excinfo:
|
269
|
-
cur.execute("create table customers (ID int, FIRST_NAME varchar, LAST_NAME varchar)")
|
270
|
-
|
271
|
-
assert (
|
272
|
-
"090105 (22000): Cannot perform CREATE TABLE. This session does not have a current database. Call 'USE DATABASE', or use a qualified name."
|
273
|
-
in str(excinfo.value)
|
274
|
-
)
|
275
|
-
|
276
|
-
# test description works without database
|
277
|
-
assert cur.execute("SELECT 1").fetchall() == [(1,)]
|
278
|
-
assert cur.description
|
279
|
-
|
280
|
-
|
281
|
-
def test_connect_without_schema(_fakesnow: None):
|
282
|
-
# database will be created but not schema
|
283
|
-
with snowflake.connector.connect(database="marts") as conn, conn.cursor() as cur:
|
284
|
-
assert not conn.schema
|
285
|
-
|
286
|
-
with pytest.raises(snowflake.connector.errors.ProgrammingError) as excinfo:
|
287
|
-
cur.execute("select * from customers")
|
288
|
-
|
289
|
-
# actual snowflake error message is:
|
290
|
-
#
|
291
|
-
# 002003 (42S02): SQL compilation error:
|
292
|
-
# Object 'CUSTOMERS' does not exist or not authorized.
|
293
|
-
# assert (
|
294
|
-
# "002003 (42S02): Catalog Error: Table with name customers does not exist!"
|
295
|
-
# in str(excinfo.value)
|
296
|
-
# )
|
297
|
-
|
298
|
-
with pytest.raises(snowflake.connector.errors.ProgrammingError) as excinfo:
|
299
|
-
cur.execute("create table customers (ID int, FIRST_NAME varchar, LAST_NAME varchar)")
|
300
|
-
|
301
|
-
assert (
|
302
|
-
"090106 (22000): Cannot perform CREATE TABLE. This session does not have a current schema. Call 'USE SCHEMA', or use a qualified name."
|
303
|
-
in str(excinfo.value)
|
304
|
-
)
|
305
|
-
|
306
|
-
# test description works without schema
|
307
|
-
assert cur.execute("SELECT 1").fetchall() == [(1,)]
|
308
|
-
assert cur.description
|
309
|
-
|
310
|
-
conn.execute_string("CREATE SCHEMA schema1; USE SCHEMA schema1;")
|
311
|
-
assert conn.schema == "SCHEMA1"
|
312
|
-
|
313
|
-
|
314
|
-
def test_connect_with_non_existent_db_or_schema(_fakesnow_no_auto_create: None):
|
315
|
-
# can connect with db that doesn't exist
|
316
|
-
with snowflake.connector.connect(database="marts") as conn, conn.cursor() as cur:
|
317
|
-
# but no valid database set
|
318
|
-
with pytest.raises(snowflake.connector.errors.ProgrammingError) as excinfo:
|
319
|
-
cur.execute("create table foobar (i int)")
|
320
|
-
|
321
|
-
assert (
|
322
|
-
"090105 (22000): Cannot perform CREATE TABLE. This session does not have a current database. Call 'USE DATABASE', or use a qualified name."
|
323
|
-
in str(excinfo.value)
|
324
|
-
)
|
325
|
-
|
326
|
-
# database still present on connection
|
327
|
-
assert conn.database == "MARTS"
|
328
|
-
|
329
|
-
cur.execute("CREATE database marts")
|
330
|
-
|
331
|
-
# can connect with schema that doesn't exist
|
332
|
-
with snowflake.connector.connect(database="marts", schema="jaffles") as conn, conn.cursor() as cur:
|
333
|
-
# but no valid schema set
|
334
|
-
with pytest.raises(snowflake.connector.errors.ProgrammingError) as excinfo:
|
335
|
-
cur.execute("create table foobar (i int)")
|
336
|
-
|
337
|
-
assert (
|
338
|
-
"090106 (22000): Cannot perform CREATE TABLE. This session does not have a current schema. Call 'USE SCHEMA', or use a qualified name."
|
339
|
-
in str(excinfo.value)
|
340
|
-
)
|
341
|
-
|
342
|
-
# schema still present on connection
|
343
|
-
assert conn.schema == "JAFFLES"
|
344
|
-
|
345
|
-
|
346
175
|
def test_create_database_respects_if_not_exists() -> None:
|
347
176
|
with tempfile.TemporaryDirectory(prefix="fakesnow-test") as db_path, fakesnow.patch(db_path=db_path):
|
348
177
|
cursor = snowflake.connector.connect().cursor()
|
@@ -1400,6 +1229,12 @@ def test_sfqid(cur: snowflake.connector.cursor.SnowflakeCursor):
|
|
1400
1229
|
assert cur.sfqid == "fakesnow"
|
1401
1230
|
|
1402
1231
|
|
1232
|
+
def test_string_constant(cur: snowflake.connector.cursor.SnowflakeCursor):
|
1233
|
+
assert cur.execute("""
|
1234
|
+
select $$hello
|
1235
|
+
world$$""").fetchall() == [("hello\nworld",)]
|
1236
|
+
|
1237
|
+
|
1403
1238
|
def test_tags_noop(cur: snowflake.connector.cursor.SnowflakeCursor):
|
1404
1239
|
cur.execute("CREATE TABLE table1 (id int)")
|
1405
1240
|
cur.execute("ALTER TABLE table1 SET TAG foo='bar'")
|
@@ -1557,6 +1392,53 @@ def test_use_invalid_schema(_fakesnow: None):
|
|
1557
1392
|
)
|
1558
1393
|
|
1559
1394
|
|
1395
|
+
# Snowflake SQL variables: https://docs.snowflake.com/en/sql-reference/session-variables#using-variables-in-sql
|
1396
|
+
#
|
1397
|
+
# Variables are scoped to the session (Eg. The connection, not the cursor)
|
1398
|
+
# [x] Simple scalar variables: SET var1 = 1;
|
1399
|
+
# [x] Unset variables: UNSET var1;
|
1400
|
+
# [x] Simple SQL expression variables: SET INCREMENTAL_DATE = DATEADD( 'DAY', -7, CURRENT_DATE());
|
1401
|
+
# [x] Basic use of variables in SQL using $ syntax: SELECT $var1;
|
1402
|
+
# [ ] Multiple variables: SET (var1, var2) = (1, 'hello');
|
1403
|
+
# [ ] Variables set via 'properties' on the connection https://docs.snowflake.com/en/sql-reference/session-variables#setting-variables-on-connection
|
1404
|
+
# [ ] Using variables via the IDENTIFIER function: INSERT INTO IDENTIFIER($my_table_name) (i) VALUES (42);
|
1405
|
+
# [ ] Session variable functions: https://docs.snowflake.com/en/sql-reference/session-variables#session-variable-functions
|
1406
|
+
def test_variables(conn: snowflake.connector.SnowflakeConnection):
|
1407
|
+
with conn.cursor() as cur:
|
1408
|
+
cur.execute("SET var1 = 1;")
|
1409
|
+
cur.execute("SET var2 = 'hello';")
|
1410
|
+
cur.execute("SET var3 = DATEADD( 'DAY', -7, '2024-10-09');")
|
1411
|
+
|
1412
|
+
cur.execute("select $var1, $var2, $var3;")
|
1413
|
+
assert cur.fetchall() == [(1, "hello", datetime.datetime(2024, 10, 2, 0, 0))]
|
1414
|
+
|
1415
|
+
cur.execute("CREATE TABLE example (id int, name varchar);")
|
1416
|
+
cur.execute("INSERT INTO example VALUES (10, 'hello'), (20, 'world');")
|
1417
|
+
cur.execute("select id, name from example where name = $var2;")
|
1418
|
+
assert cur.fetchall() == [(10, "hello")]
|
1419
|
+
|
1420
|
+
cur.execute("UNSET var3;")
|
1421
|
+
with pytest.raises(
|
1422
|
+
snowflake.connector.errors.ProgrammingError, match=re.escape("Session variable '$VAR3' does not exist")
|
1423
|
+
):
|
1424
|
+
cur.execute("select $var3;")
|
1425
|
+
|
1426
|
+
# variables are scoped to the session, so they should be available in a new cursor.
|
1427
|
+
with conn.cursor() as cur:
|
1428
|
+
cur.execute("select $var1, $var2")
|
1429
|
+
assert cur.fetchall() == [(1, "hello")]
|
1430
|
+
|
1431
|
+
# but not in a new connection.
|
1432
|
+
with (
|
1433
|
+
snowflake.connector.connect() as conn,
|
1434
|
+
conn.cursor() as cur,
|
1435
|
+
pytest.raises(
|
1436
|
+
snowflake.connector.errors.ProgrammingError, match=re.escape("Session variable '$VAR1' does not exist")
|
1437
|
+
),
|
1438
|
+
):
|
1439
|
+
cur.execute("select $var1;")
|
1440
|
+
|
1441
|
+
|
1560
1442
|
def test_values(conn: snowflake.connector.SnowflakeConnection):
|
1561
1443
|
with conn.cursor(snowflake.connector.cursor.DictCursor) as cur:
|
1562
1444
|
cur.execute("select * from VALUES ('Amsterdam', 1), ('London', 2)")
|
@@ -193,3 +193,21 @@ def test_info_schema_views_with_views(conn: snowflake.connector.SnowflakeConnect
|
|
193
193
|
"comment": None,
|
194
194
|
}
|
195
195
|
]
|
196
|
+
|
197
|
+
|
198
|
+
def test_info_schema_show_primary_keys_from_table(cur: snowflake.connector.cursor.SnowflakeCursor) -> None:
|
199
|
+
cur.execute(
|
200
|
+
"""
|
201
|
+
CREATE TABLE test_table (
|
202
|
+
ID varchar,
|
203
|
+
VERSION varchar,
|
204
|
+
PRIMARY KEY (ID, VERSION)
|
205
|
+
)
|
206
|
+
"""
|
207
|
+
)
|
208
|
+
|
209
|
+
cur.execute("SHOW PRIMARY KEYS IN test_table")
|
210
|
+
pk_result = cur.fetchall()
|
211
|
+
|
212
|
+
pk_columns = [result[4] for result in pk_result]
|
213
|
+
assert pk_columns == ["ID", "VERSION"]
|
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
|