fakesnow 0.9.0__py3-none-any.whl → 0.9.2__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/fakes.py +10 -0
- fakesnow/global_database.py +46 -0
- fakesnow/info_schema.py +2 -1
- fakesnow/transforms.py +39 -15
- {fakesnow-0.9.0.dist-info → fakesnow-0.9.2.dist-info}/METADATA +4 -3
- {fakesnow-0.9.0.dist-info → fakesnow-0.9.2.dist-info}/RECORD +10 -9
- {fakesnow-0.9.0.dist-info → fakesnow-0.9.2.dist-info}/LICENSE +0 -0
- {fakesnow-0.9.0.dist-info → fakesnow-0.9.2.dist-info}/WHEEL +0 -0
- {fakesnow-0.9.0.dist-info → fakesnow-0.9.2.dist-info}/entry_points.txt +0 -0
- {fakesnow-0.9.0.dist-info → fakesnow-0.9.2.dist-info}/top_level.txt +0 -0
fakesnow/fakes.py
CHANGED
@@ -30,6 +30,7 @@ import fakesnow.expr as expr
|
|
30
30
|
import fakesnow.info_schema as info_schema
|
31
31
|
import fakesnow.macros as macros
|
32
32
|
import fakesnow.transforms as transforms
|
33
|
+
from fakesnow.global_database import create_global_database
|
33
34
|
|
34
35
|
SCHEMA_UNSET = "schema_unset"
|
35
36
|
SQL_SUCCESS = "SELECT 'Statement executed successfully.' as 'status'"
|
@@ -39,6 +40,7 @@ SQL_CREATED_TABLE = Template("SELECT 'Table ${name} successfully created.' as 's
|
|
39
40
|
SQL_DROPPED = Template("SELECT '${name} successfully dropped.' as 'status'")
|
40
41
|
SQL_INSERTED_ROWS = Template("SELECT ${count} as 'number of rows inserted'")
|
41
42
|
SQL_UPDATED_ROWS = Template("SELECT ${count} as 'number of rows updated', 0 as 'number of multi-joined rows updated'")
|
43
|
+
SQL_DELETED_ROWS = Template("SELECT ${count} as 'number of rows deleted'")
|
42
44
|
|
43
45
|
|
44
46
|
class FakeSnowflakeCursor:
|
@@ -194,6 +196,8 @@ class FakeSnowflakeCursor:
|
|
194
196
|
.transform(transforms.identifier)
|
195
197
|
.transform(lambda e: transforms.show_schemas(e, self._conn.database))
|
196
198
|
.transform(lambda e: transforms.show_objects_tables(e, self._conn.database))
|
199
|
+
.transform(transforms.show_users)
|
200
|
+
.transform(transforms.create_user)
|
197
201
|
)
|
198
202
|
sql = transformed.sql(dialect="duckdb")
|
199
203
|
result_sql = None
|
@@ -265,6 +269,10 @@ class FakeSnowflakeCursor:
|
|
265
269
|
(affected_count,) = self._duck_conn.fetchall()[0]
|
266
270
|
result_sql = SQL_UPDATED_ROWS.substitute(count=affected_count)
|
267
271
|
|
272
|
+
elif cmd == "DELETE":
|
273
|
+
(affected_count,) = self._duck_conn.fetchall()[0]
|
274
|
+
result_sql = SQL_DELETED_ROWS.substitute(count=affected_count)
|
275
|
+
|
268
276
|
elif cmd == "DESCRIBE TABLE":
|
269
277
|
# DESCRIBE TABLE has already been run above to detect and error if the table exists
|
270
278
|
# We now rerun DESCRIBE TABLE but transformed with columns to match Snowflake
|
@@ -480,6 +488,8 @@ class FakeSnowflakeConnection:
|
|
480
488
|
self.db_path = db_path
|
481
489
|
self._paramstyle = "pyformat"
|
482
490
|
|
491
|
+
create_global_database(duck_conn)
|
492
|
+
|
483
493
|
# create database if needed
|
484
494
|
if (
|
485
495
|
create_database
|
@@ -0,0 +1,46 @@
|
|
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/info_schema.py
CHANGED
@@ -74,7 +74,8 @@ select
|
|
74
74
|
1 as retention_time,
|
75
75
|
'STANDARD' as type
|
76
76
|
from information_schema.schemata
|
77
|
-
where catalog_name not in ('memory', 'system', 'temp'
|
77
|
+
where catalog_name not in ('memory', 'system', 'temp', '_fs_global')
|
78
|
+
and schema_name = 'information_schema'
|
78
79
|
"""
|
79
80
|
)
|
80
81
|
|
fakesnow/transforms.py
CHANGED
@@ -7,6 +7,8 @@ from typing import cast
|
|
7
7
|
import sqlglot
|
8
8
|
from sqlglot import exp
|
9
9
|
|
10
|
+
from fakesnow.global_database import USERS_TABLE_FQ_NAME
|
11
|
+
|
10
12
|
MISSING_DATABASE = "missing_database"
|
11
13
|
SUCCESS_NOP = sqlglot.parse_one("SELECT 'Statement executed successfully.'")
|
12
14
|
|
@@ -271,22 +273,15 @@ def flatten(expression: exp.Expression) -> exp.Expression:
|
|
271
273
|
return exp.Lateral(
|
272
274
|
this=exp.Unnest(
|
273
275
|
expressions=[
|
274
|
-
exp.
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
to=exp.DataType(
|
282
|
-
this=exp.DataType.Type.ARRAY,
|
283
|
-
expressions=[exp.DataType(this=exp.DataType.Type.JSON, nested=False, prefix=False)],
|
284
|
-
nested=True,
|
285
|
-
),
|
286
|
-
)
|
287
|
-
],
|
276
|
+
exp.Cast(
|
277
|
+
this=explode_expression,
|
278
|
+
to=exp.DataType(
|
279
|
+
this=exp.DataType.Type.ARRAY,
|
280
|
+
expressions=[exp.DataType(this=exp.DataType.Type.JSON, nested=False, prefix=False)],
|
281
|
+
nested=True,
|
282
|
+
),
|
288
283
|
)
|
289
|
-
]
|
284
|
+
],
|
290
285
|
),
|
291
286
|
alias=exp.TableAlias(this=alias.this, columns=[exp.Identifier(this="VALUE", quoted=False)]),
|
292
287
|
)
|
@@ -963,3 +958,32 @@ def values_columns(expression: exp.Expression) -> exp.Expression:
|
|
963
958
|
expression.set("alias", exp.TableAlias(this=exp.Identifier(this="_", quoted=False), columns=columns))
|
964
959
|
|
965
960
|
return expression
|
961
|
+
|
962
|
+
|
963
|
+
def show_users(expression: exp.Expression) -> exp.Expression:
|
964
|
+
"""Transform SHOW USERS to a query against the global database's information_schema._fs_users table.
|
965
|
+
|
966
|
+
https://docs.snowflake.com/en/sql-reference/sql/show-users
|
967
|
+
"""
|
968
|
+
if isinstance(expression, exp.Show) and isinstance(expression.this, str) and expression.this.upper() == "USERS":
|
969
|
+
return sqlglot.parse_one(f"SELECT * FROM {USERS_TABLE_FQ_NAME}", read="duckdb")
|
970
|
+
|
971
|
+
return expression
|
972
|
+
|
973
|
+
|
974
|
+
def create_user(expression: exp.Expression) -> exp.Expression:
|
975
|
+
"""Transform CREATE USER to a query against the global database's information_schema._fs_users table.
|
976
|
+
|
977
|
+
https://docs.snowflake.com/en/sql-reference/sql/create-user
|
978
|
+
"""
|
979
|
+
# XXX: this is a placeholder. We need to implement the full CREATE USER syntax, but
|
980
|
+
# sqlglot doesnt yet support Create for snowflake.
|
981
|
+
if isinstance(expression, exp.Command) and expression.this == "CREATE":
|
982
|
+
sub_exp = expression.expression.strip()
|
983
|
+
if sub_exp.upper().startswith("USER"):
|
984
|
+
_, name, *ignored = sub_exp.split(" ")
|
985
|
+
if ignored:
|
986
|
+
raise NotImplementedError(f"`CREATE USER` with {ignored} not yet supported")
|
987
|
+
return sqlglot.parse_one(f"INSERT INTO {USERS_TABLE_FQ_NAME} (name) VALUES ('{name}')", read="duckdb")
|
988
|
+
|
989
|
+
return expression
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: fakesnow
|
3
|
-
Version: 0.9.
|
3
|
+
Version: 0.9.2
|
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.
|
213
|
+
Requires-Dist: duckdb ~=0.10.0
|
214
214
|
Requires-Dist: pyarrow
|
215
215
|
Requires-Dist: snowflake-connector-python
|
216
|
-
Requires-Dist: sqlglot ~=21.0
|
216
|
+
Requires-Dist: sqlglot ~=21.1.0
|
217
217
|
Provides-Extra: dev
|
218
218
|
Requires-Dist: black ~=23.9 ; extra == 'dev'
|
219
219
|
Requires-Dist: build ~=1.0 ; extra == 'dev'
|
@@ -349,6 +349,7 @@ Partial support
|
|
349
349
|
- [x] regex functions
|
350
350
|
- [x] semi-structured data
|
351
351
|
- [x] tags
|
352
|
+
- [x] user management (See [tests/test_users.py](tests/test_users.py))
|
352
353
|
|
353
354
|
For more detail see [tests/test_fakes.py](tests/test_fakes.py)
|
354
355
|
|
@@ -3,15 +3,16 @@ fakesnow/__main__.py,sha256=GDrGyNTvBFuqn_UfDjKs7b3LPtU6gDv1KwosVDrukIM,76
|
|
3
3
|
fakesnow/checks.py,sha256=-QMvdcrRbhN60rnzxLBJ0IkUBWyLR8gGGKKmCS0w9mA,2383
|
4
4
|
fakesnow/cli.py,sha256=9qfI-Ssr6mo8UmIlXkUAOz2z2YPBgDsrEVaZv9FjGFs,2201
|
5
5
|
fakesnow/expr.py,sha256=CAxuYIUkwI339DQIBzvFF0F-m1tcVGKEPA5rDTzmH9A,892
|
6
|
-
fakesnow/fakes.py,sha256=
|
6
|
+
fakesnow/fakes.py,sha256=iF9IN7nG74DNMDNTZ9CBUGqsYsqicZ7PgRNe4B_zAFs,27931
|
7
7
|
fakesnow/fixtures.py,sha256=G-NkVeruSQAJ7fvSS2fR2oysUn0Yra1pohHlOvacKEk,455
|
8
|
-
fakesnow/
|
8
|
+
fakesnow/global_database.py,sha256=WTVIP1VhNvdCeX7TQncX1TRpGQU5rBf5Pbxim40zeSU,1399
|
9
|
+
fakesnow/info_schema.py,sha256=eiKEVBbaSGfsuPD8USuhHF9BDC8pU9sPiDHgy0orTN8,4701
|
9
10
|
fakesnow/macros.py,sha256=pX1YJDnQOkFJSHYUjQ6ErEkYIKvFI6Ncz_au0vv1csA,265
|
10
11
|
fakesnow/py.typed,sha256=B-DLSjYBi7pkKjwxCSdpVj2J02wgfJr-E7B1wOUyxYU,80
|
11
|
-
fakesnow/transforms.py,sha256=
|
12
|
-
fakesnow-0.9.
|
13
|
-
fakesnow-0.9.
|
14
|
-
fakesnow-0.9.
|
15
|
-
fakesnow-0.9.
|
16
|
-
fakesnow-0.9.
|
17
|
-
fakesnow-0.9.
|
12
|
+
fakesnow/transforms.py,sha256=z9tZDOtvQDAoYmQDvDegUOwxgBFvjnSfKCo7kTc-ryA,36300
|
13
|
+
fakesnow-0.9.2.dist-info/LICENSE,sha256=kW-7NWIyaRMQiDpryfSmF2DObDZHGR1cJZ39s6B1Svg,11344
|
14
|
+
fakesnow-0.9.2.dist-info/METADATA,sha256=7rSjgiC1nVjW_38DFx5I6OfyFz71f_TbjapFAIZ-kkI,17724
|
15
|
+
fakesnow-0.9.2.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
|
16
|
+
fakesnow-0.9.2.dist-info/entry_points.txt,sha256=2riAUgu928ZIHawtO8EsfrMEJhi-EH-z_Vq7Q44xKPM,47
|
17
|
+
fakesnow-0.9.2.dist-info/top_level.txt,sha256=500evXI1IFX9so82cizGIEMHAb_dJNPaZvd2H9dcKTA,24
|
18
|
+
fakesnow-0.9.2.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|