fakesnow 0.9.28__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
@@ -42,13 +42,15 @@ class FakeSnowflakeConnection:
42
42
  # information_schema.schemata below we use upper-case to match any existing duckdb
43
43
  # catalog or schemas like "information_schema"
44
44
  self.database = database and database.upper()
45
- self.schema = schema and schema.upper()
45
+ self._schema = schema and (
46
+ "_FS_INFORMATION_SCHEMA" if schema.upper() == "INFORMATION_SCHEMA" else schema.upper()
47
+ )
46
48
 
47
49
  self.database_set = False
48
50
  self.schema_set = False
49
51
  self.db_path = Path(db_path) if db_path else None
50
52
  self.nop_regexes = nop_regexes
51
- self._paramstyle = snowflake.connector.paramstyle
53
+ self._paramstyle = kwargs.get("paramstyle", snowflake.connector.paramstyle)
52
54
  self.variables = Variables()
53
55
 
54
56
  # create database if needed
@@ -62,31 +64,31 @@ class FakeSnowflakeConnection:
62
64
  ):
63
65
  db_file = f"{self.db_path / self.database}.db" if self.db_path else ":memory:"
64
66
  duck_conn.execute(f"ATTACH DATABASE '{db_file}' AS {self.database}")
65
- duck_conn.execute(info_schema.creation_sql(self.database))
67
+ duck_conn.execute(info_schema.per_db_creation_sql(self.database))
66
68
  duck_conn.execute(macros.creation_sql(self.database))
67
69
 
68
70
  # create schema if needed
69
71
  if (
70
72
  create_schema
71
73
  and self.database
72
- and self.schema
74
+ and self._schema
73
75
  and not duck_conn.execute(
74
76
  f"""select * from information_schema.schemata
75
- where upper(catalog_name) = '{self.database}' and upper(schema_name) = '{self.schema}'"""
77
+ where upper(catalog_name) = '{self.database}' and upper(schema_name) = '{self._schema}'"""
76
78
  ).fetchone()
77
79
  ):
78
- duck_conn.execute(f"CREATE SCHEMA {self.database}.{self.schema}")
80
+ duck_conn.execute(f"CREATE SCHEMA {self.database}.{self._schema}")
79
81
 
80
82
  # set database and schema if both exist
81
83
  if (
82
84
  self.database
83
- and self.schema
85
+ and self._schema
84
86
  and duck_conn.execute(
85
87
  f"""select * from information_schema.schemata
86
- where upper(catalog_name) = '{self.database}' and upper(schema_name) = '{self.schema}'"""
88
+ where upper(catalog_name) = '{self.database}' and upper(schema_name) = '{self._schema}'"""
87
89
  ).fetchone()
88
90
  ):
89
- duck_conn.execute(f"SET schema='{self.database}.{self.schema}'")
91
+ duck_conn.execute(f"SET schema='{self.database}.{self._schema}'")
90
92
  self.database_set = True
91
93
  self.schema_set = True
92
94
  # set database if only that exists
@@ -149,3 +151,7 @@ class FakeSnowflakeConnection:
149
151
 
150
152
  def rollback(self) -> None:
151
153
  self.cursor().execute("ROLLBACK")
154
+
155
+ @property
156
+ def schema(self) -> str | None:
157
+ return "INFORMATION_SCHEMA" if self._schema == "_FS_INFORMATION_SCHEMA" else self._schema
fakesnow/cursor.py CHANGED
@@ -145,6 +145,8 @@ class FakeSnowflakeCursor:
145
145
  return self
146
146
 
147
147
  expression = parse_one(command, read="snowflake")
148
+ self.check_db_and_schema(expression)
149
+
148
150
  for exp in self._transform_explode(expression):
149
151
  transformed = self._transform(exp)
150
152
  self._execute(transformed, params)
@@ -154,6 +156,24 @@ class FakeSnowflakeCursor:
154
156
  self._sqlstate = e.sqlstate
155
157
  raise e
156
158
 
159
+ def check_db_and_schema(self, expression: exp.Expression) -> None:
160
+ no_database, no_schema = checks.is_unqualified_table_expression(expression)
161
+
162
+ if no_database and not self._conn.database_set:
163
+ cmd = expr.key_command(expression)
164
+ raise snowflake.connector.errors.ProgrammingError(
165
+ msg=f"Cannot perform {cmd}. This session does not have a current database. Call 'USE DATABASE', or use a qualified name.", # noqa: E501
166
+ errno=90105,
167
+ sqlstate="22000",
168
+ )
169
+ elif no_schema and not self._conn.schema_set:
170
+ cmd = expr.key_command(expression)
171
+ raise snowflake.connector.errors.ProgrammingError(
172
+ msg=f"Cannot perform {cmd}. This session does not have a current schema. Call 'USE SCHEMA', or use a qualified name.", # noqa: E501
173
+ errno=90106,
174
+ sqlstate="22000",
175
+ )
176
+
157
177
  def _transform(self, expression: exp.Expression) -> exp.Expression:
158
178
  return (
159
179
  expression.transform(transforms.upper_case_unquoted_identifiers)
@@ -162,8 +182,9 @@ class FakeSnowflakeCursor:
162
182
  .transform(transforms.create_database, db_path=self._conn.db_path)
163
183
  .transform(transforms.extract_comment_on_table)
164
184
  .transform(transforms.extract_comment_on_columns)
165
- .transform(transforms.information_schema_fs_columns_snowflake)
166
- .transform(transforms.information_schema_fs_tables_ext)
185
+ .transform(transforms.information_schema_fs_columns)
186
+ .transform(transforms.information_schema_databases, current_schema=self._conn.schema)
187
+ .transform(transforms.information_schema_fs_tables)
167
188
  .transform(transforms.information_schema_fs_views)
168
189
  .transform(transforms.drop_schema_cascade)
169
190
  .transform(transforms.tag)
@@ -201,6 +222,9 @@ class FakeSnowflakeCursor:
201
222
  .transform(transforms.dateadd_date_cast)
202
223
  .transform(transforms.dateadd_string_literal_timestamp_cast)
203
224
  .transform(transforms.datediff_string_literal_timestamp_cast)
225
+ .transform(transforms.show_databases)
226
+ .transform(transforms.show_functions)
227
+ .transform(transforms.show_procedures)
204
228
  .transform(lambda e: transforms.show_schemas(e, self._conn.database))
205
229
  .transform(lambda e: transforms.show_objects_tables(e, self._conn.database))
206
230
  # TODO collapse into a single show_keys function
@@ -228,21 +252,6 @@ class FakeSnowflakeCursor:
228
252
 
229
253
  cmd = expr.key_command(transformed)
230
254
 
231
- no_database, no_schema = checks.is_unqualified_table_expression(transformed)
232
-
233
- if no_database and not self._conn.database_set:
234
- raise snowflake.connector.errors.ProgrammingError(
235
- msg=f"Cannot perform {cmd}. This session does not have a current database. Call 'USE DATABASE', or use a qualified name.", # noqa: E501
236
- errno=90105,
237
- sqlstate="22000",
238
- )
239
- elif no_schema and not self._conn.schema_set:
240
- raise snowflake.connector.errors.ProgrammingError(
241
- msg=f"Cannot perform {cmd}. This session does not have a current schema. Call 'USE SCHEMA', or use a qualified name.", # noqa: E501
242
- errno=90106,
243
- sqlstate="22000",
244
- )
245
-
246
255
  sql = transformed.sql(dialect="duckdb")
247
256
 
248
257
  if transformed.find(exp.Select) and (seed := transformed.args.get("seed")):
@@ -270,20 +279,24 @@ class FakeSnowflakeCursor:
270
279
  raise e
271
280
  except duckdb.ConnectionException as e:
272
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
273
284
 
274
285
  affected_count = None
275
286
 
276
287
  if set_database := transformed.args.get("set_database"):
277
288
  self._conn.database = set_database
278
289
  self._conn.database_set = True
290
+ result_sql = SQL_SUCCESS
279
291
 
280
292
  elif set_schema := transformed.args.get("set_schema"):
281
- self._conn.schema = set_schema
293
+ self._conn._schema = set_schema # noqa: SLF001
282
294
  self._conn.schema_set = True
295
+ result_sql = SQL_SUCCESS
283
296
 
284
297
  elif create_db_name := transformed.args.get("create_db_name"):
285
298
  # we created a new database, so create the info schema extensions
286
- 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))
287
300
  result_sql = SQL_CREATED_DATABASE.substitute(name=create_db_name)
288
301
 
289
302
  elif cmd == "INSERT":
@@ -328,10 +341,10 @@ class FakeSnowflakeCursor:
328
341
  # if dropping the current database/schema then reset conn metadata
329
342
  if cmd == "DROP DATABASE" and ident == self._conn.database:
330
343
  self._conn.database = None
331
- self._conn.schema = None
344
+ self._conn._schema = None # noqa: SLF001
332
345
 
333
346
  elif cmd == "DROP SCHEMA" and ident == self._conn.schema:
334
- self._conn.schema = None
347
+ self._conn._schema = None # noqa: SLF001
335
348
 
336
349
  if table_comment := cast(tuple[exp.Table, str], transformed.args.get("table_comment")):
337
350
  # record table comment
fakesnow/info_schema.py CHANGED
@@ -4,10 +4,14 @@ from __future__ import annotations
4
4
 
5
5
  from string import Template
6
6
 
7
+ SQL_CREATE_GLOBAL_FS_INFORMATION_SCHEMA = """
8
+ create schema if not exists _fs_global._fs_information_schema
9
+ """
10
+
11
+
7
12
  # use ext prefix in columns to disambiguate when joining with information_schema.tables
8
- SQL_CREATE_INFORMATION_SCHEMA_TABLES_EXT = Template(
9
- """
10
- create table if not exists ${catalog}.information_schema._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 (
11
15
  ext_table_catalog varchar,
12
16
  ext_table_schema varchar,
13
17
  ext_table_name varchar,
@@ -15,11 +19,10 @@ create table if not exists ${catalog}.information_schema._fs_tables_ext (
15
19
  PRIMARY KEY(ext_table_catalog, ext_table_schema, ext_table_name)
16
20
  )
17
21
  """
18
- )
19
22
 
20
- SQL_CREATE_INFORMATION_SCHEMA_COLUMNS_EXT = Template(
21
- """
22
- create table if not exists ${catalog}.information_schema._fs_columns_ext (
23
+
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,
@@ -29,13 +32,57 @@ create table if not exists ${catalog}.information_schema._fs_columns_ext (
29
32
  PRIMARY KEY(ext_table_catalog, ext_table_schema, ext_table_name, ext_column_name)
30
33
  )
31
34
  """
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
+
69
+ SQL_CREATE_FS_INFORMATION_SCHEMA = Template(
70
+ """
71
+ create schema if not exists ${catalog}._fs_information_schema
72
+ """
32
73
  )
33
74
 
34
- # only include fields applicable to snowflake (as mentioned by describe table information_schema.columns)
35
- # snowflake integers are 38 digits, base 10, See https://docs.snowflake.com/en/sql-reference/data-types-numeric
36
75
  SQL_CREATE_INFORMATION_SCHEMA_COLUMNS_VIEW = Template(
37
76
  """
38
- create view if not exists ${catalog}.information_schema._fs_columns_snowflake AS
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
+ )
81
+
82
+ # only include fields applicable to snowflake (as mentioned by describe table information_schema.columns)
83
+ # snowflake integers are 38 digits, base 10, See https://docs.snowflake.com/en/sql-reference/data-types-numeric
84
+ SQL_CREATE_GLOBAL_INFORMATION_SCHEMA_COLUMNS_VIEW = """
85
+ create view if not exists _fs_global._fs_information_schema._fs_columns AS
39
86
  select
40
87
  columns.table_catalog AS table_catalog,
41
88
  columns.table_schema AS table_schema,
@@ -64,8 +111,8 @@ collation_name, is_identity, identity_generation, identity_cycle,
64
111
  ddb_columns.comment as comment,
65
112
  null::VARCHAR as identity_start,
66
113
  null::VARCHAR as identity_increment,
67
- from ${catalog}.information_schema.columns columns
68
- left join ${catalog}.information_schema._fs_columns_ext ext
114
+ from system.information_schema.columns columns
115
+ left join _fs_global._fs_information_schema._fs_columns_ext ext
69
116
  on ext_table_catalog = columns.table_catalog
70
117
  AND ext_table_schema = columns.table_schema
71
118
  AND ext_table_name = columns.table_name
@@ -75,14 +122,14 @@ LEFT JOIN duckdb_columns ddb_columns
75
122
  AND ddb_columns.schema_name = columns.table_schema
76
123
  AND ddb_columns.table_name = columns.table_name
77
124
  AND ddb_columns.column_name = columns.column_name
125
+ where schema_name != '_fs_information_schema'
78
126
  """
79
- )
80
127
 
81
128
 
82
129
  # replicates https://docs.snowflake.com/sql-reference/info-schema/databases
83
130
  SQL_CREATE_INFORMATION_SCHEMA_DATABASES_VIEW = Template(
84
131
  """
85
- create view if not exists ${catalog}.information_schema.databases AS
132
+ create view if not exists ${catalog}._fs_information_schema.databases AS
86
133
  select
87
134
  catalog_name as database_name,
88
135
  'SYSADMIN' as database_owner,
@@ -92,17 +139,31 @@ select
92
139
  to_timestamp(0)::timestamptz as last_altered,
93
140
  1 as retention_time,
94
141
  'STANDARD' as type
95
- from information_schema.schemata
142
+ from system.information_schema.schemata
96
143
  where catalog_name not in ('memory', 'system', 'temp', '_fs_global')
97
- and schema_name = 'information_schema'
144
+ and schema_name = 'main'
98
145
  """
99
146
  )
100
147
 
148
+ # replicates https://docs.snowflake.com/sql-reference/info-schema/tables
149
+ SQL_CREATE_INFORMATION_SCHEMA_TABLES_VIEW = Template(
150
+ """
151
+ create view if not exists ${catalog}._fs_information_schema._fs_tables AS
152
+ select *
153
+ from system.information_schema.tables tables
154
+ left join _fs_global._fs_information_schema._fs_tables_ext on
155
+ tables.table_catalog = _fs_tables_ext.ext_table_catalog AND
156
+ tables.table_schema = _fs_tables_ext.ext_table_schema AND
157
+ tables.table_name = _fs_tables_ext.ext_table_name
158
+ where table_catalog = '${catalog}'
159
+ and table_schema != '_fs_information_schema'
160
+ """
161
+ )
101
162
 
102
163
  # replicates https://docs.snowflake.com/sql-reference/info-schema/views
103
164
  SQL_CREATE_INFORMATION_SCHEMA_VIEWS_VIEW = Template(
104
165
  """
105
- create view if not exists ${catalog}.information_schema._fs_views AS
166
+ create view if not exists ${catalog}._fs_information_schema._fs_views AS
106
167
  select
107
168
  database_name as table_catalog,
108
169
  schema_name as table_schema,
@@ -120,24 +181,34 @@ select
120
181
  null::VARCHAR as comment
121
182
  from duckdb_views
122
183
  where database_name = '${catalog}'
123
- and schema_name != 'information_schema'
184
+ and schema_name != '_fs_information_schema'
124
185
  """
125
186
  )
126
187
 
127
188
 
128
- def creation_sql(catalog: str) -> str:
189
+ def per_db_creation_sql(catalog: str) -> str:
129
190
  return f"""
130
- {SQL_CREATE_INFORMATION_SCHEMA_TABLES_EXT.substitute(catalog=catalog)};
131
- {SQL_CREATE_INFORMATION_SCHEMA_COLUMNS_EXT.substitute(catalog=catalog)};
191
+ {SQL_CREATE_FS_INFORMATION_SCHEMA.substitute(catalog=catalog)};
132
192
  {SQL_CREATE_INFORMATION_SCHEMA_COLUMNS_VIEW.substitute(catalog=catalog)};
133
193
  {SQL_CREATE_INFORMATION_SCHEMA_DATABASES_VIEW.substitute(catalog=catalog)};
194
+ {SQL_CREATE_INFORMATION_SCHEMA_TABLES_VIEW.substitute(catalog=catalog)};
134
195
  {SQL_CREATE_INFORMATION_SCHEMA_VIEWS_VIEW.substitute(catalog=catalog)};
135
196
  """
136
197
 
137
198
 
199
+ def fs_global_creation_sql(catalog: str) -> str:
200
+ return f"""
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};
206
+ """
207
+
208
+
138
209
  def insert_table_comment_sql(catalog: str, schema: str, table: str, comment: str) -> str:
139
210
  return f"""
140
- INSERT INTO {catalog}.information_schema._fs_tables_ext
211
+ INSERT INTO _fs_global._fs_information_schema._fs_tables_ext
141
212
  values ('{catalog}', '{schema}', '{table}', '{comment}')
142
213
  ON CONFLICT (ext_table_catalog, ext_table_schema, ext_table_name)
143
214
  DO UPDATE SET comment = excluded.comment
@@ -151,7 +222,7 @@ def insert_text_lengths_sql(catalog: str, schema: str, table: str, text_lengths:
151
222
  )
152
223
 
153
224
  return f"""
154
- INSERT INTO {catalog}.information_schema._fs_columns_ext
225
+ INSERT INTO _fs_global._fs_information_schema._fs_columns_ext
155
226
  values {values}
156
227
  ON CONFLICT (ext_table_catalog, ext_table_schema, ext_table_name, ext_column_name)
157
228
  DO UPDATE SET ext_character_maximum_length = excluded.ext_character_maximum_length,
fakesnow/instance.py CHANGED
@@ -6,51 +6,9 @@ from typing import Any
6
6
  import duckdb
7
7
 
8
8
  import fakesnow.fakes as fakes
9
+ from fakesnow import info_schema
9
10
 
10
11
  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
12
 
55
13
 
56
14
  class FakeSnow:
@@ -70,7 +28,8 @@ class FakeSnow:
70
28
 
71
29
  # create a "global" database for storing objects which span databases.
72
30
  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)
31
+ # create the info schema extensions
32
+ self.duck_conn.execute(info_schema.fs_global_creation_sql(GLOBAL_DATABASE_NAME))
74
33
 
75
34
  def connect(
76
35
  self, database: str | None = None, schema: str | None = None, **kwargs: Any
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
 
@@ -34,7 +40,9 @@ async def login_request(request: Request) -> JSONResponse:
34
40
  database = request.query_params.get("databaseName")
35
41
  schema = request.query_params.get("schemaName")
36
42
  body = await request.body()
37
- body_json = json.loads(gzip.decompress(body))
43
+ if request.headers.get("Content-Encoding") == "gzip":
44
+ body = gzip.decompress(body)
45
+ body_json = json.loads(body)
38
46
  session_params: dict[str, Any] = body_json["data"]["SESSION_PARAMETERS"]
39
47
  if db_path := session_params.get("FAKESNOW_DB_PATH"):
40
48
  # isolated creates a new in-memory database, rather than using the shared in-memory database
@@ -44,22 +52,36 @@ async def login_request(request: Request) -> JSONResponse:
44
52
  # share the in-memory database across connections
45
53
  fs = shared_fs
46
54
  token = secrets.token_urlsafe(32)
55
+ logger.info(f"Session login {database=} {schema=}")
47
56
  sessions[token] = fs.connect(database, schema)
48
- 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
+ )
49
66
 
50
67
 
51
68
  async def query_request(request: Request) -> JSONResponse:
52
69
  try:
53
- conn = to_conn(request)
70
+ conn = to_conn(to_token(request))
54
71
 
55
72
  body = await request.body()
56
- body_json = json.loads(gzip.decompress(body))
73
+ if request.headers.get("Content-Encoding") == "gzip":
74
+ body = gzip.decompress(body)
75
+
76
+ body_json = json.loads(body)
57
77
 
58
78
  sql_text = body_json["sqlText"]
59
79
 
60
80
  try:
61
81
  # only a single sql statement is sent at a time by the python snowflake connector
62
82
  cur = await run_in_threadpool(conn.cursor().execute, sql_text)
83
+ rowtype = describe_as_rowtype(cur._describe_last_sql()) # noqa: SLF001
84
+
63
85
  except snowflake.connector.errors.ProgrammingError as e:
64
86
  code = f"{e.errno:06d}"
65
87
  return JSONResponse(
@@ -73,8 +95,13 @@ async def query_request(request: Request) -> JSONResponse:
73
95
  "success": False,
74
96
  }
75
97
  )
76
-
77
- 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
78
105
 
79
106
  if cur._arrow_table: # noqa: SLF001
80
107
  batch_bytes = to_ipc(to_sf(cur._arrow_table, rowtype)) # noqa: SLF001
@@ -102,24 +129,46 @@ async def query_request(request: Request) -> JSONResponse:
102
129
  )
103
130
 
104
131
 
105
- def to_conn(request: Request) -> FakeSnowflakeConnection:
132
+ def to_token(request: Request) -> str:
106
133
  if not (auth := request.headers.get("Authorization")):
107
- 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.")
135
+
136
+ return auth[17:-1]
108
137
 
109
- token = auth[17:-1]
110
138
 
139
+ def to_conn(token: str) -> FakeSnowflakeConnection:
111
140
  if not (conn := sessions.get(token)):
112
141
  raise ServerError(status_code=401, code="390104", message="User must login again to access the service.")
113
142
 
114
143
  return conn
115
144
 
116
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
+
117
165
  routes = [
118
166
  Route(
119
167
  "/session/v1/login-request",
120
168
  login_request,
121
169
  methods=["POST"],
122
170
  ),
171
+ Route("/session", session, methods=["POST"]),
123
172
  Route(
124
173
  "/queries/v1/query-request",
125
174
  query_request,
@@ -7,11 +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
 
14
- MISSING_DATABASE = "missing_database"
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
+
15
69
  SUCCESS_NOP = sqlglot.parse_one("SELECT 'Statement executed successfully.' as status")
16
70
 
17
71
 
@@ -167,7 +221,7 @@ SELECT
167
221
  NULL::VARCHAR AS "comment",
168
222
  NULL::VARCHAR AS "policy name",
169
223
  NULL::JSON AS "privacy domain",
170
- FROM information_schema._fs_columns_snowflake
224
+ FROM _fs_information_schema._fs_columns
171
225
  WHERE table_catalog = '${catalog}' AND table_schema = '${schema}' AND table_name = '${table}'
172
226
  ORDER BY ordinal_position
173
227
  """
@@ -188,7 +242,7 @@ SELECT
188
242
  NULL::VARCHAR AS "comment",
189
243
  NULL::VARCHAR AS "policy name",
190
244
  NULL::JSON AS "privacy domain",
191
- FROM (DESCRIBE information_schema.${view})
245
+ FROM (DESCRIBE ${view})
192
246
  """
193
247
  )
194
248
 
@@ -196,7 +250,7 @@ FROM (DESCRIBE information_schema.${view})
196
250
  def describe_table(
197
251
  expression: exp.Expression, current_database: str | None = None, current_schema: str | None = None
198
252
  ) -> exp.Expression:
199
- """Redirect to the information_schema._fs_columns_snowflake to match snowflake.
253
+ """Redirect to the information_schema._fs_columns to match snowflake.
200
254
 
201
255
  See https://docs.snowflake.com/en/sql-reference/sql/desc-table
202
256
  """
@@ -211,9 +265,10 @@ def describe_table(
211
265
  catalog = table.catalog or current_database
212
266
  schema = table.db or current_schema
213
267
 
214
- if schema and schema.upper() == "INFORMATION_SCHEMA":
215
- # information schema views don't exist in _fs_columns_snowflake
216
- return sqlglot.parse_one(SQL_DESCRIBE_INFO_SCHEMA.substitute(view=table.name), read="duckdb")
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")
217
272
 
218
273
  return sqlglot.parse_one(
219
274
  SQL_DESCRIBE_TABLE.substitute(catalog=catalog, schema=schema, table=table.name),
@@ -223,7 +278,7 @@ def describe_table(
223
278
  return expression
224
279
 
225
280
 
226
- def drop_schema_cascade(expression: exp.Expression) -> exp.Expression:
281
+ def drop_schema_cascade(expression: exp.Expression) -> exp.Expression: #
227
282
  """Drop schema cascade.
228
283
 
229
284
  By default duckdb won't delete a schema if it contains tables, whereas snowflake will.
@@ -595,8 +650,8 @@ def indices_to_json_extract(expression: exp.Expression) -> exp.Expression:
595
650
  return expression
596
651
 
597
652
 
598
- def information_schema_fs_columns_snowflake(expression: exp.Expression) -> exp.Expression:
599
- """Redirect to the information_schema._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.
600
655
 
601
656
  Because duckdb doesn't store character_maximum_length or character_octet_length.
602
657
  """
@@ -608,45 +663,59 @@ def information_schema_fs_columns_snowflake(expression: exp.Expression) -> exp.E
608
663
  and expression.name
609
664
  and expression.name.upper() == "COLUMNS"
610
665
  ):
611
- expression.set("this", exp.Identifier(this="_FS_COLUMNS_SNOWFLAKE", quoted=False))
666
+ expression.set("this", exp.Identifier(this="_FS_COLUMNS", quoted=False))
667
+ expression.set("db", exp.Identifier(this="_FS_INFORMATION_SCHEMA", quoted=False))
612
668
 
613
669
  return expression
614
670
 
615
671
 
616
- def information_schema_fs_tables_ext(expression: exp.Expression) -> exp.Expression:
617
- """Join to information_schema._fs_tables_ext to access additional metadata columns (eg: comment)."""
672
+ def information_schema_databases(
673
+ expression: exp.Expression,
674
+ current_schema: str | None = None,
675
+ ) -> exp.Expression:
676
+ if (
677
+ isinstance(expression, exp.Table)
678
+ and (
679
+ expression.db.upper() == "INFORMATION_SCHEMA"
680
+ or (current_schema and current_schema.upper() == "INFORMATION_SCHEMA")
681
+ )
682
+ and expression.name.upper() == "DATABASES"
683
+ ):
684
+ return exp.Table(
685
+ this=exp.Identifier(this="DATABASES", quoted=False),
686
+ db=exp.Identifier(this="_FS_INFORMATION_SCHEMA", quoted=False),
687
+ )
688
+ return expression
689
+
690
+
691
+ def information_schema_fs_tables(
692
+ expression: exp.Expression,
693
+ ) -> exp.Expression:
694
+ """Use _FS_TABLES to access additional metadata columns (eg: comment)."""
618
695
 
619
696
  if (
620
697
  isinstance(expression, exp.Select)
621
- and (tbl_exp := expression.find(exp.Table))
622
- and tbl_exp.name.upper() == "TABLES"
623
- and tbl_exp.db.upper() == "INFORMATION_SCHEMA"
698
+ and (tbl := expression.find(exp.Table))
699
+ and tbl.db.upper() == "INFORMATION_SCHEMA"
700
+ and tbl.name.upper() == "TABLES"
624
701
  ):
625
- return expression.join(
626
- "information_schema._fs_tables_ext",
627
- on=(
628
- """
629
- tables.table_catalog = _fs_tables_ext.ext_table_catalog AND
630
- tables.table_schema = _fs_tables_ext.ext_table_schema AND
631
- tables.table_name = _fs_tables_ext.ext_table_name
632
- """
633
- ),
634
- join_type="left",
635
- )
702
+ tbl.set("this", exp.Identifier(this="_FS_TABLES", quoted=False))
703
+ tbl.set("db", exp.Identifier(this="_FS_INFORMATION_SCHEMA", quoted=False))
636
704
 
637
705
  return expression
638
706
 
639
707
 
640
708
  def information_schema_fs_views(expression: exp.Expression) -> exp.Expression:
641
- """Use information_schema._fs_views to return Snowflake's version instead of duckdb's."""
709
+ """Use _FS_VIEWS to return Snowflake's version instead of duckdb's."""
642
710
 
643
711
  if (
644
712
  isinstance(expression, exp.Select)
645
- and (tbl_exp := expression.find(exp.Table))
646
- and tbl_exp.name.upper() == "VIEWS"
647
- and tbl_exp.db.upper() == "INFORMATION_SCHEMA"
713
+ and (tbl := expression.find(exp.Table))
714
+ and tbl.db.upper() == "INFORMATION_SCHEMA"
715
+ and tbl.name.upper() == "VIEWS"
648
716
  ):
649
- tbl_exp.set("this", exp.Identifier(this="_FS_VIEWS", quoted=False))
717
+ tbl.set("this", exp.Identifier(this="_FS_VIEWS", quoted=False))
718
+ tbl.set("db", exp.Identifier(this="_FS_INFORMATION_SCHEMA", quoted=False))
650
719
 
651
720
  return expression
652
721
 
@@ -720,10 +789,6 @@ def json_extract_precedence(expression: exp.Expression) -> exp.Expression:
720
789
  return expression
721
790
 
722
791
 
723
- def merge(expression: exp.Expression) -> list[exp.Expression]:
724
- return transforms_merge.merge(expression)
725
-
726
-
727
792
  def random(expression: exp.Expression) -> exp.Expression:
728
793
  """Convert random() and random(seed).
729
794
 
@@ -921,7 +986,10 @@ def set_schema(expression: exp.Expression, current_database: str | None) -> exp.
921
986
  db_name = db.name
922
987
  else:
923
988
  # isn't qualified with a database
924
- db_name = current_database or MISSING_DATABASE
989
+ db_name = current_database
990
+
991
+ # assertion always true because check_db_schema is called before this
992
+ assert db_name
925
993
 
926
994
  schema = expression.this.name
927
995
  return exp.Command(
@@ -960,7 +1028,7 @@ def show_objects_tables(expression: exp.Expression, current_database: str | None
960
1028
  schema = None
961
1029
 
962
1030
  tables_only = "table_type = 'BASE TABLE' and " if show == "TABLES" else ""
963
- exclude_fakesnow_tables = "not (table_schema == 'information_schema' and table_name like '_fs_%%')"
1031
+ exclude_fakesnow_tables = "not (table_schema == '_fs_information_schema')"
964
1032
  # without a database will show everything in the "account"
965
1033
  table_catalog = f" and table_catalog = '{catalog}'" if catalog else ""
966
1034
  schema = f" and table_schema = '{schema}'" if schema else ""
@@ -991,12 +1059,16 @@ def show_objects_tables(expression: exp.Expression, current_database: str | None
991
1059
  SQL_SHOW_SCHEMAS = """
992
1060
  select
993
1061
  to_timestamp(0)::timestamptz as 'created_on',
994
- schema_name as 'name',
1062
+ case
1063
+ when schema_name = '_fs_information_schema' then 'information_schema'
1064
+ else schema_name
1065
+ end as 'name',
995
1066
  NULL as 'kind',
996
1067
  catalog_name as 'database_name',
997
1068
  NULL as 'schema_name'
998
1069
  from information_schema.schemata
999
- where catalog_name not in ('memory', 'system', 'temp') and schema_name not in ('main', 'pg_catalog')
1070
+ where not catalog_name in ('memory', 'system', 'temp', '_fs_global')
1071
+ and not schema_name in ('main', 'pg_catalog')
1000
1072
  """
1001
1073
 
1002
1074
 
@@ -1018,6 +1090,113 @@ def show_schemas(expression: exp.Expression, current_database: str | None = None
1018
1090
  return expression
1019
1091
 
1020
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
+
1021
1200
  def split(expression: exp.Expression) -> exp.Expression:
1022
1201
  """
1023
1202
  Convert output of duckdb str_split from varchar[] to JSON array to match Snowflake.
@@ -1364,7 +1543,7 @@ def show_users(expression: exp.Expression) -> exp.Expression:
1364
1543
  https://docs.snowflake.com/en/sql-reference/sql/show-users
1365
1544
  """
1366
1545
  if isinstance(expression, exp.Show) and isinstance(expression.this, str) and expression.this.upper() == "USERS":
1367
- 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")
1368
1547
 
1369
1548
  return expression
1370
1549
 
@@ -1382,7 +1561,9 @@ def create_user(expression: exp.Expression) -> exp.Expression:
1382
1561
  _, name, *ignored = sub_exp.split(" ")
1383
1562
  if ignored:
1384
1563
  raise NotImplementedError(f"`CREATE USER` with {ignored} not yet supported")
1385
- 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
+ )
1386
1567
 
1387
1568
  return expression
1388
1569
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: fakesnow
3
- Version: 0.9.28
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
@@ -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~=1.1.3
213
+ Requires-Dist: duckdb~=1.2.0
214
214
  Requires-Dist: pyarrow
215
215
  Requires-Dist: snowflake-connector-python
216
- Requires-Dist: sqlglot~=26.3.9
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=GJ7Y2dBW2jcOCIZ0gTYS0F8OmeoD7aW6lTWHpm02hbE,5459
7
- fakesnow/cursor.py,sha256=uYf3zshauWXnKdUoVEE3YxMbc-SoVLHCUUkqwtMW8ns,20228
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=FyDcajHU0BK0Yx6JTIkWFoM0PPFm6f6Pf0B2NHLNR0M,6310
12
- fakesnow/instance.py,sha256=3cJvPRuFy19dMKXbtBLl6imzO48pEw8uTYhZyFDuwhk,3133
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=lrXk8iXioSl-qWweXLH7l6aPrcV4Bym9rjB6x_C5Fg8,4222
18
- fakesnow/transforms.py,sha256=pSv3pQlD1Y8tzXQ1rft2g5wYLcHrDRoy5EkFWgqkmec,55453
19
- fakesnow/transforms_merge.py,sha256=Pg7_rwbAT_vr1U4ocBofUSyqaK8_e3qdIz_2SDm2S3s,8320
20
- fakesnow/variables.py,sha256=WXyPnkeNwD08gy52yF66CVe2twiYC50tztNfgXV4q1k,3032
21
- fakesnow-0.9.28.dist-info/LICENSE,sha256=kW-7NWIyaRMQiDpryfSmF2DObDZHGR1cJZ39s6B1Svg,11344
22
- fakesnow-0.9.28.dist-info/METADATA,sha256=t0B6J7rG5uyS2430rgPWieQgACfyUU9dIL5cUdYllDg,18107
23
- fakesnow-0.9.28.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
24
- fakesnow-0.9.28.dist-info/entry_points.txt,sha256=2riAUgu928ZIHawtO8EsfrMEJhi-EH-z_Vq7Q44xKPM,47
25
- fakesnow-0.9.28.dist-info/top_level.txt,sha256=500evXI1IFX9so82cizGIEMHAb_dJNPaZvd2H9dcKTA,24
26
- fakesnow-0.9.28.dist-info/RECORD,,
File without changes