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 +5 -0
- fakesnow/conn.py +15 -9
- fakesnow/cursor.py +34 -21
- fakesnow/info_schema.py +94 -23
- fakesnow/instance.py +3 -44
- fakesnow/server.py +58 -9
- fakesnow/{transforms.py → transforms/__init__.py} +225 -44
- {fakesnow-0.9.28.dist-info → fakesnow-0.9.30.dist-info}/METADATA +4 -4
- fakesnow-0.9.30.dist-info/RECORD +26 -0
- {fakesnow-0.9.28.dist-info → fakesnow-0.9.30.dist-info}/WHEEL +1 -1
- fakesnow-0.9.28.dist-info/RECORD +0 -26
- /fakesnow/{transforms_merge.py → transforms/merge.py} +0 -0
- {fakesnow-0.9.28.dist-info → fakesnow-0.9.30.dist-info}/LICENSE +0 -0
- {fakesnow-0.9.28.dist-info → fakesnow-0.9.30.dist-info}/entry_points.txt +0 -0
- {fakesnow-0.9.28.dist-info → fakesnow-0.9.30.dist-info}/top_level.txt +0 -0
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
166
|
-
.transform(transforms.
|
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.
|
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.
|
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.
|
344
|
+
self._conn._schema = None # noqa: SLF001
|
332
345
|
|
333
346
|
elif cmd == "DROP SCHEMA" and ident == self._conn.schema:
|
334
|
-
self._conn.
|
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
|
-
|
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
|
-
|
21
|
-
|
22
|
-
create table if not exists
|
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}.
|
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
|
68
|
-
left join
|
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}.
|
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 = '
|
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}.
|
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 != '
|
184
|
+
and schema_name != '_fs_information_schema'
|
124
185
|
"""
|
125
186
|
)
|
126
187
|
|
127
188
|
|
128
|
-
def
|
189
|
+
def per_db_creation_sql(catalog: str) -> str:
|
129
190
|
return f"""
|
130
|
-
{
|
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
|
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
|
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
|
-
|
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
|
-
|
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(
|
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
|
-
|
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
|
-
|
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
|
132
|
+
def to_token(request: Request) -> str:
|
106
133
|
if not (auth := request.headers.get("Authorization")):
|
107
|
-
raise ServerError(status_code=401, code="
|
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
|
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
|
-
|
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
|
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
|
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.
|
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() == "
|
215
|
-
#
|
216
|
-
|
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
|
599
|
-
"""Redirect to the
|
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="
|
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
|
617
|
-
|
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 (
|
622
|
-
and
|
623
|
-
and
|
698
|
+
and (tbl := expression.find(exp.Table))
|
699
|
+
and tbl.db.upper() == "INFORMATION_SCHEMA"
|
700
|
+
and tbl.name.upper() == "TABLES"
|
624
701
|
):
|
625
|
-
|
626
|
-
|
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
|
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 (
|
646
|
-
and
|
647
|
-
and
|
713
|
+
and (tbl := expression.find(exp.Table))
|
714
|
+
and tbl.db.upper() == "INFORMATION_SCHEMA"
|
715
|
+
and tbl.name.upper() == "VIEWS"
|
648
716
|
):
|
649
|
-
|
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
|
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 == '
|
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
|
-
|
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
|
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(
|
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(
|
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.
|
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.
|
213
|
+
Requires-Dist: duckdb~=1.2.0
|
214
214
|
Requires-Dist: pyarrow
|
215
215
|
Requires-Dist: snowflake-connector-python
|
216
|
-
Requires-Dist: sqlglot~=26.
|
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.
|
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,,
|
fakesnow-0.9.28.dist-info/RECORD
DELETED
@@ -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
|
File without changes
|
File without changes
|
File without changes
|