fakesnow 0.9.27__py3-none-any.whl → 0.9.29__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/arrow.py CHANGED
@@ -5,7 +5,7 @@ from typing import cast
5
5
  import pyarrow as pa
6
6
  import pyarrow.compute as pc
7
7
 
8
- from fakesnow.types import ColumnInfo
8
+ from fakesnow.rowtype import ColumnInfo
9
9
 
10
10
 
11
11
  def to_sf_schema(schema: pa.Schema, rowtype: list[ColumnInfo]) -> pa.Schema:
@@ -27,6 +27,10 @@ def to_sf_schema(schema: pa.Schema, rowtype: list[ColumnInfo]) -> pa.Schema:
27
27
  field = field.with_type(pa.struct(fields))
28
28
  elif isinstance(field.type, pa.Time64Type):
29
29
  field = field.with_type(pa.int64())
30
+ elif pa.types.is_uint64(field.type):
31
+ # snowflake-python-connector expects signed ints
32
+ # see https://github.com/snowflakedb/snowflake-connector-python/blob/5d7064c7f3f756792c1f6252bf5c9d807e4307e8/src/snowflake/connector/nanoarrow_cpp/ArrowIterator/CArrowChunkIterator.cpp#L187
33
+ field = field.with_type(pa.int64())
30
34
 
31
35
  return field.with_metadata(
32
36
  {
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
@@ -60,7 +62,7 @@ class FakeSnowflakeConnection:
60
62
  where upper(catalog_name) = '{self.database}'"""
61
63
  ).fetchone()
62
64
  ):
63
- db_file = f"{self.db_path/self.database}.db" if self.db_path else ":memory:"
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
67
  duck_conn.execute(info_schema.creation_sql(self.database))
66
68
  duck_conn.execute(macros.creation_sql(self.database))
@@ -69,24 +71,24 @@ class FakeSnowflakeConnection:
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
@@ -114,6 +116,10 @@ class FakeSnowflakeConnection:
114
116
  ) -> None:
115
117
  pass
116
118
 
119
+ def autocommit(self, _mode: bool) -> None:
120
+ # autcommit is always on in duckdb
121
+ pass
122
+
117
123
  def close(self, retry: bool = True) -> None:
118
124
  self._duck_conn.close()
119
125
  self._is_closed = True
@@ -145,3 +151,7 @@ class FakeSnowflakeConnection:
145
151
 
146
152
  def rollback(self) -> None:
147
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
@@ -3,13 +3,14 @@ from __future__ import annotations
3
3
  import os
4
4
  import re
5
5
  import sys
6
+ import uuid
6
7
  from collections.abc import Iterator, Sequence
7
8
  from string import Template
8
9
  from types import TracebackType
9
10
  from typing import TYPE_CHECKING, Any, cast
10
11
 
11
12
  import duckdb
12
- import pyarrow
13
+ import pyarrow # needed by fetch_arrow_table()
13
14
  import snowflake.connector.converter
14
15
  import snowflake.connector.errors
15
16
  import sqlglot
@@ -23,7 +24,7 @@ import fakesnow.checks as checks
23
24
  import fakesnow.expr as expr
24
25
  import fakesnow.info_schema as info_schema
25
26
  import fakesnow.transforms as transforms
26
- from fakesnow.types import describe_as_result_metadata
27
+ from fakesnow.rowtype import describe_as_result_metadata
27
28
 
28
29
  if TYPE_CHECKING:
29
30
  # don't require pandas at import time
@@ -71,6 +72,7 @@ class FakeSnowflakeCursor:
71
72
  self._arrow_table = None
72
73
  self._arrow_table_fetch_index = None
73
74
  self._rowcount = None
75
+ self._sfqid = None
74
76
  self._converter = snowflake.connector.converter.SnowflakeConverter()
75
77
 
76
78
  def __enter__(self) -> Self:
@@ -143,6 +145,8 @@ class FakeSnowflakeCursor:
143
145
  return self
144
146
 
145
147
  expression = parse_one(command, read="snowflake")
148
+ self.check_db_and_schema(expression)
149
+
146
150
  for exp in self._transform_explode(expression):
147
151
  transformed = self._transform(exp)
148
152
  self._execute(transformed, params)
@@ -152,6 +156,24 @@ class FakeSnowflakeCursor:
152
156
  self._sqlstate = e.sqlstate
153
157
  raise e
154
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
+
155
177
  def _transform(self, expression: exp.Expression) -> exp.Expression:
156
178
  return (
157
179
  expression.transform(transforms.upper_case_unquoted_identifiers)
@@ -161,7 +183,8 @@ class FakeSnowflakeCursor:
161
183
  .transform(transforms.extract_comment_on_table)
162
184
  .transform(transforms.extract_comment_on_columns)
163
185
  .transform(transforms.information_schema_fs_columns_snowflake)
164
- .transform(transforms.information_schema_fs_tables_ext)
186
+ .transform(transforms.information_schema_databases, current_schema=self._conn.schema)
187
+ .transform(transforms.information_schema_fs_tables)
165
188
  .transform(transforms.information_schema_fs_views)
166
189
  .transform(transforms.drop_schema_cascade)
167
190
  .transform(transforms.tag)
@@ -222,24 +245,10 @@ class FakeSnowflakeCursor:
222
245
  self._arrow_table = None
223
246
  self._arrow_table_fetch_index = None
224
247
  self._rowcount = None
248
+ self._sfqid = None
225
249
 
226
250
  cmd = expr.key_command(transformed)
227
251
 
228
- no_database, no_schema = checks.is_unqualified_table_expression(transformed)
229
-
230
- if no_database and not self._conn.database_set:
231
- raise snowflake.connector.errors.ProgrammingError(
232
- msg=f"Cannot perform {cmd}. This session does not have a current database. Call 'USE DATABASE', or use a qualified name.", # noqa: E501
233
- errno=90105,
234
- sqlstate="22000",
235
- )
236
- elif no_schema and not self._conn.schema_set:
237
- raise snowflake.connector.errors.ProgrammingError(
238
- msg=f"Cannot perform {cmd}. This session does not have a current schema. Call 'USE SCHEMA', or use a qualified name.", # noqa: E501
239
- errno=90106,
240
- sqlstate="22000",
241
- )
242
-
243
252
  sql = transformed.sql(dialect="duckdb")
244
253
 
245
254
  if transformed.find(exp.Select) and (seed := transformed.args.get("seed")):
@@ -275,7 +284,7 @@ class FakeSnowflakeCursor:
275
284
  self._conn.database_set = True
276
285
 
277
286
  elif set_schema := transformed.args.get("set_schema"):
278
- self._conn.schema = set_schema
287
+ self._conn._schema = set_schema # noqa: SLF001
279
288
  self._conn.schema_set = True
280
289
 
281
290
  elif create_db_name := transformed.args.get("create_db_name"):
@@ -325,10 +334,10 @@ class FakeSnowflakeCursor:
325
334
  # if dropping the current database/schema then reset conn metadata
326
335
  if cmd == "DROP DATABASE" and ident == self._conn.database:
327
336
  self._conn.database = None
328
- self._conn.schema = None
337
+ self._conn._schema = None # noqa: SLF001
329
338
 
330
339
  elif cmd == "DROP SCHEMA" and ident == self._conn.schema:
331
- self._conn.schema = None
340
+ self._conn._schema = None # noqa: SLF001
332
341
 
333
342
  if table_comment := cast(tuple[exp.Table, str], transformed.args.get("table_comment")):
334
343
  # record table comment
@@ -353,6 +362,7 @@ class FakeSnowflakeCursor:
353
362
 
354
363
  self._arrow_table = self._duck_conn.fetch_arrow_table()
355
364
  self._rowcount = affected_count or self._arrow_table.num_rows
365
+ self._sfqid = str(uuid.uuid4())
356
366
 
357
367
  self._last_sql = result_sql or sql
358
368
  self._last_params = params
@@ -423,7 +433,7 @@ class FakeSnowflakeCursor:
423
433
 
424
434
  @property
425
435
  def sfqid(self) -> str | None:
426
- return "fakesnow"
436
+ return self._sfqid
427
437
 
428
438
  @property
429
439
  def sqlstate(self) -> str | None:
fakesnow/info_schema.py CHANGED
@@ -4,10 +4,11 @@ from __future__ import annotations
4
4
 
5
5
  from string import Template
6
6
 
7
+ from fakesnow.instance import GLOBAL_DATABASE_NAME
8
+
7
9
  # 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 (
10
+ SQL_CREATE_INFORMATION_SCHEMA_TABLES_EXT = f"""
11
+ create table if not exists {GLOBAL_DATABASE_NAME}.main._fs_tables_ext (
11
12
  ext_table_catalog varchar,
12
13
  ext_table_schema varchar,
13
14
  ext_table_name varchar,
@@ -15,11 +16,10 @@ create table if not exists ${catalog}.information_schema._fs_tables_ext (
15
16
  PRIMARY KEY(ext_table_catalog, ext_table_schema, ext_table_name)
16
17
  )
17
18
  """
18
- )
19
19
 
20
- SQL_CREATE_INFORMATION_SCHEMA_COLUMNS_EXT = Template(
21
- """
22
- create table if not exists ${catalog}.information_schema._fs_columns_ext (
20
+
21
+ SQL_CREATE_INFORMATION_SCHEMA_COLUMNS_EXT = f"""
22
+ create table if not exists {GLOBAL_DATABASE_NAME}.main._fs_columns_ext (
23
23
  ext_table_catalog varchar,
24
24
  ext_table_schema varchar,
25
25
  ext_table_name varchar,
@@ -29,13 +29,19 @@ create table if not exists ${catalog}.information_schema._fs_columns_ext (
29
29
  PRIMARY KEY(ext_table_catalog, ext_table_schema, ext_table_name, ext_column_name)
30
30
  )
31
31
  """
32
+
33
+ SQL_CREATE_FS_INFORMATION_SCHEMA = Template(
34
+ """
35
+ create schema if not exists ${catalog}._fs_information_schema
36
+ """
32
37
  )
33
38
 
39
+
34
40
  # only include fields applicable to snowflake (as mentioned by describe table information_schema.columns)
35
41
  # snowflake integers are 38 digits, base 10, See https://docs.snowflake.com/en/sql-reference/data-types-numeric
36
42
  SQL_CREATE_INFORMATION_SCHEMA_COLUMNS_VIEW = Template(
37
43
  """
38
- create view if not exists ${catalog}.information_schema._fs_columns_snowflake AS
44
+ create view if not exists ${catalog}._fs_information_schema._fs_columns_snowflake AS
39
45
  select
40
46
  columns.table_catalog AS table_catalog,
41
47
  columns.table_schema AS table_schema,
@@ -64,8 +70,8 @@ collation_name, is_identity, identity_generation, identity_cycle,
64
70
  ddb_columns.comment as comment,
65
71
  null::VARCHAR as identity_start,
66
72
  null::VARCHAR as identity_increment,
67
- from ${catalog}.information_schema.columns columns
68
- left join ${catalog}.information_schema._fs_columns_ext ext
73
+ from system.information_schema.columns columns
74
+ left join _fs_global.main._fs_columns_ext ext
69
75
  on ext_table_catalog = columns.table_catalog
70
76
  AND ext_table_schema = columns.table_schema
71
77
  AND ext_table_name = columns.table_name
@@ -75,6 +81,8 @@ LEFT JOIN duckdb_columns ddb_columns
75
81
  AND ddb_columns.schema_name = columns.table_schema
76
82
  AND ddb_columns.table_name = columns.table_name
77
83
  AND ddb_columns.column_name = columns.column_name
84
+ where database_name = '${catalog}'
85
+ and schema_name != '_fs_information_schema'
78
86
  """
79
87
  )
80
88
 
@@ -82,7 +90,7 @@ LEFT JOIN duckdb_columns ddb_columns
82
90
  # replicates https://docs.snowflake.com/sql-reference/info-schema/databases
83
91
  SQL_CREATE_INFORMATION_SCHEMA_DATABASES_VIEW = Template(
84
92
  """
85
- create view if not exists ${catalog}.information_schema.databases AS
93
+ create view if not exists ${catalog}._fs_information_schema.databases AS
86
94
  select
87
95
  catalog_name as database_name,
88
96
  'SYSADMIN' as database_owner,
@@ -92,17 +100,31 @@ select
92
100
  to_timestamp(0)::timestamptz as last_altered,
93
101
  1 as retention_time,
94
102
  'STANDARD' as type
95
- from information_schema.schemata
103
+ from system.information_schema.schemata
96
104
  where catalog_name not in ('memory', 'system', 'temp', '_fs_global')
97
- and schema_name = 'information_schema'
105
+ and schema_name = 'main'
98
106
  """
99
107
  )
100
108
 
109
+ # replicates https://docs.snowflake.com/sql-reference/info-schema/tables
110
+ SQL_CREATE_INFORMATION_SCHEMA_TABLES_VIEW = Template(
111
+ """
112
+ create view if not exists ${catalog}._fs_information_schema._fs_tables AS
113
+ select *
114
+ from system.information_schema.tables tables
115
+ left join _fs_global.main._fs_tables_ext on
116
+ tables.table_catalog = _fs_tables_ext.ext_table_catalog AND
117
+ tables.table_schema = _fs_tables_ext.ext_table_schema AND
118
+ tables.table_name = _fs_tables_ext.ext_table_name
119
+ where table_catalog = '${catalog}'
120
+ and table_schema != '_fs_information_schema'
121
+ """
122
+ )
101
123
 
102
124
  # replicates https://docs.snowflake.com/sql-reference/info-schema/views
103
125
  SQL_CREATE_INFORMATION_SCHEMA_VIEWS_VIEW = Template(
104
126
  """
105
- create view if not exists ${catalog}.information_schema._fs_views AS
127
+ create view if not exists ${catalog}._fs_information_schema._fs_views AS
106
128
  select
107
129
  database_name as table_catalog,
108
130
  schema_name as table_schema,
@@ -120,24 +142,31 @@ select
120
142
  null::VARCHAR as comment
121
143
  from duckdb_views
122
144
  where database_name = '${catalog}'
123
- and schema_name != 'information_schema'
145
+ and schema_name != '_fs_information_schema'
124
146
  """
125
147
  )
126
148
 
127
149
 
128
150
  def creation_sql(catalog: str) -> str:
129
151
  return f"""
130
- {SQL_CREATE_INFORMATION_SCHEMA_TABLES_EXT.substitute(catalog=catalog)};
131
- {SQL_CREATE_INFORMATION_SCHEMA_COLUMNS_EXT.substitute(catalog=catalog)};
152
+ {SQL_CREATE_FS_INFORMATION_SCHEMA.substitute(catalog=catalog)};
132
153
  {SQL_CREATE_INFORMATION_SCHEMA_COLUMNS_VIEW.substitute(catalog=catalog)};
133
154
  {SQL_CREATE_INFORMATION_SCHEMA_DATABASES_VIEW.substitute(catalog=catalog)};
155
+ {SQL_CREATE_INFORMATION_SCHEMA_TABLES_VIEW.substitute(catalog=catalog)};
134
156
  {SQL_CREATE_INFORMATION_SCHEMA_VIEWS_VIEW.substitute(catalog=catalog)};
135
157
  """
136
158
 
137
159
 
160
+ def fs_global_creation_sql(catalog: str) -> str:
161
+ return f"""
162
+ {SQL_CREATE_INFORMATION_SCHEMA_TABLES_EXT};
163
+ {SQL_CREATE_INFORMATION_SCHEMA_COLUMNS_EXT};
164
+ """
165
+
166
+
138
167
  def insert_table_comment_sql(catalog: str, schema: str, table: str, comment: str) -> str:
139
168
  return f"""
140
- INSERT INTO {catalog}.information_schema._fs_tables_ext
169
+ INSERT INTO {GLOBAL_DATABASE_NAME}.main._fs_tables_ext
141
170
  values ('{catalog}', '{schema}', '{table}', '{comment}')
142
171
  ON CONFLICT (ext_table_catalog, ext_table_schema, ext_table_name)
143
172
  DO UPDATE SET comment = excluded.comment
@@ -146,12 +175,12 @@ def insert_table_comment_sql(catalog: str, schema: str, table: str, comment: str
146
175
 
147
176
  def insert_text_lengths_sql(catalog: str, schema: str, table: str, text_lengths: list[tuple[str, int]]) -> str:
148
177
  values = ", ".join(
149
- f"('{catalog}', '{schema}', '{table}', '{col_name}', {size}, {min(size*4,16777216)})"
178
+ f"('{catalog}', '{schema}', '{table}', '{col_name}', {size}, {min(size * 4, 16777216)})"
150
179
  for (col_name, size) in text_lengths
151
180
  )
152
181
 
153
182
  return f"""
154
- INSERT INTO {catalog}.information_schema._fs_columns_ext
183
+ INSERT INTO {GLOBAL_DATABASE_NAME}.main._fs_columns_ext
155
184
  values {values}
156
185
  ON CONFLICT (ext_table_catalog, ext_table_schema, ext_table_name, ext_column_name)
157
186
  DO UPDATE SET ext_character_maximum_length = excluded.ext_character_maximum_length,
fakesnow/instance.py CHANGED
@@ -6,6 +6,7 @@ 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
12
  USERS_TABLE_FQ_NAME = f"{GLOBAL_DATABASE_NAME}._fs_users_ext"
@@ -71,6 +72,8 @@ class FakeSnow:
71
72
  # create a "global" database for storing objects which span databases.
72
73
  self.duck_conn.execute(f"ATTACH IF NOT EXISTS ':memory:' AS {GLOBAL_DATABASE_NAME}")
73
74
  self.duck_conn.execute(SQL_CREATE_INFORMATION_SCHEMA_USERS_TABLE_EXT)
75
+ # create the info schema extensions
76
+ self.duck_conn.execute(info_schema.fs_global_creation_sql(GLOBAL_DATABASE_NAME))
74
77
 
75
78
  def connect(
76
79
  self, database: str | None = None, schema: str | None = None, **kwargs: Any
fakesnow/pandas_tools.py CHANGED
@@ -4,13 +4,13 @@ import json
4
4
  from collections.abc import Sequence
5
5
  from typing import TYPE_CHECKING, Any, Literal, Optional
6
6
 
7
- import numpy as np
8
7
  from duckdb import DuckDBPyConnection
9
8
 
10
9
  from fakesnow.conn import FakeSnowflakeConnection
11
10
 
12
11
  if TYPE_CHECKING:
13
- # don't require pandas at import time
12
+ # don't require pandas or numpy at import time
13
+ import numpy as np
14
14
  import pandas as pd
15
15
 
16
16
 
@@ -25,12 +25,14 @@ duckdb_to_sf_type = {
25
25
  "DATE": "date",
26
26
  "DECIMAL": "fixed",
27
27
  "DOUBLE": "real",
28
+ "HUGEINT": "fixed",
28
29
  "INTEGER": "fixed",
29
30
  "JSON": "variant",
30
31
  "TIME": "time",
31
32
  "TIMESTAMP WITH TIME ZONE": "timestamp_tz",
32
33
  "TIMESTAMP_NS": "timestamp_ntz",
33
34
  "TIMESTAMP": "timestamp_ntz",
35
+ "UBIGINT": "fixed",
34
36
  "VARCHAR": "text",
35
37
  }
36
38
 
fakesnow/server.py CHANGED
@@ -17,7 +17,7 @@ from starlette.routing import Route
17
17
  from fakesnow.arrow import to_ipc, to_sf
18
18
  from fakesnow.fakes import FakeSnowflakeConnection
19
19
  from fakesnow.instance import FakeSnow
20
- from fakesnow.types import describe_as_rowtype
20
+ from fakesnow.rowtype import describe_as_rowtype
21
21
 
22
22
  shared_fs = FakeSnow()
23
23
  sessions: dict[str, FakeSnowflakeConnection] = {}
@@ -34,7 +34,9 @@ async def login_request(request: Request) -> JSONResponse:
34
34
  database = request.query_params.get("databaseName")
35
35
  schema = request.query_params.get("schemaName")
36
36
  body = await request.body()
37
- body_json = json.loads(gzip.decompress(body))
37
+ if request.headers.get("Content-Encoding") == "gzip":
38
+ body = gzip.decompress(body)
39
+ body_json = json.loads(body)
38
40
  session_params: dict[str, Any] = body_json["data"]["SESSION_PARAMETERS"]
39
41
  if db_path := session_params.get("FAKESNOW_DB_PATH"):
40
42
  # isolated creates a new in-memory database, rather than using the shared in-memory database
@@ -53,7 +55,10 @@ async def query_request(request: Request) -> JSONResponse:
53
55
  conn = to_conn(request)
54
56
 
55
57
  body = await request.body()
56
- body_json = json.loads(gzip.decompress(body))
58
+ if request.headers.get("Content-Encoding") == "gzip":
59
+ body = gzip.decompress(body)
60
+
61
+ body_json = json.loads(body)
57
62
 
58
63
  sql_text = body_json["sqlText"]
59
64
 
@@ -87,7 +92,8 @@ async def query_request(request: Request) -> JSONResponse:
87
92
  "data": {
88
93
  "rowtype": rowtype,
89
94
  "rowsetBase64": rowset_b64,
90
- "total": 1,
95
+ "total": cur._rowcount, # noqa: SLF001
96
+ "queryId": cur.sfqid,
91
97
  "queryResultFormat": "arrow",
92
98
  },
93
99
  "success": True,
fakesnow/transforms.py CHANGED
@@ -11,7 +11,6 @@ from fakesnow import transforms_merge
11
11
  from fakesnow.instance import USERS_TABLE_FQ_NAME
12
12
  from fakesnow.variables import Variables
13
13
 
14
- MISSING_DATABASE = "missing_database"
15
14
  SUCCESS_NOP = sqlglot.parse_one("SELECT 'Statement executed successfully.' as status")
16
15
 
17
16
 
@@ -133,7 +132,7 @@ def create_database(expression: exp.Expression, db_path: Path | None = None) ->
133
132
  ident = expression.find(exp.Identifier)
134
133
  assert ident, f"No identifier in {expression.sql}"
135
134
  db_name = ident.this
136
- db_file = f"{db_path/db_name}.db" if db_path else ":memory:"
135
+ db_file = f"{db_path / db_name}.db" if db_path else ":memory:"
137
136
 
138
137
  if_not_exists = "IF NOT EXISTS " if expression.args.get("exists") else ""
139
138
 
@@ -167,7 +166,7 @@ SELECT
167
166
  NULL::VARCHAR AS "comment",
168
167
  NULL::VARCHAR AS "policy name",
169
168
  NULL::JSON AS "privacy domain",
170
- FROM information_schema._fs_columns_snowflake
169
+ FROM _fs_information_schema._fs_columns_snowflake
171
170
  WHERE table_catalog = '${catalog}' AND table_schema = '${schema}' AND table_name = '${table}'
172
171
  ORDER BY ordinal_position
173
172
  """
@@ -188,7 +187,7 @@ SELECT
188
187
  NULL::VARCHAR AS "comment",
189
188
  NULL::VARCHAR AS "policy name",
190
189
  NULL::JSON AS "privacy domain",
191
- FROM (DESCRIBE information_schema.${view})
190
+ FROM (DESCRIBE ${view})
192
191
  """
193
192
  )
194
193
 
@@ -211,9 +210,17 @@ def describe_table(
211
210
  catalog = table.catalog or current_database
212
211
  schema = table.db or current_schema
213
212
 
213
+ # TODO - move this after information_schema_fs_columns_snowflake
214
214
  if schema and schema.upper() == "INFORMATION_SCHEMA":
215
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")
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
+ )
217
224
 
218
225
  return sqlglot.parse_one(
219
226
  SQL_DESCRIBE_TABLE.substitute(catalog=catalog, schema=schema, table=table.name),
@@ -596,7 +603,7 @@ def indices_to_json_extract(expression: exp.Expression) -> exp.Expression:
596
603
 
597
604
 
598
605
  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.
606
+ """Redirect to the _FS_COLUMNS_SNOWFLAKE view which has metadata that matches snowflake.
600
607
 
601
608
  Because duckdb doesn't store character_maximum_length or character_octet_length.
602
609
  """
@@ -609,44 +616,58 @@ def information_schema_fs_columns_snowflake(expression: exp.Expression) -> exp.E
609
616
  and expression.name.upper() == "COLUMNS"
610
617
  ):
611
618
  expression.set("this", exp.Identifier(this="_FS_COLUMNS_SNOWFLAKE", quoted=False))
619
+ expression.set("db", exp.Identifier(this="_FS_INFORMATION_SCHEMA", quoted=False))
612
620
 
613
621
  return expression
614
622
 
615
623
 
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)."""
624
+ def information_schema_databases(
625
+ expression: exp.Expression,
626
+ current_schema: str | None = None,
627
+ ) -> exp.Expression:
628
+ if (
629
+ isinstance(expression, exp.Table)
630
+ and (
631
+ expression.db.upper() == "INFORMATION_SCHEMA"
632
+ or (current_schema and current_schema.upper() == "INFORMATION_SCHEMA")
633
+ )
634
+ and expression.name.upper() == "DATABASES"
635
+ ):
636
+ return exp.Table(
637
+ this=exp.Identifier(this="DATABASES", quoted=False),
638
+ db=exp.Identifier(this="_FS_INFORMATION_SCHEMA", quoted=False),
639
+ )
640
+ return expression
641
+
642
+
643
+ def information_schema_fs_tables(
644
+ expression: exp.Expression,
645
+ ) -> exp.Expression:
646
+ """Use _FS_TABLES to access additional metadata columns (eg: comment)."""
618
647
 
619
648
  if (
620
649
  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"
650
+ and (tbl := expression.find(exp.Table))
651
+ and tbl.db.upper() == "INFORMATION_SCHEMA"
652
+ and tbl.name.upper() == "TABLES"
624
653
  ):
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
- )
654
+ tbl.set("this", exp.Identifier(this="_FS_TABLES", quoted=False))
655
+ tbl.set("db", exp.Identifier(this="_FS_INFORMATION_SCHEMA", quoted=False))
636
656
 
637
657
  return expression
638
658
 
639
659
 
640
660
  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."""
661
+ """Use _FS_VIEWS to return Snowflake's version instead of duckdb's."""
642
662
 
643
663
  if (
644
664
  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"
665
+ and (tbl := expression.find(exp.Table))
666
+ and tbl.db.upper() == "INFORMATION_SCHEMA"
667
+ and tbl.name.upper() == "VIEWS"
648
668
  ):
649
- tbl_exp.set("this", exp.Identifier(this="_FS_VIEWS", quoted=False))
669
+ tbl.set("this", exp.Identifier(this="_FS_VIEWS", quoted=False))
670
+ tbl.set("db", exp.Identifier(this="_FS_INFORMATION_SCHEMA", quoted=False))
650
671
 
651
672
  return expression
652
673
 
@@ -921,7 +942,10 @@ def set_schema(expression: exp.Expression, current_database: str | None) -> exp.
921
942
  db_name = db.name
922
943
  else:
923
944
  # isn't qualified with a database
924
- db_name = current_database or MISSING_DATABASE
945
+ db_name = current_database
946
+
947
+ # assertion always true because check_db_schema is called before this
948
+ assert db_name
925
949
 
926
950
  schema = expression.this.name
927
951
  return exp.Command(
@@ -960,7 +984,7 @@ def show_objects_tables(expression: exp.Expression, current_database: str | None
960
984
  schema = None
961
985
 
962
986
  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_%%')"
987
+ exclude_fakesnow_tables = "not (table_schema == '_fs_information_schema')"
964
988
  # without a database will show everything in the "account"
965
989
  table_catalog = f" and table_catalog = '{catalog}'" if catalog else ""
966
990
  schema = f" and table_schema = '{schema}'" if schema else ""
@@ -991,12 +1015,16 @@ def show_objects_tables(expression: exp.Expression, current_database: str | None
991
1015
  SQL_SHOW_SCHEMAS = """
992
1016
  select
993
1017
  to_timestamp(0)::timestamptz as 'created_on',
994
- schema_name as 'name',
1018
+ case
1019
+ when schema_name = '_fs_information_schema' then 'information_schema'
1020
+ else schema_name
1021
+ end as 'name',
995
1022
  NULL as 'kind',
996
1023
  catalog_name as 'database_name',
997
1024
  NULL as 'schema_name'
998
1025
  from information_schema.schemata
999
- where catalog_name not in ('memory', 'system', 'temp') and schema_name not in ('main', 'pg_catalog')
1026
+ where not catalog_name in ('memory', 'system', 'temp')
1027
+ and not schema_name in ('main', 'pg_catalog')
1000
1028
  """
1001
1029
 
1002
1030
 
@@ -48,7 +48,7 @@ def _create_merge_candidates(merge_expr: exp.Merge) -> exp.Expression:
48
48
  )
49
49
 
50
50
  # Iterate through the WHEN clauses to build up the CASE WHEN clauses
51
- for w_idx, w in enumerate(merge_expr.expressions):
51
+ for w_idx, w in enumerate(merge_expr.args["whens"]):
52
52
  assert isinstance(w, exp.When), f"Expected When expression, got {w}"
53
53
 
54
54
  predicate = join_expr.copy()
@@ -83,9 +83,9 @@ def _create_merge_candidates(merge_expr: exp.Merge) -> exp.Expression:
83
83
  sql = f"""
84
84
  CREATE OR REPLACE TEMPORARY TABLE merge_candidates AS
85
85
  SELECT
86
- {', '.join(sorted(values))},
86
+ {", ".join(sorted(values))},
87
87
  CASE
88
- {' '.join(case_when_clauses)}
88
+ {" ".join(case_when_clauses)}
89
89
  ELSE NULL
90
90
  END AS MERGE_OP
91
91
  FROM {target_tbl}
@@ -109,7 +109,7 @@ def _mutations(merge_expr: exp.Merge) -> list[exp.Expression]:
109
109
  statements: list[exp.Expression] = []
110
110
 
111
111
  # Iterate through the WHEN clauses to generate delete/update/insert statements
112
- for w_idx, w in enumerate(merge_expr.expressions):
112
+ for w_idx, w in enumerate(merge_expr.args["whens"]):
113
113
  assert isinstance(w, exp.When), f"Expected When expression, got {w}"
114
114
 
115
115
  matched = w.args.get("matched")
@@ -173,7 +173,7 @@ def _counts(merge_expr: exp.Merge) -> exp.Expression:
173
173
  operations = {"inserted": [], "updated": [], "deleted": []}
174
174
 
175
175
  # Iterate through the WHEN clauses to categorize operations
176
- for w_idx, w in enumerate(merge_expr.expressions):
176
+ for w_idx, w in enumerate(merge_expr.args["whens"]):
177
177
  assert isinstance(w, exp.When), f"Expected When expression, got {w}"
178
178
 
179
179
  matched = w.args.get("matched")
@@ -191,12 +191,12 @@ def _counts(merge_expr: exp.Merge) -> exp.Expression:
191
191
  operations["inserted"].append(w_idx)
192
192
 
193
193
  count_statements = [
194
- f"""COUNT_IF(merge_op in ({','.join(map(str, indices))})) as \"number of rows {op}\""""
194
+ f"""COUNT_IF(merge_op in ({",".join(map(str, indices))})) as \"number of rows {op}\""""
195
195
  for op, indices in operations.items()
196
196
  if indices
197
197
  ]
198
198
  sql = f"""
199
- SELECT {', '.join(count_statements)}
199
+ SELECT {", ".join(count_statements)}
200
200
  FROM merge_candidates
201
201
  """
202
202
 
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: fakesnow
3
- Version: 0.9.27
3
+ Version: 0.9.29
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~=25.34.0
216
+ Requires-Dist: sqlglot~=26.6.0
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.8.1; extra == "dev"
226
+ Requires-Dist: ruff~=0.9.4; 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=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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.6.0)
2
+ Generator: setuptools (75.8.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=EGAYeuCnRuvmWBEGqw2YOcgQR4zcCsZBu85kSRl70dQ,4698
4
- fakesnow/checks.py,sha256=N8sXldhS3u1gG32qvZ4VFlsKgavRKrQrxLiQU8am1lw,2691
5
- fakesnow/cli.py,sha256=9qfI-Ssr6mo8UmIlXkUAOz2z2YPBgDsrEVaZv9FjGFs,2201
6
- fakesnow/conn.py,sha256=Gy_Z7BZRm5yMjV3x6hR4iegDQFdG9aJBjqWdc3iWYFU,5353
7
- fakesnow/cursor.py,sha256=8wWtRCxzrM1yiHmH2C-9CT0b98nTzr23ygeaEAkumRE,20086
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=nsDceFtjiSXrvkksKziVvqrefskaSyOmAspBwMAsaDg,6307
12
- fakesnow/instance.py,sha256=3cJvPRuFy19dMKXbtBLl6imzO48pEw8uTYhZyFDuwhk,3133
13
- fakesnow/macros.py,sha256=pX1YJDnQOkFJSHYUjQ6ErEkYIKvFI6Ncz_au0vv1csA,265
14
- fakesnow/pandas_tools.py,sha256=WjyjTV8QUCQQaCGboaEOvx2uo4BkknpWYjtLwkeCY6U,3468
15
- fakesnow/py.typed,sha256=B-DLSjYBi7pkKjwxCSdpVj2J02wgfJr-E7B1wOUyxYU,80
16
- fakesnow/server.py,sha256=SO5xKZ4rvySsuKDsoSPSCZcFuIX_K7d1XJYhRRJ-7Bk,4150
17
- fakesnow/transforms.py,sha256=h35WHWxhlXpfe03f3FCF64QVmiCLMdDZF7oZmaAOS60,55451
18
- fakesnow/transforms_merge.py,sha256=7rq-UPjfFNRrFsqR8xx3otwP6-k4eslLVLhfuqSXq1A,8314
19
- fakesnow/types.py,sha256=9Tt83Z7ctc9_v6SYyayXYz4MEI4RZo4zq_uqdj4g3Dk,2681
20
- fakesnow/variables.py,sha256=WXyPnkeNwD08gy52yF66CVe2twiYC50tztNfgXV4q1k,3032
21
- fakesnow-0.9.27.dist-info/LICENSE,sha256=kW-7NWIyaRMQiDpryfSmF2DObDZHGR1cJZ39s6B1Svg,11344
22
- fakesnow-0.9.27.dist-info/METADATA,sha256=k7PwECuxM55u-Dx_WyieDaH1p2BrHlcmu9INMsxGtzQ,18108
23
- fakesnow-0.9.27.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
24
- fakesnow-0.9.27.dist-info/entry_points.txt,sha256=2riAUgu928ZIHawtO8EsfrMEJhi-EH-z_Vq7Q44xKPM,47
25
- fakesnow-0.9.27.dist-info/top_level.txt,sha256=500evXI1IFX9so82cizGIEMHAb_dJNPaZvd2H9dcKTA,24
26
- fakesnow-0.9.27.dist-info/RECORD,,