|
5 | 5 | import logging |
6 | 6 | import stat |
7 | 7 | import sys |
| 8 | +import uuid |
8 | 9 | from pathlib import Path |
9 | 10 | from secrets import token_urlsafe |
10 | 11 | from textwrap import dedent |
@@ -1021,3 +1022,76 @@ def test_server_session_keep_alive_false_calls_async_check(mock_post_requests): |
1021 | 1022 |
|
1022 | 1023 | # Verify delete_session WAS called (since async queries are finished and keep_alive=False) |
1023 | 1024 | delete_session_mock.assert_called_once() |
| 1025 | + |
| 1026 | + |
| 1027 | +def _run_cmd_query_with_final_names( |
| 1028 | + conn: SnowflakeConnection, sql: str, final_db: str | None, final_schema: str | None |
| 1029 | +) -> None: |
| 1030 | + """Drive ``cmd_query`` with a mocked server response carrying |
| 1031 | + finalDatabaseName / finalSchemaName so the cache-update guard runs against a |
| 1032 | + controlled response. Caches are mutated in place per the guard logic.""" |
| 1033 | + conn._rest.request = mock.MagicMock( |
| 1034 | + return_value={ |
| 1035 | + "success": True, |
| 1036 | + "data": { |
| 1037 | + "finalDatabaseName": final_db, |
| 1038 | + "finalSchemaName": final_schema, |
| 1039 | + }, |
| 1040 | + } |
| 1041 | + ) |
| 1042 | + # _no_results=True skips query-context plumbing; the cache-update logic under |
| 1043 | + # test runs regardless of that flag. |
| 1044 | + conn.cmd_query(sql, 0, uuid.uuid4(), _no_results=True) |
| 1045 | + |
| 1046 | + |
| 1047 | +@pytest.mark.skipolddriver |
| 1048 | +def test_fqn_ddl_does_not_pollute_none_database_cache(mock_post_requests): |
| 1049 | + """SNOW-3665226: when the session has no current database (e.g. the user has |
| 1050 | + no default namespace), a fully-qualified DDL's finalDatabaseName / |
| 1051 | + finalSchemaName reflect the *referenced* object, not the session context, and |
| 1052 | + must NOT overwrite the connector's ``None`` cache. |
| 1053 | +
|
| 1054 | + This is the mocked counterpart to the integ test: the integ test exercises |
| 1055 | + the schema side on real accounts (which have no default schema), while this |
| 1056 | + unit test locks in the database side, which can't be driven end-to-end on |
| 1057 | + accounts that carry a default database. |
| 1058 | + """ |
| 1059 | + conn = fake_connector() |
| 1060 | + try: |
| 1061 | + # Simulate a session with no current namespace at all. |
| 1062 | + conn._database = None |
| 1063 | + conn._schema = None |
| 1064 | + |
| 1065 | + # A fully-qualified DDL returns the referenced object's namespace in |
| 1066 | + # final*Name — this must be ignored while the cache is None. |
| 1067 | + _run_cmd_query_with_final_names( |
| 1068 | + conn, |
| 1069 | + 'CREATE OR REPLACE VIEW "OTHER_DB"."OTHER_SCHEMA"."V" AS SELECT 1', |
| 1070 | + final_db="OTHER_DB", |
| 1071 | + final_schema="OTHER_SCHEMA", |
| 1072 | + ) |
| 1073 | + assert conn._database is None |
| 1074 | + assert conn._schema is None |
| 1075 | + |
| 1076 | + # A genuine context switch (USE ...) must still update the None caches, |
| 1077 | + # otherwise the guard would wrongly suppress real changes. |
| 1078 | + _run_cmd_query_with_final_names( |
| 1079 | + conn, |
| 1080 | + "USE SCHEMA OTHER_DB.OTHER_SCHEMA", |
| 1081 | + final_db="OTHER_DB", |
| 1082 | + final_schema="OTHER_SCHEMA", |
| 1083 | + ) |
| 1084 | + assert conn._database == "OTHER_DB" |
| 1085 | + assert conn._schema == "OTHER_SCHEMA" |
| 1086 | + |
| 1087 | + # Once a context is set, subsequent final*Name values refresh the cache |
| 1088 | + # as before (pre-existing behaviour is unchanged). |
| 1089 | + _run_cmd_query_with_final_names( |
| 1090 | + conn, |
| 1091 | + "SELECT 1", |
| 1092 | + final_db="OTHER_DB", |
| 1093 | + final_schema="NEW_SCHEMA", |
| 1094 | + ) |
| 1095 | + assert conn._schema == "NEW_SCHEMA" |
| 1096 | + finally: |
| 1097 | + conn.close() |
0 commit comments