fakesnow 0.9.20__py3-none-any.whl → 0.9.21__py3-none-any.whl
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/__init__.py +8 -14
- fakesnow/arrow.py +32 -0
- fakesnow/fakes.py +12 -5
- fakesnow/info_schema.py +4 -4
- fakesnow/instance.py +92 -0
- fakesnow/server.py +108 -0
- fakesnow/transforms.py +20 -7
- fakesnow/variables.py +19 -6
- {fakesnow-0.9.20.dist-info → fakesnow-0.9.21.dist-info}/METADATA +8 -3
- fakesnow-0.9.21.dist-info/RECORD +21 -0
- fakesnow/global_database.py +0 -46
- fakesnow-0.9.20.dist-info/RECORD +0 -19
- {fakesnow-0.9.20.dist-info → fakesnow-0.9.21.dist-info}/LICENSE +0 -0
- {fakesnow-0.9.20.dist-info → fakesnow-0.9.21.dist-info}/WHEEL +0 -0
- {fakesnow-0.9.20.dist-info → fakesnow-0.9.21.dist-info}/entry_points.txt +0 -0
- {fakesnow-0.9.20.dist-info → fakesnow-0.9.21.dist-info}/top_level.txt +0 -0
fakesnow/__init__.py
CHANGED
@@ -8,12 +8,11 @@ import unittest.mock as mock
|
|
8
8
|
from collections.abc import Iterator, Sequence
|
9
9
|
from contextlib import contextmanager
|
10
10
|
|
11
|
-
import duckdb
|
12
11
|
import snowflake.connector
|
13
12
|
import snowflake.connector.pandas_tools
|
14
13
|
|
15
14
|
import fakesnow.fakes as fakes
|
16
|
-
from fakesnow.
|
15
|
+
from fakesnow.instance import FakeSnow
|
17
16
|
|
18
17
|
|
19
18
|
@contextmanager
|
@@ -52,20 +51,15 @@ def patch(
|
|
52
51
|
# won't be able to patch extra targets
|
53
52
|
assert not isinstance(snowflake.connector.connect, mock.MagicMock), "Snowflake connector is already patched"
|
54
53
|
|
55
|
-
|
56
|
-
|
54
|
+
fs = FakeSnow(
|
55
|
+
create_database_on_connect=create_database_on_connect,
|
56
|
+
create_schema_on_connect=create_schema_on_connect,
|
57
|
+
db_path=db_path,
|
58
|
+
nop_regexes=nop_regexes,
|
59
|
+
)
|
57
60
|
|
58
61
|
fake_fns = {
|
59
|
-
|
60
|
-
# schema setting, see https://duckdb.org/docs/api/python/overview.html#startup--shutdown
|
61
|
-
snowflake.connector.connect: lambda **kwargs: fakes.FakeSnowflakeConnection(
|
62
|
-
duck_conn.cursor(),
|
63
|
-
create_database=create_database_on_connect,
|
64
|
-
create_schema=create_schema_on_connect,
|
65
|
-
db_path=db_path,
|
66
|
-
nop_regexes=nop_regexes,
|
67
|
-
**kwargs,
|
68
|
-
),
|
62
|
+
snowflake.connector.connect: fs.connect,
|
69
63
|
snowflake.connector.pandas_tools.write_pandas: fakes.write_pandas,
|
70
64
|
}
|
71
65
|
|
fakesnow/arrow.py
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
import pyarrow as pa
|
2
|
+
|
3
|
+
|
4
|
+
def with_sf_metadata(schema: pa.Schema) -> pa.Schema:
|
5
|
+
# see https://github.com/snowflakedb/snowflake-connector-python/blob/e9393a6/src/snowflake/connector/nanoarrow_cpp/ArrowIterator/CArrowTableIterator.cpp#L32
|
6
|
+
# and https://github.com/snowflakedb/snowflake-connector-python/blob/e9393a6/src/snowflake/connector/nanoarrow_cpp/ArrowIterator/SnowflakeType.cpp#L10
|
7
|
+
fms = []
|
8
|
+
for i, t in enumerate(schema.types):
|
9
|
+
f = schema.field(i)
|
10
|
+
|
11
|
+
if isinstance(t, pa.Decimal128Type):
|
12
|
+
fm = f.with_metadata({"logicalType": "FIXED", "precision": str(t.precision), "scale": str(t.scale)})
|
13
|
+
elif t == pa.string():
|
14
|
+
fm = f.with_metadata({"logicalType": "TEXT"})
|
15
|
+
else:
|
16
|
+
raise NotImplementedError(f"Unsupported Arrow type: {t}")
|
17
|
+
fms.append(fm)
|
18
|
+
return pa.schema(fms)
|
19
|
+
|
20
|
+
|
21
|
+
def to_ipc(table: pa.Table) -> pa.Buffer:
|
22
|
+
batches = table.to_batches()
|
23
|
+
if len(batches) != 1:
|
24
|
+
raise NotImplementedError(f"{len(batches)} batches")
|
25
|
+
batch = batches[0]
|
26
|
+
|
27
|
+
sink = pa.BufferOutputStream()
|
28
|
+
|
29
|
+
with pa.ipc.new_stream(sink, with_sf_metadata(batch.schema)) as writer:
|
30
|
+
writer.write_batch(batch)
|
31
|
+
|
32
|
+
return sink.getvalue()
|
fakesnow/fakes.py
CHANGED
@@ -203,6 +203,7 @@ class FakeSnowflakeCursor:
|
|
203
203
|
.transform(transforms.sha256)
|
204
204
|
.transform(transforms.create_clone)
|
205
205
|
.transform(transforms.alias_in_join)
|
206
|
+
.transform(transforms.alter_table_strip_cluster_by)
|
206
207
|
)
|
207
208
|
|
208
209
|
def _execute(
|
@@ -522,9 +523,14 @@ class FakeSnowflakeConnection:
|
|
522
523
|
):
|
523
524
|
self._duck_conn = duck_conn
|
524
525
|
# upper case database and schema like snowflake unquoted identifiers
|
525
|
-
#
|
526
|
+
# so they appear as upper-cased in information_schema
|
527
|
+
# catalog and schema names are not actually case-sensitive in duckdb even though
|
528
|
+
# they are as cased in information_schema.schemata, so when selecting from
|
529
|
+
# information_schema.schemata below we use upper-case to match any existing duckdb
|
530
|
+
# catalog or schemas like "information_schema"
|
526
531
|
self.database = database and database.upper()
|
527
532
|
self.schema = schema and schema.upper()
|
533
|
+
|
528
534
|
self.database_set = False
|
529
535
|
self.schema_set = False
|
530
536
|
self.db_path = Path(db_path) if db_path else None
|
@@ -538,7 +544,7 @@ class FakeSnowflakeConnection:
|
|
538
544
|
and self.database
|
539
545
|
and not duck_conn.execute(
|
540
546
|
f"""select * from information_schema.schemata
|
541
|
-
where catalog_name = '{self.database}'"""
|
547
|
+
where upper(catalog_name) = '{self.database}'"""
|
542
548
|
).fetchone()
|
543
549
|
):
|
544
550
|
db_file = f"{self.db_path/self.database}.db" if self.db_path else ":memory:"
|
@@ -553,7 +559,7 @@ class FakeSnowflakeConnection:
|
|
553
559
|
and self.schema
|
554
560
|
and not duck_conn.execute(
|
555
561
|
f"""select * from information_schema.schemata
|
556
|
-
where catalog_name = '{self.database}' and schema_name = '{self.schema}'"""
|
562
|
+
where upper(catalog_name) = '{self.database}' and upper(schema_name) = '{self.schema}'"""
|
557
563
|
).fetchone()
|
558
564
|
):
|
559
565
|
duck_conn.execute(f"CREATE SCHEMA {self.database}.{self.schema}")
|
@@ -564,7 +570,7 @@ class FakeSnowflakeConnection:
|
|
564
570
|
and self.schema
|
565
571
|
and duck_conn.execute(
|
566
572
|
f"""select * from information_schema.schemata
|
567
|
-
where catalog_name = '{self.database}' and schema_name = '{self.schema}'"""
|
573
|
+
where upper(catalog_name) = '{self.database}' and upper(schema_name) = '{self.schema}'"""
|
568
574
|
).fetchone()
|
569
575
|
):
|
570
576
|
duck_conn.execute(f"SET schema='{self.database}.{self.schema}'")
|
@@ -575,7 +581,7 @@ class FakeSnowflakeConnection:
|
|
575
581
|
self.database
|
576
582
|
and duck_conn.execute(
|
577
583
|
f"""select * from information_schema.schemata
|
578
|
-
where catalog_name = '{self.database}'"""
|
584
|
+
where upper(catalog_name) = '{self.database}'"""
|
579
585
|
).fetchone()
|
580
586
|
):
|
581
587
|
duck_conn.execute(f"SET schema='{self.database}.main'")
|
@@ -602,6 +608,7 @@ class FakeSnowflakeConnection:
|
|
602
608
|
self.cursor().execute("COMMIT")
|
603
609
|
|
604
610
|
def cursor(self, cursor_class: type[SnowflakeCursor] = SnowflakeCursor) -> FakeSnowflakeCursor:
|
611
|
+
# TODO: use duck_conn cursor for thread-safety
|
605
612
|
return FakeSnowflakeCursor(conn=self, duck_conn=self._duck_conn, use_dict_result=cursor_class == DictCursor)
|
606
613
|
|
607
614
|
def execute_string(
|
fakesnow/info_schema.py
CHANGED
@@ -62,8 +62,8 @@ case when columns.data_type='BIGINT' then 10
|
|
62
62
|
case when columns.data_type='DOUBLE' then NULL else columns.numeric_scale end as numeric_scale,
|
63
63
|
collation_name, is_identity, identity_generation, identity_cycle,
|
64
64
|
ddb_columns.comment as comment,
|
65
|
-
null as identity_start,
|
66
|
-
null as identity_increment,
|
65
|
+
null::VARCHAR as identity_start,
|
66
|
+
null::VARCHAR as identity_increment,
|
67
67
|
from ${catalog}.information_schema.columns columns
|
68
68
|
left join ${catalog}.information_schema._fs_columns_ext ext
|
69
69
|
on ext_table_catalog = columns.table_catalog
|
@@ -86,7 +86,7 @@ select
|
|
86
86
|
catalog_name as database_name,
|
87
87
|
'SYSADMIN' as database_owner,
|
88
88
|
'NO' as is_transient,
|
89
|
-
null as comment,
|
89
|
+
null::VARCHAR as comment,
|
90
90
|
to_timestamp(0)::timestamptz as created,
|
91
91
|
to_timestamp(0)::timestamptz as last_altered,
|
92
92
|
1 as retention_time,
|
@@ -116,7 +116,7 @@ select
|
|
116
116
|
to_timestamp(0)::timestamptz as last_altered,
|
117
117
|
to_timestamp(0)::timestamptz as last_ddl,
|
118
118
|
'SYSADMIN' as last_ddl_by,
|
119
|
-
null as comment
|
119
|
+
null::VARCHAR as comment
|
120
120
|
from duckdb_views
|
121
121
|
where database_name = '${catalog}'
|
122
122
|
and schema_name != 'information_schema'
|
fakesnow/instance.py
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import os
|
4
|
+
from typing import Any
|
5
|
+
|
6
|
+
import duckdb
|
7
|
+
|
8
|
+
import fakesnow.fakes as fakes
|
9
|
+
|
10
|
+
GLOBAL_DATABASE_NAME = "_fs_global"
|
11
|
+
USERS_TABLE_FQ_NAME = f"{GLOBAL_DATABASE_NAME}._fs_users_ext"
|
12
|
+
|
13
|
+
# replicates the output structure of https://docs.snowflake.com/en/sql-reference/sql/show-users
|
14
|
+
SQL_CREATE_INFORMATION_SCHEMA_USERS_TABLE_EXT = f"""
|
15
|
+
create table if not exists {USERS_TABLE_FQ_NAME} (
|
16
|
+
name varchar,
|
17
|
+
created_on TIMESTAMPTZ,
|
18
|
+
login_name varchar,
|
19
|
+
display_name varchar,
|
20
|
+
first_name varchar,
|
21
|
+
last_name varchar,
|
22
|
+
email varchar,
|
23
|
+
mins_to_unlock varchar,
|
24
|
+
days_to_expiry varchar,
|
25
|
+
comment varchar,
|
26
|
+
disabled varchar,
|
27
|
+
must_change_password varchar,
|
28
|
+
snowflake_lock varchar,
|
29
|
+
default_warehouse varchar,
|
30
|
+
default_namespace varchar,
|
31
|
+
default_role varchar,
|
32
|
+
default_secondary_roles varchar,
|
33
|
+
ext_authn_duo varchar,
|
34
|
+
ext_authn_uid varchar,
|
35
|
+
mins_to_bypass_mfa varchar,
|
36
|
+
owner varchar,
|
37
|
+
last_success_login TIMESTAMPTZ,
|
38
|
+
expires_at_time TIMESTAMPTZ,
|
39
|
+
locked_until_time TIMESTAMPTZ,
|
40
|
+
has_password varchar,
|
41
|
+
has_rsa_public_key varchar,
|
42
|
+
)
|
43
|
+
"""
|
44
|
+
|
45
|
+
|
46
|
+
def create_global_database(conn: duckdb.DuckDBPyConnection) -> None:
|
47
|
+
"""Create a "global" database for storing objects which span databases.
|
48
|
+
|
49
|
+
Including (but not limited to):
|
50
|
+
- Users
|
51
|
+
"""
|
52
|
+
conn.execute(f"ATTACH IF NOT EXISTS ':memory:' AS {GLOBAL_DATABASE_NAME}")
|
53
|
+
conn.execute(SQL_CREATE_INFORMATION_SCHEMA_USERS_TABLE_EXT)
|
54
|
+
|
55
|
+
|
56
|
+
class FakeSnow:
|
57
|
+
def __init__(
|
58
|
+
self,
|
59
|
+
create_database_on_connect: bool = True,
|
60
|
+
create_schema_on_connect: bool = True,
|
61
|
+
db_path: str | os.PathLike | None = None,
|
62
|
+
nop_regexes: list[str] | None = None,
|
63
|
+
):
|
64
|
+
self.create_database_on_connect = create_database_on_connect
|
65
|
+
self.create_schema_on_connect = create_schema_on_connect
|
66
|
+
self.db_path = db_path
|
67
|
+
self.nop_regexes = nop_regexes
|
68
|
+
|
69
|
+
self.duck_conn = duckdb.connect(database=":memory:")
|
70
|
+
|
71
|
+
# create a "global" database for storing objects which span databases.
|
72
|
+
self.duck_conn.execute(f"ATTACH IF NOT EXISTS ':memory:' AS {GLOBAL_DATABASE_NAME}")
|
73
|
+
self.duck_conn.execute(SQL_CREATE_INFORMATION_SCHEMA_USERS_TABLE_EXT)
|
74
|
+
|
75
|
+
def connect(
|
76
|
+
self, database: str | None = None, schema: str | None = None, **kwargs: Any
|
77
|
+
) -> fakes.FakeSnowflakeConnection:
|
78
|
+
# every time we connect, create a new cursor (ie: connection) so we can isolate each connection's
|
79
|
+
# schema setting see
|
80
|
+
# https://github.com/duckdb/duckdb/blob/18254ec/tools/pythonpkg/src/pyconnection.cpp#L1440
|
81
|
+
# and to make connections thread-safe see
|
82
|
+
# https://duckdb.org/docs/api/python/overview.html#using-connections-in-parallel-python-programs
|
83
|
+
return fakes.FakeSnowflakeConnection(
|
84
|
+
self.duck_conn.cursor(),
|
85
|
+
database,
|
86
|
+
schema,
|
87
|
+
create_database=self.create_database_on_connect,
|
88
|
+
create_schema=self.create_schema_on_connect,
|
89
|
+
db_path=self.db_path,
|
90
|
+
nop_regexes=self.nop_regexes,
|
91
|
+
**kwargs,
|
92
|
+
)
|
fakesnow/server.py
ADDED
@@ -0,0 +1,108 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import gzip
|
4
|
+
import json
|
5
|
+
import secrets
|
6
|
+
from base64 import b64encode
|
7
|
+
from dataclasses import dataclass
|
8
|
+
|
9
|
+
from starlette.applications import Starlette
|
10
|
+
from starlette.concurrency import run_in_threadpool
|
11
|
+
from starlette.requests import Request
|
12
|
+
from starlette.responses import JSONResponse
|
13
|
+
from starlette.routing import Route
|
14
|
+
|
15
|
+
from fakesnow.arrow import to_ipc
|
16
|
+
from fakesnow.fakes import FakeSnowflakeConnection
|
17
|
+
from fakesnow.instance import FakeSnow
|
18
|
+
|
19
|
+
fs = FakeSnow()
|
20
|
+
sessions = {}
|
21
|
+
|
22
|
+
|
23
|
+
@dataclass
|
24
|
+
class ServerError(Exception):
|
25
|
+
status_code: int
|
26
|
+
code: str
|
27
|
+
message: str
|
28
|
+
|
29
|
+
|
30
|
+
def login_request(request: Request) -> JSONResponse:
|
31
|
+
database = request.query_params.get("databaseName")
|
32
|
+
schema = request.query_params.get("schemaName")
|
33
|
+
token = secrets.token_urlsafe(32)
|
34
|
+
sessions[token] = fs.connect(database, schema)
|
35
|
+
return JSONResponse({"data": {"token": token}, "success": True})
|
36
|
+
|
37
|
+
|
38
|
+
async def query_request(request: Request) -> JSONResponse:
|
39
|
+
try:
|
40
|
+
conn = to_conn(request)
|
41
|
+
|
42
|
+
body = await request.body()
|
43
|
+
body_json = json.loads(gzip.decompress(body))
|
44
|
+
|
45
|
+
sql_text = body_json["sqlText"]
|
46
|
+
|
47
|
+
# only a single sql statement is sent at a time by the python snowflake connector
|
48
|
+
cur = await run_in_threadpool(conn.cursor().execute, sql_text)
|
49
|
+
|
50
|
+
assert cur._arrow_table, "No result set" # noqa: SLF001
|
51
|
+
|
52
|
+
batch_bytes = to_ipc(cur._arrow_table) # noqa: SLF001
|
53
|
+
rowset_b64 = b64encode(batch_bytes).decode("utf-8")
|
54
|
+
|
55
|
+
return JSONResponse(
|
56
|
+
{
|
57
|
+
"data": {
|
58
|
+
"rowtype": [
|
59
|
+
{
|
60
|
+
"name": "'HELLO WORLD'",
|
61
|
+
"nullable": False,
|
62
|
+
"type": "text",
|
63
|
+
"length": 11,
|
64
|
+
"scale": None,
|
65
|
+
"precision": None,
|
66
|
+
}
|
67
|
+
],
|
68
|
+
"rowsetBase64": rowset_b64,
|
69
|
+
"total": 1,
|
70
|
+
"queryResultFormat": "arrow",
|
71
|
+
},
|
72
|
+
"success": True,
|
73
|
+
}
|
74
|
+
)
|
75
|
+
|
76
|
+
except ServerError as e:
|
77
|
+
return JSONResponse(
|
78
|
+
{"data": None, "code": e.code, "message": e.message, "success": False, "headers": None},
|
79
|
+
status_code=e.status_code,
|
80
|
+
)
|
81
|
+
|
82
|
+
|
83
|
+
def to_conn(request: Request) -> FakeSnowflakeConnection:
|
84
|
+
if not (auth := request.headers.get("Authorization")):
|
85
|
+
raise ServerError(status_code=401, code="390103", message="Session token not found in the request data.")
|
86
|
+
|
87
|
+
token = auth[17:-1]
|
88
|
+
|
89
|
+
if not (conn := sessions.get(token)):
|
90
|
+
raise ServerError(status_code=401, code="390104", message="User must login again to access the service.")
|
91
|
+
|
92
|
+
return conn
|
93
|
+
|
94
|
+
|
95
|
+
routes = [
|
96
|
+
Route(
|
97
|
+
"/session/v1/login-request",
|
98
|
+
login_request,
|
99
|
+
methods=["POST"],
|
100
|
+
),
|
101
|
+
Route(
|
102
|
+
"/queries/v1/query-request",
|
103
|
+
query_request,
|
104
|
+
methods=["POST"],
|
105
|
+
),
|
106
|
+
]
|
107
|
+
|
108
|
+
app = Starlette(debug=True, routes=routes)
|
fakesnow/transforms.py
CHANGED
@@ -7,11 +7,11 @@ from typing import ClassVar, Literal, cast
|
|
7
7
|
import sqlglot
|
8
8
|
from sqlglot import exp
|
9
9
|
|
10
|
-
from fakesnow.
|
10
|
+
from fakesnow.instance import USERS_TABLE_FQ_NAME
|
11
11
|
from fakesnow.variables import Variables
|
12
12
|
|
13
13
|
MISSING_DATABASE = "missing_database"
|
14
|
-
SUCCESS_NOP = sqlglot.parse_one("SELECT 'Statement executed successfully.'")
|
14
|
+
SUCCESS_NOP = sqlglot.parse_one("SELECT 'Statement executed successfully.' as status")
|
15
15
|
|
16
16
|
|
17
17
|
def alias_in_join(expression: exp.Expression) -> exp.Expression:
|
@@ -33,6 +33,18 @@ def alias_in_join(expression: exp.Expression) -> exp.Expression:
|
|
33
33
|
return expression
|
34
34
|
|
35
35
|
|
36
|
+
def alter_table_strip_cluster_by(expression: exp.Expression) -> exp.Expression:
|
37
|
+
"""Turn alter table cluster by into a no-op"""
|
38
|
+
if (
|
39
|
+
isinstance(expression, exp.AlterTable)
|
40
|
+
and (actions := expression.args.get("actions"))
|
41
|
+
and len(actions) == 1
|
42
|
+
and (isinstance(actions[0], exp.Cluster))
|
43
|
+
):
|
44
|
+
return SUCCESS_NOP
|
45
|
+
return expression
|
46
|
+
|
47
|
+
|
36
48
|
def array_size(expression: exp.Expression) -> exp.Expression:
|
37
49
|
if isinstance(expression, exp.ArraySize):
|
38
50
|
# case is used to convert 0 to null, because null is returned by duckdb when no case matches
|
@@ -551,12 +563,13 @@ def information_schema_fs_columns_snowflake(expression: exp.Expression) -> exp.E
|
|
551
563
|
"""
|
552
564
|
|
553
565
|
if (
|
554
|
-
isinstance(expression, exp.
|
555
|
-
and
|
556
|
-
and
|
557
|
-
and
|
566
|
+
isinstance(expression, exp.Table)
|
567
|
+
and expression.db
|
568
|
+
and expression.db.upper() == "INFORMATION_SCHEMA"
|
569
|
+
and expression.name
|
570
|
+
and expression.name.upper() == "COLUMNS"
|
558
571
|
):
|
559
|
-
|
572
|
+
expression.set("this", exp.Identifier(this="_FS_COLUMNS_SNOWFLAKE", quoted=False))
|
560
573
|
|
561
574
|
return expression
|
562
575
|
|
fakesnow/variables.py
CHANGED
@@ -5,10 +5,23 @@ from sqlglot import exp
|
|
5
5
|
|
6
6
|
|
7
7
|
# Implements snowflake variables: https://docs.snowflake.com/en/sql-reference/session-variables#using-variables-in-sql
|
8
|
+
# [ ] Add support for setting multiple variables in a single statement
|
8
9
|
class Variables:
|
9
10
|
@classmethod
|
10
11
|
def is_variable_modifier(cls, expr: exp.Expression) -> bool:
|
11
|
-
return
|
12
|
+
return cls._is_set_expression(expr) or cls._is_unset_expression(expr)
|
13
|
+
|
14
|
+
@classmethod
|
15
|
+
def _is_set_expression(cls, expr: exp.Expression) -> bool:
|
16
|
+
if isinstance(expr, exp.Set):
|
17
|
+
is_set = not expr.args.get("unset")
|
18
|
+
if is_set: # SET varname = value;
|
19
|
+
set_expressions = expr.args.get("expressions")
|
20
|
+
assert set_expressions, "SET without values in expression(s) is unexpected."
|
21
|
+
# Avoids mistakenly setting variables for statements that use SET in a different context.
|
22
|
+
# (eg. WHEN MATCHED THEN UPDATE SET x=7)
|
23
|
+
return isinstance(set_expressions[0], exp.SetItem)
|
24
|
+
return False
|
12
25
|
|
13
26
|
@classmethod
|
14
27
|
def _is_unset_expression(cls, expr: exp.Expression) -> bool:
|
@@ -22,11 +35,11 @@ class Variables:
|
|
22
35
|
|
23
36
|
def update_variables(self, expr: exp.Expression) -> None:
|
24
37
|
if isinstance(expr, exp.Set):
|
25
|
-
|
26
|
-
if
|
27
|
-
|
28
|
-
assert
|
29
|
-
eq =
|
38
|
+
is_set = not expr.args.get("unset")
|
39
|
+
if is_set: # SET varname = value;
|
40
|
+
set_expressions = expr.args.get("expressions")
|
41
|
+
assert set_expressions, "SET without values in expression(s) is unexpected."
|
42
|
+
eq = set_expressions[0].this
|
30
43
|
name = eq.this.sql()
|
31
44
|
value = eq.args.get("expression").sql()
|
32
45
|
self._set(name, value)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: fakesnow
|
3
|
-
Version: 0.9.
|
3
|
+
Version: 0.9.21
|
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
|
@@ -213,20 +213,25 @@ License-File: LICENSE
|
|
213
213
|
Requires-Dist: duckdb ~=1.0.0
|
214
214
|
Requires-Dist: pyarrow
|
215
215
|
Requires-Dist: snowflake-connector-python
|
216
|
-
Requires-Dist: sqlglot ~=25.
|
216
|
+
Requires-Dist: sqlglot ~=25.5.1
|
217
217
|
Provides-Extra: dev
|
218
218
|
Requires-Dist: build ~=1.0 ; extra == 'dev'
|
219
219
|
Requires-Dist: pandas-stubs ; extra == 'dev'
|
220
220
|
Requires-Dist: snowflake-connector-python[pandas,secure-local-storage] ; extra == 'dev'
|
221
221
|
Requires-Dist: pre-commit ~=3.4 ; extra == 'dev'
|
222
|
+
Requires-Dist: pyarrow-stubs ; extra == 'dev'
|
222
223
|
Requires-Dist: pytest ~=8.0 ; extra == 'dev'
|
223
|
-
Requires-Dist:
|
224
|
+
Requires-Dist: pytest-asyncio ; extra == 'dev'
|
225
|
+
Requires-Dist: ruff ~=0.5.1 ; extra == 'dev'
|
224
226
|
Requires-Dist: twine ~=5.0 ; extra == 'dev'
|
225
227
|
Requires-Dist: snowflake-sqlalchemy ~=1.5.0 ; extra == 'dev'
|
226
228
|
Provides-Extra: notebook
|
227
229
|
Requires-Dist: duckdb-engine ; extra == 'notebook'
|
228
230
|
Requires-Dist: ipykernel ; extra == 'notebook'
|
229
231
|
Requires-Dist: jupysql ; extra == 'notebook'
|
232
|
+
Provides-Extra: server
|
233
|
+
Requires-Dist: starlette ; extra == 'server'
|
234
|
+
Requires-Dist: uvicorn ; extra == 'server'
|
230
235
|
|
231
236
|
# fakesnow ❄️
|
232
237
|
|
@@ -0,0 +1,21 @@
|
|
1
|
+
fakesnow/__init__.py,sha256=9tFJJKvowKNW3vfnlmza6hOLN1I52DwChgNc5Ew6CcA,3499
|
2
|
+
fakesnow/__main__.py,sha256=GDrGyNTvBFuqn_UfDjKs7b3LPtU6gDv1KwosVDrukIM,76
|
3
|
+
fakesnow/arrow.py,sha256=1ypCsf-r2Ven6CuSm-bTLoeq1G31kBD6JnaLvDxpwhU,1218
|
4
|
+
fakesnow/checks.py,sha256=-QMvdcrRbhN60rnzxLBJ0IkUBWyLR8gGGKKmCS0w9mA,2383
|
5
|
+
fakesnow/cli.py,sha256=9qfI-Ssr6mo8UmIlXkUAOz2z2YPBgDsrEVaZv9FjGFs,2201
|
6
|
+
fakesnow/expr.py,sha256=CAxuYIUkwI339DQIBzvFF0F-m1tcVGKEPA5rDTzmH9A,892
|
7
|
+
fakesnow/fakes.py,sha256=wLSjKrNI8wxe3MuUAa97jpUHd5vZTzvrlF1-Hf0FC0M,31208
|
8
|
+
fakesnow/fixtures.py,sha256=G-NkVeruSQAJ7fvSS2fR2oysUn0Yra1pohHlOvacKEk,455
|
9
|
+
fakesnow/info_schema.py,sha256=LjS_-8YXBtCSvkdU5uL0aJdFcZEsBa6o5zf-Q_aV9i0,6302
|
10
|
+
fakesnow/instance.py,sha256=3cJvPRuFy19dMKXbtBLl6imzO48pEw8uTYhZyFDuwhk,3133
|
11
|
+
fakesnow/macros.py,sha256=pX1YJDnQOkFJSHYUjQ6ErEkYIKvFI6Ncz_au0vv1csA,265
|
12
|
+
fakesnow/py.typed,sha256=B-DLSjYBi7pkKjwxCSdpVj2J02wgfJr-E7B1wOUyxYU,80
|
13
|
+
fakesnow/server.py,sha256=HwCAZ5AhU4nRbFGIqDBs2rdwoK70dYZDyw1XfE1cHqU,3082
|
14
|
+
fakesnow/transforms.py,sha256=GCpczoFdvnffQMvSG59PNiiTVTld9kROnnR5dWNXQvY,53624
|
15
|
+
fakesnow/variables.py,sha256=WXyPnkeNwD08gy52yF66CVe2twiYC50tztNfgXV4q1k,3032
|
16
|
+
fakesnow-0.9.21.dist-info/LICENSE,sha256=kW-7NWIyaRMQiDpryfSmF2DObDZHGR1cJZ39s6B1Svg,11344
|
17
|
+
fakesnow-0.9.21.dist-info/METADATA,sha256=6EzLZbGBTS0VEYCHdG4sNNZT-w5SiespKcitL_XEtM4,18043
|
18
|
+
fakesnow-0.9.21.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
19
|
+
fakesnow-0.9.21.dist-info/entry_points.txt,sha256=2riAUgu928ZIHawtO8EsfrMEJhi-EH-z_Vq7Q44xKPM,47
|
20
|
+
fakesnow-0.9.21.dist-info/top_level.txt,sha256=500evXI1IFX9so82cizGIEMHAb_dJNPaZvd2H9dcKTA,24
|
21
|
+
fakesnow-0.9.21.dist-info/RECORD,,
|
fakesnow/global_database.py
DELETED
@@ -1,46 +0,0 @@
|
|
1
|
-
from duckdb import DuckDBPyConnection
|
2
|
-
|
3
|
-
GLOBAL_DATABASE_NAME = "_fs_global"
|
4
|
-
USERS_TABLE_FQ_NAME = f"{GLOBAL_DATABASE_NAME}._fs_users_ext"
|
5
|
-
|
6
|
-
# replicates the output structure of https://docs.snowflake.com/en/sql-reference/sql/show-users
|
7
|
-
SQL_CREATE_INFORMATION_SCHEMA_USERS_TABLE_EXT = f"""
|
8
|
-
create table if not exists {USERS_TABLE_FQ_NAME} (
|
9
|
-
name varchar,
|
10
|
-
created_on TIMESTAMPTZ,
|
11
|
-
login_name varchar,
|
12
|
-
display_name varchar,
|
13
|
-
first_name varchar,
|
14
|
-
last_name varchar,
|
15
|
-
email varchar,
|
16
|
-
mins_to_unlock varchar,
|
17
|
-
days_to_expiry varchar,
|
18
|
-
comment varchar,
|
19
|
-
disabled varchar,
|
20
|
-
must_change_password varchar,
|
21
|
-
snowflake_lock varchar,
|
22
|
-
default_warehouse varchar,
|
23
|
-
default_namespace varchar,
|
24
|
-
default_role varchar,
|
25
|
-
default_secondary_roles varchar,
|
26
|
-
ext_authn_duo varchar,
|
27
|
-
ext_authn_uid varchar,
|
28
|
-
mins_to_bypass_mfa varchar,
|
29
|
-
owner varchar,
|
30
|
-
last_success_login TIMESTAMPTZ,
|
31
|
-
expires_at_time TIMESTAMPTZ,
|
32
|
-
locked_until_time TIMESTAMPTZ,
|
33
|
-
has_password varchar,
|
34
|
-
has_rsa_public_key varchar,
|
35
|
-
)
|
36
|
-
"""
|
37
|
-
|
38
|
-
|
39
|
-
def create_global_database(conn: DuckDBPyConnection) -> None:
|
40
|
-
"""Create a "global" database for storing objects which span database.
|
41
|
-
|
42
|
-
Including (but not limited to):
|
43
|
-
- Users
|
44
|
-
"""
|
45
|
-
conn.execute(f"ATTACH IF NOT EXISTS ':memory:' AS {GLOBAL_DATABASE_NAME}")
|
46
|
-
conn.execute(SQL_CREATE_INFORMATION_SCHEMA_USERS_TABLE_EXT)
|
fakesnow-0.9.20.dist-info/RECORD
DELETED
@@ -1,19 +0,0 @@
|
|
1
|
-
fakesnow/__init__.py,sha256=gEo6Jg6f8tJUwRTsrfb9q-FpDOufelf8Y3XeWtEJ9wg,3897
|
2
|
-
fakesnow/__main__.py,sha256=GDrGyNTvBFuqn_UfDjKs7b3LPtU6gDv1KwosVDrukIM,76
|
3
|
-
fakesnow/checks.py,sha256=-QMvdcrRbhN60rnzxLBJ0IkUBWyLR8gGGKKmCS0w9mA,2383
|
4
|
-
fakesnow/cli.py,sha256=9qfI-Ssr6mo8UmIlXkUAOz2z2YPBgDsrEVaZv9FjGFs,2201
|
5
|
-
fakesnow/expr.py,sha256=CAxuYIUkwI339DQIBzvFF0F-m1tcVGKEPA5rDTzmH9A,892
|
6
|
-
fakesnow/fakes.py,sha256=MYzj3XqVokPbOdOcU4WFep1CRsuXlBIwaz9EWaNCGo4,30778
|
7
|
-
fakesnow/fixtures.py,sha256=G-NkVeruSQAJ7fvSS2fR2oysUn0Yra1pohHlOvacKEk,455
|
8
|
-
fakesnow/global_database.py,sha256=WTVIP1VhNvdCeX7TQncX1TRpGQU5rBf5Pbxim40zeSU,1399
|
9
|
-
fakesnow/info_schema.py,sha256=CdIcGXHEQ_kmEAzdQKvA-PX41LA6wlK-4p1J45qgKYA,6266
|
10
|
-
fakesnow/macros.py,sha256=pX1YJDnQOkFJSHYUjQ6ErEkYIKvFI6Ncz_au0vv1csA,265
|
11
|
-
fakesnow/py.typed,sha256=B-DLSjYBi7pkKjwxCSdpVj2J02wgfJr-E7B1wOUyxYU,80
|
12
|
-
fakesnow/transforms.py,sha256=7XS42Ehv3SRciQLpAL4swCFCkNpOTwarXEIX6jUzO4Y,53230
|
13
|
-
fakesnow/variables.py,sha256=iE8fnyaMnIoWh_ftT2qUq39fCNglwtQoGbC7MkTqHGU,2341
|
14
|
-
fakesnow-0.9.20.dist-info/LICENSE,sha256=kW-7NWIyaRMQiDpryfSmF2DObDZHGR1cJZ39s6B1Svg,11344
|
15
|
-
fakesnow-0.9.20.dist-info/METADATA,sha256=RSk43Zbbb8LQsG3G03DwUO8nVbaxs5qNd5VKTHqg4Xw,17839
|
16
|
-
fakesnow-0.9.20.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
17
|
-
fakesnow-0.9.20.dist-info/entry_points.txt,sha256=2riAUgu928ZIHawtO8EsfrMEJhi-EH-z_Vq7Q44xKPM,47
|
18
|
-
fakesnow-0.9.20.dist-info/top_level.txt,sha256=500evXI1IFX9so82cizGIEMHAb_dJNPaZvd2H9dcKTA,24
|
19
|
-
fakesnow-0.9.20.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|