fakesnow 0.9.29__py3-none-any.whl → 0.9.30__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/checks.py CHANGED
@@ -63,6 +63,11 @@ def is_unqualified_table_expression(expression: exp.Expression) -> tuple[bool, b
63
63
  else:
64
64
  raise AssertionError(f"Unexpected parent kind: {parent_kind.name}")
65
65
 
66
+ elif node.parent.key == "show":
67
+ # don't require a database or schema for SHOW
68
+ # TODO: make this more nuanced
69
+ no_database = False
70
+ no_schema = False
66
71
  else:
67
72
  no_database = not node.args.get("catalog")
68
73
  no_schema = not node.args.get("db")
fakesnow/conn.py CHANGED
@@ -64,7 +64,7 @@ class FakeSnowflakeConnection:
64
64
  ):
65
65
  db_file = f"{self.db_path / self.database}.db" if self.db_path else ":memory:"
66
66
  duck_conn.execute(f"ATTACH DATABASE '{db_file}' AS {self.database}")
67
- duck_conn.execute(info_schema.creation_sql(self.database))
67
+ duck_conn.execute(info_schema.per_db_creation_sql(self.database))
68
68
  duck_conn.execute(macros.creation_sql(self.database))
69
69
 
70
70
  # create schema if needed
fakesnow/cursor.py CHANGED
@@ -182,7 +182,7 @@ class FakeSnowflakeCursor:
182
182
  .transform(transforms.create_database, db_path=self._conn.db_path)
183
183
  .transform(transforms.extract_comment_on_table)
184
184
  .transform(transforms.extract_comment_on_columns)
185
- .transform(transforms.information_schema_fs_columns_snowflake)
185
+ .transform(transforms.information_schema_fs_columns)
186
186
  .transform(transforms.information_schema_databases, current_schema=self._conn.schema)
187
187
  .transform(transforms.information_schema_fs_tables)
188
188
  .transform(transforms.information_schema_fs_views)
@@ -222,6 +222,9 @@ class FakeSnowflakeCursor:
222
222
  .transform(transforms.dateadd_date_cast)
223
223
  .transform(transforms.dateadd_string_literal_timestamp_cast)
224
224
  .transform(transforms.datediff_string_literal_timestamp_cast)
225
+ .transform(transforms.show_databases)
226
+ .transform(transforms.show_functions)
227
+ .transform(transforms.show_procedures)
225
228
  .transform(lambda e: transforms.show_schemas(e, self._conn.database))
226
229
  .transform(lambda e: transforms.show_objects_tables(e, self._conn.database))
227
230
  # TODO collapse into a single show_keys function
@@ -276,20 +279,24 @@ class FakeSnowflakeCursor:
276
279
  raise e
277
280
  except duckdb.ConnectionException as e:
278
281
  raise snowflake.connector.errors.DatabaseError(msg=e.args[0], errno=250002, sqlstate="08003") from None
282
+ except duckdb.ParserException as e:
283
+ raise snowflake.connector.errors.ProgrammingError(msg=e.args[0], errno=1003, sqlstate="42000") from None
279
284
 
280
285
  affected_count = None
281
286
 
282
287
  if set_database := transformed.args.get("set_database"):
283
288
  self._conn.database = set_database
284
289
  self._conn.database_set = True
290
+ result_sql = SQL_SUCCESS
285
291
 
286
292
  elif set_schema := transformed.args.get("set_schema"):
287
293
  self._conn._schema = set_schema # noqa: SLF001
288
294
  self._conn.schema_set = True
295
+ result_sql = SQL_SUCCESS
289
296
 
290
297
  elif create_db_name := transformed.args.get("create_db_name"):
291
298
  # we created a new database, so create the info schema extensions
292
- self._duck_conn.execute(info_schema.creation_sql(create_db_name))
299
+ self._duck_conn.execute(info_schema.per_db_creation_sql(create_db_name))
293
300
  result_sql = SQL_CREATED_DATABASE.substitute(name=create_db_name)
294
301
 
295
302
  elif cmd == "INSERT":
fakesnow/info_schema.py CHANGED
@@ -4,11 +4,14 @@ from __future__ import annotations
4
4
 
5
5
  from string import Template
6
6
 
7
- from fakesnow.instance import GLOBAL_DATABASE_NAME
7
+ SQL_CREATE_GLOBAL_FS_INFORMATION_SCHEMA = """
8
+ create schema if not exists _fs_global._fs_information_schema
9
+ """
10
+
8
11
 
9
12
  # use ext prefix in columns to disambiguate when joining with information_schema.tables
10
- SQL_CREATE_INFORMATION_SCHEMA_TABLES_EXT = f"""
11
- create table if not exists {GLOBAL_DATABASE_NAME}.main._fs_tables_ext (
13
+ SQL_CREATE_GLOBAL_INFORMATION_SCHEMA_TABLES_EXT = """
14
+ create table if not exists _fs_global._fs_information_schema._fs_tables_ext (
12
15
  ext_table_catalog varchar,
13
16
  ext_table_schema varchar,
14
17
  ext_table_name varchar,
@@ -18,8 +21,8 @@ create table if not exists {GLOBAL_DATABASE_NAME}.main._fs_tables_ext (
18
21
  """
19
22
 
20
23
 
21
- SQL_CREATE_INFORMATION_SCHEMA_COLUMNS_EXT = f"""
22
- create table if not exists {GLOBAL_DATABASE_NAME}.main._fs_columns_ext (
24
+ SQL_CREATE_GLOBAL_INFORMATION_SCHEMA_COLUMNS_EXT = """
25
+ create table if not exists _fs_global._fs_information_schema._fs_columns_ext (
23
26
  ext_table_catalog varchar,
24
27
  ext_table_schema varchar,
25
28
  ext_table_name varchar,
@@ -30,18 +33,56 @@ create table if not exists {GLOBAL_DATABASE_NAME}.main._fs_columns_ext (
30
33
  )
31
34
  """
32
35
 
36
+ # replicates the output structure of https://docs.snowflake.com/en/sql-reference/sql/show-users
37
+ SQL_CREATE_GLOBAL_INFORMATION_SCHEMA_USERS_TABLE_EXT = """
38
+ create table if not exists _fs_global._fs_information_schema._fs_users_ext (
39
+ name varchar,
40
+ created_on TIMESTAMPTZ,
41
+ login_name varchar,
42
+ display_name varchar,
43
+ first_name varchar,
44
+ last_name varchar,
45
+ email varchar,
46
+ mins_to_unlock varchar,
47
+ days_to_expiry varchar,
48
+ comment varchar,
49
+ disabled varchar,
50
+ must_change_password varchar,
51
+ snowflake_lock varchar,
52
+ default_warehouse varchar,
53
+ default_namespace varchar,
54
+ default_role varchar,
55
+ default_secondary_roles varchar,
56
+ ext_authn_duo varchar,
57
+ ext_authn_uid varchar,
58
+ mins_to_bypass_mfa varchar,
59
+ owner varchar,
60
+ last_success_login TIMESTAMPTZ,
61
+ expires_at_time TIMESTAMPTZ,
62
+ locked_until_time TIMESTAMPTZ,
63
+ has_password varchar,
64
+ has_rsa_public_key varchar,
65
+ )
66
+ """
67
+
68
+
33
69
  SQL_CREATE_FS_INFORMATION_SCHEMA = Template(
34
70
  """
35
71
  create schema if not exists ${catalog}._fs_information_schema
36
72
  """
37
73
  )
38
74
 
75
+ SQL_CREATE_INFORMATION_SCHEMA_COLUMNS_VIEW = Template(
76
+ """
77
+ create view if not exists ${catalog}._fs_information_schema._fs_columns AS
78
+ select * from _fs_global._fs_information_schema._fs_columns where table_catalog = '${catalog}'
79
+ """
80
+ )
39
81
 
40
82
  # only include fields applicable to snowflake (as mentioned by describe table information_schema.columns)
41
83
  # snowflake integers are 38 digits, base 10, See https://docs.snowflake.com/en/sql-reference/data-types-numeric
42
- SQL_CREATE_INFORMATION_SCHEMA_COLUMNS_VIEW = Template(
43
- """
44
- create view if not exists ${catalog}._fs_information_schema._fs_columns_snowflake AS
84
+ SQL_CREATE_GLOBAL_INFORMATION_SCHEMA_COLUMNS_VIEW = """
85
+ create view if not exists _fs_global._fs_information_schema._fs_columns AS
45
86
  select
46
87
  columns.table_catalog AS table_catalog,
47
88
  columns.table_schema AS table_schema,
@@ -71,7 +112,7 @@ collation_name, is_identity, identity_generation, identity_cycle,
71
112
  null::VARCHAR as identity_start,
72
113
  null::VARCHAR as identity_increment,
73
114
  from system.information_schema.columns columns
74
- left join _fs_global.main._fs_columns_ext ext
115
+ left join _fs_global._fs_information_schema._fs_columns_ext ext
75
116
  on ext_table_catalog = columns.table_catalog
76
117
  AND ext_table_schema = columns.table_schema
77
118
  AND ext_table_name = columns.table_name
@@ -81,10 +122,8 @@ LEFT JOIN duckdb_columns ddb_columns
81
122
  AND ddb_columns.schema_name = columns.table_schema
82
123
  AND ddb_columns.table_name = columns.table_name
83
124
  AND ddb_columns.column_name = columns.column_name
84
- where database_name = '${catalog}'
85
- and schema_name != '_fs_information_schema'
125
+ where schema_name != '_fs_information_schema'
86
126
  """
87
- )
88
127
 
89
128
 
90
129
  # replicates https://docs.snowflake.com/sql-reference/info-schema/databases
@@ -112,7 +151,7 @@ SQL_CREATE_INFORMATION_SCHEMA_TABLES_VIEW = Template(
112
151
  create view if not exists ${catalog}._fs_information_schema._fs_tables AS
113
152
  select *
114
153
  from system.information_schema.tables tables
115
- left join _fs_global.main._fs_tables_ext on
154
+ left join _fs_global._fs_information_schema._fs_tables_ext on
116
155
  tables.table_catalog = _fs_tables_ext.ext_table_catalog AND
117
156
  tables.table_schema = _fs_tables_ext.ext_table_schema AND
118
157
  tables.table_name = _fs_tables_ext.ext_table_name
@@ -147,7 +186,7 @@ where database_name = '${catalog}'
147
186
  )
148
187
 
149
188
 
150
- def creation_sql(catalog: str) -> str:
189
+ def per_db_creation_sql(catalog: str) -> str:
151
190
  return f"""
152
191
  {SQL_CREATE_FS_INFORMATION_SCHEMA.substitute(catalog=catalog)};
153
192
  {SQL_CREATE_INFORMATION_SCHEMA_COLUMNS_VIEW.substitute(catalog=catalog)};
@@ -159,14 +198,17 @@ def creation_sql(catalog: str) -> str:
159
198
 
160
199
  def fs_global_creation_sql(catalog: str) -> str:
161
200
  return f"""
162
- {SQL_CREATE_INFORMATION_SCHEMA_TABLES_EXT};
163
- {SQL_CREATE_INFORMATION_SCHEMA_COLUMNS_EXT};
201
+ {SQL_CREATE_GLOBAL_FS_INFORMATION_SCHEMA};
202
+ {SQL_CREATE_GLOBAL_INFORMATION_SCHEMA_TABLES_EXT};
203
+ {SQL_CREATE_GLOBAL_INFORMATION_SCHEMA_COLUMNS_EXT};
204
+ {SQL_CREATE_GLOBAL_INFORMATION_SCHEMA_COLUMNS_VIEW};
205
+ {SQL_CREATE_GLOBAL_INFORMATION_SCHEMA_USERS_TABLE_EXT};
164
206
  """
165
207
 
166
208
 
167
209
  def insert_table_comment_sql(catalog: str, schema: str, table: str, comment: str) -> str:
168
210
  return f"""
169
- INSERT INTO {GLOBAL_DATABASE_NAME}.main._fs_tables_ext
211
+ INSERT INTO _fs_global._fs_information_schema._fs_tables_ext
170
212
  values ('{catalog}', '{schema}', '{table}', '{comment}')
171
213
  ON CONFLICT (ext_table_catalog, ext_table_schema, ext_table_name)
172
214
  DO UPDATE SET comment = excluded.comment
@@ -180,7 +222,7 @@ def insert_text_lengths_sql(catalog: str, schema: str, table: str, text_lengths:
180
222
  )
181
223
 
182
224
  return f"""
183
- INSERT INTO {GLOBAL_DATABASE_NAME}.main._fs_columns_ext
225
+ INSERT INTO _fs_global._fs_information_schema._fs_columns_ext
184
226
  values {values}
185
227
  ON CONFLICT (ext_table_catalog, ext_table_schema, ext_table_name, ext_column_name)
186
228
  DO UPDATE SET ext_character_maximum_length = excluded.ext_character_maximum_length,
fakesnow/instance.py CHANGED
@@ -9,49 +9,6 @@ import fakesnow.fakes as fakes
9
9
  from fakesnow import info_schema
10
10
 
11
11
  GLOBAL_DATABASE_NAME = "_fs_global"
12
- USERS_TABLE_FQ_NAME = f"{GLOBAL_DATABASE_NAME}._fs_users_ext"
13
-
14
- # replicates the output structure of https://docs.snowflake.com/en/sql-reference/sql/show-users
15
- SQL_CREATE_INFORMATION_SCHEMA_USERS_TABLE_EXT = f"""
16
- create table if not exists {USERS_TABLE_FQ_NAME} (
17
- name varchar,
18
- created_on TIMESTAMPTZ,
19
- login_name varchar,
20
- display_name varchar,
21
- first_name varchar,
22
- last_name varchar,
23
- email varchar,
24
- mins_to_unlock varchar,
25
- days_to_expiry varchar,
26
- comment varchar,
27
- disabled varchar,
28
- must_change_password varchar,
29
- snowflake_lock varchar,
30
- default_warehouse varchar,
31
- default_namespace varchar,
32
- default_role varchar,
33
- default_secondary_roles varchar,
34
- ext_authn_duo varchar,
35
- ext_authn_uid varchar,
36
- mins_to_bypass_mfa varchar,
37
- owner varchar,
38
- last_success_login TIMESTAMPTZ,
39
- expires_at_time TIMESTAMPTZ,
40
- locked_until_time TIMESTAMPTZ,
41
- has_password varchar,
42
- has_rsa_public_key varchar,
43
- )
44
- """
45
-
46
-
47
- def create_global_database(conn: duckdb.DuckDBPyConnection) -> None:
48
- """Create a "global" database for storing objects which span databases.
49
-
50
- Including (but not limited to):
51
- - Users
52
- """
53
- conn.execute(f"ATTACH IF NOT EXISTS ':memory:' AS {GLOBAL_DATABASE_NAME}")
54
- conn.execute(SQL_CREATE_INFORMATION_SCHEMA_USERS_TABLE_EXT)
55
12
 
56
13
 
57
14
  class FakeSnow:
@@ -71,7 +28,6 @@ class FakeSnow:
71
28
 
72
29
  # create a "global" database for storing objects which span databases.
73
30
  self.duck_conn.execute(f"ATTACH IF NOT EXISTS ':memory:' AS {GLOBAL_DATABASE_NAME}")
74
- self.duck_conn.execute(SQL_CREATE_INFORMATION_SCHEMA_USERS_TABLE_EXT)
75
31
  # create the info schema extensions
76
32
  self.duck_conn.execute(info_schema.fs_global_creation_sql(GLOBAL_DATABASE_NAME))
77
33
 
fakesnow/server.py CHANGED
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import gzip
4
4
  import json
5
+ import logging
5
6
  import secrets
6
7
  from base64 import b64encode
7
8
  from dataclasses import dataclass
@@ -19,6 +20,11 @@ from fakesnow.fakes import FakeSnowflakeConnection
19
20
  from fakesnow.instance import FakeSnow
20
21
  from fakesnow.rowtype import describe_as_rowtype
21
22
 
23
+ logger = logging.getLogger("fakesnow.server")
24
+ # use same format as uvicorn
25
+ logger.handlers = logging.getLogger("uvicorn").handlers
26
+ logger.setLevel(logging.INFO)
27
+
22
28
  shared_fs = FakeSnow()
23
29
  sessions: dict[str, FakeSnowflakeConnection] = {}
24
30
 
@@ -46,13 +52,22 @@ async def login_request(request: Request) -> JSONResponse:
46
52
  # share the in-memory database across connections
47
53
  fs = shared_fs
48
54
  token = secrets.token_urlsafe(32)
55
+ logger.info(f"Session login {database=} {schema=}")
49
56
  sessions[token] = fs.connect(database, schema)
50
- return JSONResponse({"data": {"token": token}, "success": True})
57
+ return JSONResponse(
58
+ {
59
+ "data": {
60
+ "token": token,
61
+ "parameters": [{"name": "AUTOCOMMIT", "value": True}],
62
+ },
63
+ "success": True,
64
+ }
65
+ )
51
66
 
52
67
 
53
68
  async def query_request(request: Request) -> JSONResponse:
54
69
  try:
55
- conn = to_conn(request)
70
+ conn = to_conn(to_token(request))
56
71
 
57
72
  body = await request.body()
58
73
  if request.headers.get("Content-Encoding") == "gzip":
@@ -65,6 +80,8 @@ async def query_request(request: Request) -> JSONResponse:
65
80
  try:
66
81
  # only a single sql statement is sent at a time by the python snowflake connector
67
82
  cur = await run_in_threadpool(conn.cursor().execute, sql_text)
83
+ rowtype = describe_as_rowtype(cur._describe_last_sql()) # noqa: SLF001
84
+
68
85
  except snowflake.connector.errors.ProgrammingError as e:
69
86
  code = f"{e.errno:06d}"
70
87
  return JSONResponse(
@@ -78,8 +95,13 @@ async def query_request(request: Request) -> JSONResponse:
78
95
  "success": False,
79
96
  }
80
97
  )
81
-
82
- rowtype = describe_as_rowtype(cur._describe_last_sql()) # noqa: SLF001
98
+ except Exception as e:
99
+ # we have a bug or use of an unsupported feature
100
+ msg = f"Unhandled error during query {sql_text=}"
101
+ logger.error(msg, exc_info=e)
102
+ # my guess at mimicking a 500 error as per https://docs.snowflake.com/en/developer-guide/sql-api/reference
103
+ # and https://github.com/snowflakedb/gosnowflake/blob/8ed4c75ffd707dd712ad843f40189843ace683c4/restful.go#L318
104
+ raise ServerError(status_code=500, code="261000", message=msg) from None
83
105
 
84
106
  if cur._arrow_table: # noqa: SLF001
85
107
  batch_bytes = to_ipc(to_sf(cur._arrow_table, rowtype)) # noqa: SLF001
@@ -107,24 +129,46 @@ async def query_request(request: Request) -> JSONResponse:
107
129
  )
108
130
 
109
131
 
110
- def to_conn(request: Request) -> FakeSnowflakeConnection:
132
+ def to_token(request: Request) -> str:
111
133
  if not (auth := request.headers.get("Authorization")):
112
- raise ServerError(status_code=401, code="390103", message="Session token not found in the request data.")
134
+ raise ServerError(status_code=401, code="390101", message="Authorization header not found in the request data.")
113
135
 
114
- token = auth[17:-1]
136
+ return auth[17:-1]
115
137
 
138
+
139
+ def to_conn(token: str) -> FakeSnowflakeConnection:
116
140
  if not (conn := sessions.get(token)):
117
141
  raise ServerError(status_code=401, code="390104", message="User must login again to access the service.")
118
142
 
119
143
  return conn
120
144
 
121
145
 
146
+ async def session(request: Request) -> JSONResponse:
147
+ try:
148
+ token = to_token(request)
149
+ _ = to_conn(token)
150
+
151
+ if bool(request.query_params.get("delete")):
152
+ del sessions[token]
153
+
154
+ return JSONResponse(
155
+ {"data": None, "code": None, "message": None, "success": True},
156
+ )
157
+
158
+ except ServerError as e:
159
+ return JSONResponse(
160
+ {"data": None, "code": e.code, "message": e.message, "success": False, "headers": None},
161
+ status_code=e.status_code,
162
+ )
163
+
164
+
122
165
  routes = [
123
166
  Route(
124
167
  "/session/v1/login-request",
125
168
  login_request,
126
169
  methods=["POST"],
127
170
  ),
171
+ Route("/session", session, methods=["POST"]),
128
172
  Route(
129
173
  "/queries/v1/query-request",
130
174
  query_request,
@@ -7,10 +7,65 @@ from typing import ClassVar, Literal, cast
7
7
  import sqlglot
8
8
  from sqlglot import exp
9
9
 
10
- from fakesnow import transforms_merge
11
- from fakesnow.instance import USERS_TABLE_FQ_NAME
10
+ from fakesnow.transforms.merge import merge
12
11
  from fakesnow.variables import Variables
13
12
 
13
+ __all__ = [
14
+ "alias_in_join",
15
+ "alter_table_strip_cluster_by",
16
+ "array_agg",
17
+ "array_agg_within_group",
18
+ "array_size",
19
+ "create_clone",
20
+ "create_database",
21
+ "create_user",
22
+ "dateadd_date_cast",
23
+ "dateadd_string_literal_timestamp_cast",
24
+ "datediff_string_literal_timestamp_cast",
25
+ "drop_schema_cascade",
26
+ "extract_comment_on_columns",
27
+ "extract_comment_on_table",
28
+ "extract_text_length",
29
+ "flatten",
30
+ "flatten_value_cast_as_varchar",
31
+ "float_to_double",
32
+ "identifier",
33
+ "indices_to_json_extract",
34
+ "information_schema_databases",
35
+ "information_schema_fs_tables",
36
+ "information_schema_fs_views",
37
+ "integer_precision",
38
+ "json_extract_cased_as_varchar",
39
+ "json_extract_cast_as_varchar",
40
+ "json_extract_precedence",
41
+ "merge",
42
+ "object_construct",
43
+ "random",
44
+ "regex_replace",
45
+ "regex_substr",
46
+ "sample",
47
+ "semi_structured_types",
48
+ "set_schema",
49
+ "sha256",
50
+ "show_keys",
51
+ "show_objects_tables",
52
+ "show_schemas",
53
+ "show_users",
54
+ "split",
55
+ "tag",
56
+ "timestamp_ntz",
57
+ "to_date",
58
+ "to_decimal",
59
+ "to_timestamp",
60
+ "to_timestamp_ntz",
61
+ "trim_cast_varchar",
62
+ "try_parse_json",
63
+ "try_to_decimal",
64
+ "update_variables",
65
+ "upper_case_unquoted_identifiers",
66
+ "values_columns",
67
+ ]
68
+
14
69
  SUCCESS_NOP = sqlglot.parse_one("SELECT 'Statement executed successfully.' as status")
15
70
 
16
71
 
@@ -166,7 +221,7 @@ SELECT
166
221
  NULL::VARCHAR AS "comment",
167
222
  NULL::VARCHAR AS "policy name",
168
223
  NULL::JSON AS "privacy domain",
169
- FROM _fs_information_schema._fs_columns_snowflake
224
+ FROM _fs_information_schema._fs_columns
170
225
  WHERE table_catalog = '${catalog}' AND table_schema = '${schema}' AND table_name = '${table}'
171
226
  ORDER BY ordinal_position
172
227
  """
@@ -195,7 +250,7 @@ FROM (DESCRIBE ${view})
195
250
  def describe_table(
196
251
  expression: exp.Expression, current_database: str | None = None, current_schema: str | None = None
197
252
  ) -> exp.Expression:
198
- """Redirect to the information_schema._fs_columns_snowflake to match snowflake.
253
+ """Redirect to the information_schema._fs_columns to match snowflake.
199
254
 
200
255
  See https://docs.snowflake.com/en/sql-reference/sql/desc-table
201
256
  """
@@ -210,17 +265,10 @@ def describe_table(
210
265
  catalog = table.catalog or current_database
211
266
  schema = table.db or current_schema
212
267
 
213
- # TODO - move this after information_schema_fs_columns_snowflake
214
- if schema and schema.upper() == "INFORMATION_SCHEMA":
215
- # information schema views don't exist in _fs_columns_snowflake
216
- return sqlglot.parse_one(
217
- SQL_DESCRIBE_INFO_SCHEMA.substitute(view=f"system.information_schema.{table.name}"), read="duckdb"
218
- )
219
- elif table.name.upper() == "_FS_COLUMNS_SNOWFLAKE":
220
- # information schema views don't exist in _fs_columns_snowflake
221
- return sqlglot.parse_one(
222
- SQL_DESCRIBE_INFO_SCHEMA.substitute(view="_fs_information_schema._FS_COLUMNS_SNOWFLAKE"), read="duckdb"
223
- )
268
+ if schema and schema.upper() == "_FS_INFORMATION_SCHEMA":
269
+ # describing an information_schema view
270
+ # (schema already transformed from information_schema -> _fs_information_schema)
271
+ return sqlglot.parse_one(SQL_DESCRIBE_INFO_SCHEMA.substitute(view=f"{schema}.{table.name}"), read="duckdb")
224
272
 
225
273
  return sqlglot.parse_one(
226
274
  SQL_DESCRIBE_TABLE.substitute(catalog=catalog, schema=schema, table=table.name),
@@ -230,7 +278,7 @@ def describe_table(
230
278
  return expression
231
279
 
232
280
 
233
- def drop_schema_cascade(expression: exp.Expression) -> exp.Expression:
281
+ def drop_schema_cascade(expression: exp.Expression) -> exp.Expression: #
234
282
  """Drop schema cascade.
235
283
 
236
284
  By default duckdb won't delete a schema if it contains tables, whereas snowflake will.
@@ -602,8 +650,8 @@ def indices_to_json_extract(expression: exp.Expression) -> exp.Expression:
602
650
  return expression
603
651
 
604
652
 
605
- def information_schema_fs_columns_snowflake(expression: exp.Expression) -> exp.Expression:
606
- """Redirect to the _FS_COLUMNS_SNOWFLAKE view which has metadata that matches snowflake.
653
+ def information_schema_fs_columns(expression: exp.Expression) -> exp.Expression:
654
+ """Redirect to the _FS_COLUMNS view which has metadata that matches snowflake.
607
655
 
608
656
  Because duckdb doesn't store character_maximum_length or character_octet_length.
609
657
  """
@@ -615,7 +663,7 @@ def information_schema_fs_columns_snowflake(expression: exp.Expression) -> exp.E
615
663
  and expression.name
616
664
  and expression.name.upper() == "COLUMNS"
617
665
  ):
618
- expression.set("this", exp.Identifier(this="_FS_COLUMNS_SNOWFLAKE", quoted=False))
666
+ expression.set("this", exp.Identifier(this="_FS_COLUMNS", quoted=False))
619
667
  expression.set("db", exp.Identifier(this="_FS_INFORMATION_SCHEMA", quoted=False))
620
668
 
621
669
  return expression
@@ -741,10 +789,6 @@ def json_extract_precedence(expression: exp.Expression) -> exp.Expression:
741
789
  return expression
742
790
 
743
791
 
744
- def merge(expression: exp.Expression) -> list[exp.Expression]:
745
- return transforms_merge.merge(expression)
746
-
747
-
748
792
  def random(expression: exp.Expression) -> exp.Expression:
749
793
  """Convert random() and random(seed).
750
794
 
@@ -1023,7 +1067,7 @@ select
1023
1067
  catalog_name as 'database_name',
1024
1068
  NULL as 'schema_name'
1025
1069
  from information_schema.schemata
1026
- where not catalog_name in ('memory', 'system', 'temp')
1070
+ where not catalog_name in ('memory', 'system', 'temp', '_fs_global')
1027
1071
  and not schema_name in ('main', 'pg_catalog')
1028
1072
  """
1029
1073
 
@@ -1046,6 +1090,113 @@ def show_schemas(expression: exp.Expression, current_database: str | None = None
1046
1090
  return expression
1047
1091
 
1048
1092
 
1093
+ SQL_SHOW_DATABASES = """
1094
+ SELECT
1095
+ to_timestamp(0)::timestamptz as 'created_on',
1096
+ database_name as 'name',
1097
+ 'N' as 'is_default',
1098
+ 'N' as 'is_current',
1099
+ '' as 'origin',
1100
+ 'SYSADMIN' as 'owner',
1101
+ comment,
1102
+ '' as 'options',
1103
+ 1 as 'retention_time',
1104
+ 'STANDARD' as 'kind',
1105
+ NULL as 'budget',
1106
+ 'ROLE' as 'owner_role_type',
1107
+ NULL as 'object_visibility'
1108
+ FROM duckdb_databases
1109
+ WHERE database_name NOT IN ('memory', '_fs_global')
1110
+ """
1111
+
1112
+
1113
+ def show_databases(expression: exp.Expression) -> exp.Expression:
1114
+ """Transform SHOW DATABASES to a query against the information_schema.schemata table.
1115
+
1116
+ See https://docs.snowflake.com/en/sql-reference/sql/show-databases
1117
+ """
1118
+ if isinstance(expression, exp.Show) and isinstance(expression.this, str) and expression.this.upper() == "DATABASES":
1119
+ return sqlglot.parse_one(SQL_SHOW_DATABASES, read="duckdb")
1120
+
1121
+ return expression
1122
+
1123
+
1124
+ # returns zero rows
1125
+ SQL_SHOW_FUNCTIONS = """
1126
+ SELECT
1127
+ '1970-01-01 00:00:00 UTC'::timestamptz as created_on,
1128
+ 'SYSTIMESTAMP' as name,
1129
+ '' as schema_name,
1130
+ 'Y' as is_builtin,
1131
+ 'N' as is_aggregate,
1132
+ 'N' as is_ansi,
1133
+ 0 as min_num_arguments,
1134
+ 0 as max_num_arguments,
1135
+ 'SYSTIMESTAMP() RETURN TIMESTAMP_LTZ' as arguments,
1136
+ 'Returns the current timestamp' as description,
1137
+ '' as catalog_name,
1138
+ 'N' as is_table_function,
1139
+ 'N' as valid_for_clustering,
1140
+ NULL as is_secure,
1141
+ '' as secrets,
1142
+ '' as external_access_integrations,
1143
+ 'N' as is_external_function,
1144
+ 'SQL' as language,
1145
+ 'N' as is_memoizable,
1146
+ 'N' as is_data_metric
1147
+ WHERE 0 = 1;
1148
+ """
1149
+
1150
+
1151
+ def show_functions(expression: exp.Expression) -> exp.Expression:
1152
+ """Transform SHOW FUNCTIONS.
1153
+
1154
+ See https://docs.snowflake.com/en/sql-reference/sql/show-functions
1155
+ """
1156
+ if isinstance(expression, exp.Show) and isinstance(expression.this, str) and expression.this.upper() == "FUNCTIONS":
1157
+ return sqlglot.parse_one(SQL_SHOW_FUNCTIONS, read="duckdb")
1158
+
1159
+ return expression
1160
+
1161
+
1162
+ # returns zero rows
1163
+ SQL_SHOW_PROCEDURES = """
1164
+ SELECT
1165
+ '2012-08-01 07:00:00 UTC'::timestamptz as 'created_on',
1166
+ 'SYSTEM$CLASSIFY' as 'name',
1167
+ '' as 'schema_name',
1168
+ 'Y' as 'is_builtin',
1169
+ 'N' as 'is_aggregate',
1170
+ 'N' as 'is_ansi',
1171
+ 2 as 'min_num_arguments',
1172
+ 2 as 'max_num_arguments',
1173
+ 'SYSTEM$CLASSIFY(VARCHAR, OBJECT) RETURN OBJECT' as 'arguments',
1174
+ 'classify stored proc' as 'description',
1175
+ '' as 'catalog_name',
1176
+ 'N' as 'is_table_function',
1177
+ 'N' as 'valid_for_clustering',
1178
+ NULL as 'is_secure',
1179
+ '' as 'secrets',
1180
+ '' as 'external_access_integrations',
1181
+ WHERE 0 = 1;
1182
+ """
1183
+
1184
+
1185
+ def show_procedures(expression: exp.Expression) -> exp.Expression:
1186
+ """Transform SHOW PROCEDURES.
1187
+
1188
+ See https://docs.snowflake.com/en/sql-reference/sql/show-procedures
1189
+ """
1190
+ if (
1191
+ isinstance(expression, exp.Show)
1192
+ and isinstance(expression.this, str)
1193
+ and expression.this.upper() == "PROCEDURES"
1194
+ ):
1195
+ return sqlglot.parse_one(SQL_SHOW_PROCEDURES, read="duckdb")
1196
+
1197
+ return expression
1198
+
1199
+
1049
1200
  def split(expression: exp.Expression) -> exp.Expression:
1050
1201
  """
1051
1202
  Convert output of duckdb str_split from varchar[] to JSON array to match Snowflake.
@@ -1392,7 +1543,7 @@ def show_users(expression: exp.Expression) -> exp.Expression:
1392
1543
  https://docs.snowflake.com/en/sql-reference/sql/show-users
1393
1544
  """
1394
1545
  if isinstance(expression, exp.Show) and isinstance(expression.this, str) and expression.this.upper() == "USERS":
1395
- return sqlglot.parse_one(f"SELECT * FROM {USERS_TABLE_FQ_NAME}", read="duckdb")
1546
+ return sqlglot.parse_one("SELECT * FROM _fs_global._fs_information_schema._fs_users_ext", read="duckdb")
1396
1547
 
1397
1548
  return expression
1398
1549
 
@@ -1410,7 +1561,9 @@ def create_user(expression: exp.Expression) -> exp.Expression:
1410
1561
  _, name, *ignored = sub_exp.split(" ")
1411
1562
  if ignored:
1412
1563
  raise NotImplementedError(f"`CREATE USER` with {ignored} not yet supported")
1413
- return sqlglot.parse_one(f"INSERT INTO {USERS_TABLE_FQ_NAME} (name) VALUES ('{name}')", read="duckdb")
1564
+ return sqlglot.parse_one(
1565
+ f"INSERT INTO _fs_global._fs_information_schema._fs_users_ext (name) VALUES ('{name}')", read="duckdb"
1566
+ )
1414
1567
 
1415
1568
  return expression
1416
1569
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: fakesnow
3
- Version: 0.9.29
3
+ Version: 0.9.30
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,7 +213,7 @@ License-File: LICENSE
213
213
  Requires-Dist: duckdb~=1.2.0
214
214
  Requires-Dist: pyarrow
215
215
  Requires-Dist: snowflake-connector-python
216
- Requires-Dist: sqlglot~=26.6.0
216
+ Requires-Dist: sqlglot~=26.10.1
217
217
  Provides-Extra: dev
218
218
  Requires-Dist: build~=1.0; extra == "dev"
219
219
  Requires-Dist: dirty-equals; extra == "dev"
@@ -223,7 +223,7 @@ Requires-Dist: pre-commit~=4.0; extra == "dev"
223
223
  Requires-Dist: pyarrow-stubs==10.0.1.9; extra == "dev"
224
224
  Requires-Dist: pytest~=8.0; extra == "dev"
225
225
  Requires-Dist: pytest-asyncio; extra == "dev"
226
- Requires-Dist: ruff~=0.9.4; extra == "dev"
226
+ Requires-Dist: ruff~=0.11.0; extra == "dev"
227
227
  Requires-Dist: twine~=6.0; extra == "dev"
228
228
  Requires-Dist: snowflake-sqlalchemy~=1.7.0; extra == "dev"
229
229
  Provides-Extra: notebook
@@ -0,0 +1,26 @@
1
+ fakesnow/__init__.py,sha256=qUfgucQYPdELrJaxczalhJgWAWQ6cfTCUAHx6nUqRaI,3528
2
+ fakesnow/__main__.py,sha256=GDrGyNTvBFuqn_UfDjKs7b3LPtU6gDv1KwosVDrukIM,76
3
+ fakesnow/arrow.py,sha256=MwatkdZX5AFADzXvxhBFmcRJVxbW4D39VoqLyhpTbl0,5057
4
+ fakesnow/checks.py,sha256=be-xo0oMoAUVhlMDCu1_Rkoh_L8p_p8qo9P6reJSHIQ,2874
5
+ fakesnow/cli.py,sha256=9qfI-Ssr6mo8UmIlXkUAOz2z2YPBgDsrEVaZv9FjGFs,2201
6
+ fakesnow/conn.py,sha256=HGhFKErKWvAfVEy3QSc0tfNmzGh_T7FtvRfWuDBy_CQ,5744
7
+ fakesnow/cursor.py,sha256=KP8aDhq_m10ibpLjiL4retcwUCh_8PsY5sZfEFY_3No,20970
8
+ fakesnow/expr.py,sha256=CAxuYIUkwI339DQIBzvFF0F-m1tcVGKEPA5rDTzmH9A,892
9
+ fakesnow/fakes.py,sha256=JQTiUkkwPeQrJ8FDWhPFPK6pGwd_aR2oiOrNzCWznlM,187
10
+ fakesnow/fixtures.py,sha256=G-NkVeruSQAJ7fvSS2fR2oysUn0Yra1pohHlOvacKEk,455
11
+ fakesnow/info_schema.py,sha256=xDhGy07fpc8bcy_VTfh54UzwNIaB4ZhGmjgJeoiZ0hQ,8744
12
+ fakesnow/instance.py,sha256=vbg4XiAjpdglEqOM7X_HvCOnE-6Bf67nTYeBfGVUSNU,1889
13
+ fakesnow/macros.py,sha256=pX1YJDnQOkFJSHYUjQ6ErEkYIKvFI6Ncz_au0vv1csA,265
14
+ fakesnow/pandas_tools.py,sha256=wI203UQHC8JvDzxE_VjE1NeV4rThek2P-u52oTg2foo,3481
15
+ fakesnow/py.typed,sha256=B-DLSjYBi7pkKjwxCSdpVj2J02wgfJr-E7B1wOUyxYU,80
16
+ fakesnow/rowtype.py,sha256=QUp8EaXD5LT0Xv8BXk5ze4WseEn52xoJ6R05pJjs5mM,2729
17
+ fakesnow/server.py,sha256=-jKyEVuD2TEr88jUSA1Lu86MAymel7LQAiNlytHqhTg,5934
18
+ fakesnow/variables.py,sha256=WXyPnkeNwD08gy52yF66CVe2twiYC50tztNfgXV4q1k,3032
19
+ fakesnow/transforms/__init__.py,sha256=ENBHnwfQHAlC9PWOq4tdz-9-YQGy2E48xJB5ce7qEA0,60345
20
+ fakesnow/transforms/merge.py,sha256=Pg7_rwbAT_vr1U4ocBofUSyqaK8_e3qdIz_2SDm2S3s,8320
21
+ fakesnow-0.9.30.dist-info/LICENSE,sha256=kW-7NWIyaRMQiDpryfSmF2DObDZHGR1cJZ39s6B1Svg,11344
22
+ fakesnow-0.9.30.dist-info/METADATA,sha256=ryFSnkBU9Vb4BrYI9g9q16zN4M2env6w8-syT7LbJ1k,18109
23
+ fakesnow-0.9.30.dist-info/WHEEL,sha256=52BFRY2Up02UkjOa29eZOS2VxUrpPORXg1pkohGGUS8,91
24
+ fakesnow-0.9.30.dist-info/entry_points.txt,sha256=2riAUgu928ZIHawtO8EsfrMEJhi-EH-z_Vq7Q44xKPM,47
25
+ fakesnow-0.9.30.dist-info/top_level.txt,sha256=500evXI1IFX9so82cizGIEMHAb_dJNPaZvd2H9dcKTA,24
26
+ fakesnow-0.9.30.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.8.0)
2
+ Generator: setuptools (76.0.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,26 +0,0 @@
1
- fakesnow/__init__.py,sha256=qUfgucQYPdELrJaxczalhJgWAWQ6cfTCUAHx6nUqRaI,3528
2
- fakesnow/__main__.py,sha256=GDrGyNTvBFuqn_UfDjKs7b3LPtU6gDv1KwosVDrukIM,76
3
- fakesnow/arrow.py,sha256=MwatkdZX5AFADzXvxhBFmcRJVxbW4D39VoqLyhpTbl0,5057
4
- fakesnow/checks.py,sha256=N8sXldhS3u1gG32qvZ4VFlsKgavRKrQrxLiQU8am1lw,2691
5
- fakesnow/cli.py,sha256=9qfI-Ssr6mo8UmIlXkUAOz2z2YPBgDsrEVaZv9FjGFs,2201
6
- fakesnow/conn.py,sha256=da9ln_covsyKgdNdPXLzMTUBr72P0rRGadIDVt-kaeI,5737
7
- fakesnow/cursor.py,sha256=1BP1rZ28JfIfJkIR_8yEFDq2FrUf93JFrrYLJoKJr14,20587
8
- fakesnow/expr.py,sha256=CAxuYIUkwI339DQIBzvFF0F-m1tcVGKEPA5rDTzmH9A,892
9
- fakesnow/fakes.py,sha256=JQTiUkkwPeQrJ8FDWhPFPK6pGwd_aR2oiOrNzCWznlM,187
10
- fakesnow/fixtures.py,sha256=G-NkVeruSQAJ7fvSS2fR2oysUn0Yra1pohHlOvacKEk,455
11
- fakesnow/info_schema.py,sha256=_4YWnpuOFuyACr9k4iYdf2vLN7GDMG8X_pEBlC-8OmM,7269
12
- fakesnow/instance.py,sha256=7xHJv-5-KKAI3Qm7blcvkXgkGg7WYtXEm3nUS4jLyFs,3299
13
- fakesnow/macros.py,sha256=pX1YJDnQOkFJSHYUjQ6ErEkYIKvFI6Ncz_au0vv1csA,265
14
- fakesnow/pandas_tools.py,sha256=wI203UQHC8JvDzxE_VjE1NeV4rThek2P-u52oTg2foo,3481
15
- fakesnow/py.typed,sha256=B-DLSjYBi7pkKjwxCSdpVj2J02wgfJr-E7B1wOUyxYU,80
16
- fakesnow/rowtype.py,sha256=QUp8EaXD5LT0Xv8BXk5ze4WseEn52xoJ6R05pJjs5mM,2729
17
- fakesnow/server.py,sha256=VpM-ZjFS4JekLESEoQTndvXqnqz8bH4ZO8lq_66-c6s,4387
18
- fakesnow/transforms.py,sha256=dAoFFRFkJG8kcQdBPnE5w2eed4AZkh4NV3ajx1vu3A8,56444
19
- fakesnow/transforms_merge.py,sha256=Pg7_rwbAT_vr1U4ocBofUSyqaK8_e3qdIz_2SDm2S3s,8320
20
- fakesnow/variables.py,sha256=WXyPnkeNwD08gy52yF66CVe2twiYC50tztNfgXV4q1k,3032
21
- fakesnow-0.9.29.dist-info/LICENSE,sha256=kW-7NWIyaRMQiDpryfSmF2DObDZHGR1cJZ39s6B1Svg,11344
22
- fakesnow-0.9.29.dist-info/METADATA,sha256=sx4qqMsuxOaaDMKR5U8A4ZT0djn9wHSrOUjCqthUoWQ,18107
23
- fakesnow-0.9.29.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
24
- fakesnow-0.9.29.dist-info/entry_points.txt,sha256=2riAUgu928ZIHawtO8EsfrMEJhi-EH-z_Vq7Q44xKPM,47
25
- fakesnow-0.9.29.dist-info/top_level.txt,sha256=500evXI1IFX9so82cizGIEMHAb_dJNPaZvd2H9dcKTA,24
26
- fakesnow-0.9.29.dist-info/RECORD,,
File without changes