fakesnow 0.9.0__tar.gz → 0.9.2__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 (31) hide show
  1. {fakesnow-0.9.0/fakesnow.egg-info → fakesnow-0.9.2}/PKG-INFO +4 -3
  2. {fakesnow-0.9.0 → fakesnow-0.9.2}/README.md +1 -0
  3. {fakesnow-0.9.0 → fakesnow-0.9.2}/fakesnow/fakes.py +10 -0
  4. fakesnow-0.9.2/fakesnow/global_database.py +46 -0
  5. {fakesnow-0.9.0 → fakesnow-0.9.2}/fakesnow/info_schema.py +2 -1
  6. {fakesnow-0.9.0 → fakesnow-0.9.2}/fakesnow/transforms.py +39 -15
  7. {fakesnow-0.9.0 → fakesnow-0.9.2/fakesnow.egg-info}/PKG-INFO +4 -3
  8. {fakesnow-0.9.0 → fakesnow-0.9.2}/fakesnow.egg-info/SOURCES.txt +3 -1
  9. {fakesnow-0.9.0 → fakesnow-0.9.2}/fakesnow.egg-info/requires.txt +2 -2
  10. {fakesnow-0.9.0 → fakesnow-0.9.2}/pyproject.toml +3 -3
  11. {fakesnow-0.9.0 → fakesnow-0.9.2}/tests/test_fakes.py +13 -0
  12. {fakesnow-0.9.0 → fakesnow-0.9.2}/tests/test_transforms.py +1 -1
  13. fakesnow-0.9.2/tests/test_users.py +19 -0
  14. {fakesnow-0.9.0 → fakesnow-0.9.2}/LICENSE +0 -0
  15. {fakesnow-0.9.0 → fakesnow-0.9.2}/MANIFEST.in +0 -0
  16. {fakesnow-0.9.0 → fakesnow-0.9.2}/fakesnow/__init__.py +0 -0
  17. {fakesnow-0.9.0 → fakesnow-0.9.2}/fakesnow/__main__.py +0 -0
  18. {fakesnow-0.9.0 → fakesnow-0.9.2}/fakesnow/checks.py +0 -0
  19. {fakesnow-0.9.0 → fakesnow-0.9.2}/fakesnow/cli.py +0 -0
  20. {fakesnow-0.9.0 → fakesnow-0.9.2}/fakesnow/expr.py +0 -0
  21. {fakesnow-0.9.0 → fakesnow-0.9.2}/fakesnow/fixtures.py +0 -0
  22. {fakesnow-0.9.0 → fakesnow-0.9.2}/fakesnow/macros.py +0 -0
  23. {fakesnow-0.9.0 → fakesnow-0.9.2}/fakesnow/py.typed +0 -0
  24. {fakesnow-0.9.0 → fakesnow-0.9.2}/fakesnow.egg-info/dependency_links.txt +0 -0
  25. {fakesnow-0.9.0 → fakesnow-0.9.2}/fakesnow.egg-info/entry_points.txt +0 -0
  26. {fakesnow-0.9.0 → fakesnow-0.9.2}/fakesnow.egg-info/top_level.txt +0 -0
  27. {fakesnow-0.9.0 → fakesnow-0.9.2}/setup.cfg +0 -0
  28. {fakesnow-0.9.0 → fakesnow-0.9.2}/tests/test_checks.py +0 -0
  29. {fakesnow-0.9.0 → fakesnow-0.9.2}/tests/test_cli.py +0 -0
  30. {fakesnow-0.9.0 → fakesnow-0.9.2}/tests/test_expr.py +0 -0
  31. {fakesnow-0.9.0 → fakesnow-0.9.2}/tests/test_patch.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fakesnow
3
- Version: 0.9.0
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.9.2
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.1
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
 
@@ -119,6 +119,7 @@ Partial support
119
119
  - [x] regex functions
120
120
  - [x] semi-structured data
121
121
  - [x] tags
122
+ - [x] user management (See [tests/test_users.py](tests/test_users.py))
122
123
 
123
124
  For more detail see [tests/test_fakes.py](tests/test_fakes.py)
124
125
 
@@ -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)
@@ -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') and schema_name = 'information_schema'
77
+ where catalog_name not in ('memory', 'system', 'temp', '_fs_global')
78
+ and schema_name = 'information_schema'
78
79
  """
79
80
  )
80
81
 
@@ -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.Anonymous(
275
- # duckdb unnests in reserve, so we reverse the list to match
276
- # the order of the original array (and snowflake)
277
- this="list_reverse",
278
- expressions=[
279
- exp.Cast(
280
- this=explode_expression,
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.0
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.9.2
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.1
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
 
@@ -9,6 +9,7 @@ fakesnow/cli.py
9
9
  fakesnow/expr.py
10
10
  fakesnow/fakes.py
11
11
  fakesnow/fixtures.py
12
+ fakesnow/global_database.py
12
13
  fakesnow/info_schema.py
13
14
  fakesnow/macros.py
14
15
  fakesnow/py.typed
@@ -24,4 +25,5 @@ tests/test_cli.py
24
25
  tests/test_expr.py
25
26
  tests/test_fakes.py
26
27
  tests/test_patch.py
27
- tests/test_transforms.py
28
+ tests/test_transforms.py
29
+ tests/test_users.py
@@ -1,7 +1,7 @@
1
- duckdb~=0.9.2
1
+ duckdb~=0.10.0
2
2
  pyarrow
3
3
  snowflake-connector-python
4
- sqlglot~=21.0.1
4
+ sqlglot~=21.1.0
5
5
 
6
6
  [dev]
7
7
  black~=23.9
@@ -1,17 +1,17 @@
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.0"
4
+ version = "0.9.2"
5
5
  readme = "README.md"
6
6
  license = { file = "LICENSE" }
7
7
  classifiers = ["License :: OSI Approved :: MIT License"]
8
8
  keywords = ["snowflake", "snowflakedb", "fake", "local", "mock", "testing"]
9
9
  requires-python = ">=3.9"
10
10
  dependencies = [
11
- "duckdb~=0.9.2",
11
+ "duckdb~=0.10.0",
12
12
  "pyarrow",
13
13
  "snowflake-connector-python",
14
- "sqlglot~=21.0.1",
14
+ "sqlglot~=21.1.0",
15
15
  ]
16
16
 
17
17
  [project.urls]
@@ -459,6 +459,19 @@ def test_description_update(dcur: snowflake.connector.cursor.DictCursor):
459
459
  # fmt: on
460
460
 
461
461
 
462
+ def test_description_delete(dcur: snowflake.connector.cursor.DictCursor):
463
+ dcur.execute("create table example (x int)")
464
+ dcur.execute("insert into example values (1), (2), (3)")
465
+ dcur.execute("delete from example where x>1")
466
+ assert dcur.fetchall() == [{"number of rows deleted": 2}]
467
+ # TODO: Snowflake is actually precision=19, is_nullable=False
468
+ # fmt: off
469
+ assert dcur.description == [
470
+ ResultMetadata(name='number of rows deleted', type_code=0, display_size=None, internal_size=None, precision=38, scale=0, is_nullable=True),
471
+ ]
472
+ # fmt: on
473
+
474
+
462
475
  def test_equal_null(cur: snowflake.connector.cursor.SnowflakeCursor):
463
476
  cur.execute("select equal_null(NULL, NULL), equal_null(1, 1), equal_null(1, 2), equal_null(1, NULL)")
464
477
  assert cur.fetchall() == [(True, True, False, False)]
@@ -140,7 +140,7 @@ def test_flatten() -> None:
140
140
  )
141
141
  .transform(flatten)
142
142
  .sql(dialect="duckdb")
143
- == """SELECT t.id, flat.value -> '$.fruit' FROM (SELECT 1, JSON('[{"fruit":"banana"}]') UNION SELECT 2, JSON('[{"fruit":"coconut"}, {"fruit":"durian"}]')) AS t(id, fruits), LATERAL UNNEST(LIST_REVERSE(CAST(t.fruits AS JSON[]))) AS flat(VALUE)""" # noqa: E501
143
+ == """SELECT t.id, flat.value -> '$.fruit' FROM (SELECT 1, JSON('[{"fruit":"banana"}]') UNION SELECT 2, JSON('[{"fruit":"coconut"}, {"fruit":"durian"}]')) AS t(id, fruits), LATERAL UNNEST(CAST(t.fruits AS JSON[])) AS flat(VALUE)""" # noqa: E501
144
144
  )
145
145
 
146
146
 
@@ -0,0 +1,19 @@
1
+ import snowflake.connector.cursor
2
+
3
+
4
+ def test_show_users_base_case(cur: snowflake.connector.cursor.SnowflakeCursor):
5
+ result = cur.execute("SHOW USERS")
6
+ assert result
7
+ assert result.fetchall() == []
8
+
9
+
10
+ def test_show_users_with_users(cur: snowflake.connector.cursor.SnowflakeCursor):
11
+ result = cur.execute("CREATE USER foo")
12
+ result = cur.execute("CREATE USER bar")
13
+
14
+ result = cur.execute("SHOW USERS")
15
+ assert result
16
+
17
+ rows = result.fetchall()
18
+ names = [row[0] for row in rows]
19
+ assert names == ["foo", "bar"]
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