fakesnow 0.9.31__py3-none-any.whl → 0.9.32__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/conn.py CHANGED
@@ -62,7 +62,16 @@ class FakeSnowflakeConnection:
62
62
  where upper(catalog_name) = '{self.database}'"""
63
63
  ).fetchone()
64
64
  ):
65
- db_file = f"{self.db_path / self.database}.db" if self.db_path else ":memory:"
65
+ if self.db_path:
66
+ # raise a helpful error message when directory doesn't exist so users don't think
67
+ # they have to create the database themselves
68
+ if not os.path.isdir(self.db_path):
69
+ raise NotADirectoryError(f"No such directory: '{self.db_path}'. Please ensure db_path exists.")
70
+ db_file = f"{self.db_path / self.database}.db"
71
+ else:
72
+ db_file = ":memory:"
73
+
74
+ # creates db file if it doesn't exist
66
75
  duck_conn.execute(f"ATTACH DATABASE '{db_file}' AS {self.database}")
67
76
  duck_conn.execute(info_schema.per_db_creation_sql(self.database))
68
77
  duck_conn.execute(macros.creation_sql(self.database))
@@ -102,9 +111,6 @@ class FakeSnowflakeConnection:
102
111
  duck_conn.execute(f"SET schema='{self.database}.main'")
103
112
  self.database_set = True
104
113
 
105
- # use UTC instead of local time zone for consistent testing
106
- duck_conn.execute("SET GLOBAL TimeZone = 'UTC'")
107
-
108
114
  def __enter__(self) -> Self:
109
115
  return self
110
116
 
fakesnow/cursor.py CHANGED
@@ -231,8 +231,10 @@ class FakeSnowflakeCursor:
231
231
  .transform(transforms.show_databases)
232
232
  .transform(transforms.show_functions)
233
233
  .transform(transforms.show_procedures)
234
+ .transform(transforms.show_warehouses)
234
235
  .transform(lambda e: transforms.show_schemas(e, self._conn.database))
235
236
  .transform(lambda e: transforms.show_objects_tables(e, self._conn.database))
237
+ .transform(lambda e: transforms.show_columns(e, self._conn.database))
236
238
  # TODO collapse into a single show_keys function
237
239
  .transform(lambda e: transforms.show_keys(e, self._conn.database, kind="PRIMARY"))
238
240
  .transform(lambda e: transforms.show_keys(e, self._conn.database, kind="UNIQUE"))
@@ -260,6 +262,9 @@ class FakeSnowflakeCursor:
260
262
 
261
263
  sql = transformed.sql(dialect="duckdb")
262
264
 
265
+ if not sql:
266
+ raise NotImplementedError(transformed.sql(dialect="snowflake"))
267
+
263
268
  if transformed.find(exp.Select) and (seed := transformed.args.get("seed")):
264
269
  sql = f"SELECT setseed({seed}); {sql}"
265
270
 
@@ -293,6 +298,7 @@ class FakeSnowflakeCursor:
293
298
  if set_database := transformed.args.get("set_database"):
294
299
  self._conn.database = set_database
295
300
  self._conn.database_set = True
301
+ self._conn.schema_set = False
296
302
  result_sql = SQL_SUCCESS
297
303
 
298
304
  elif set_schema := transformed.args.get("set_schema"):
fakesnow/instance.py CHANGED
@@ -31,6 +31,9 @@ class FakeSnow:
31
31
  # create the info schema extensions
32
32
  self.duck_conn.execute(info_schema.fs_global_creation_sql(GLOBAL_DATABASE_NAME))
33
33
 
34
+ # use UTC instead of local time zone for consistent testing
35
+ self.duck_conn.execute("SET GLOBAL TimeZone = 'UTC'")
36
+
34
37
  def connect(
35
38
  self, database: str | None = None, schema: str | None = None, **kwargs: Any
36
39
  ) -> fakes.FakeSnowflakeConnection:
fakesnow/server.py CHANGED
@@ -83,6 +83,7 @@ async def query_request(request: Request) -> JSONResponse:
83
83
  rowtype = describe_as_rowtype(cur._describe_last_sql()) # noqa: SLF001
84
84
 
85
85
  except snowflake.connector.errors.ProgrammingError as e:
86
+ logger.info(f"{sql_text=} ProgrammingError {e}")
86
87
  code = f"{e.errno:06d}"
87
88
  return JSONResponse(
88
89
  {
@@ -97,7 +98,7 @@ async def query_request(request: Request) -> JSONResponse:
97
98
  )
98
99
  except Exception as e:
99
100
  # we have a bug or use of an unsupported feature
100
- msg = f"Unhandled error during query {sql_text=}"
101
+ msg = f"{sql_text=} Unhandled exception"
101
102
  logger.error(msg, exc_info=e)
102
103
  # my guess at mimicking a 500 error as per https://docs.snowflake.com/en/developer-guide/sql-api/reference
103
104
  # and https://github.com/snowflakedb/gosnowflake/blob/8ed4c75ffd707dd712ad843f40189843ace683c4/restful.go#L318
@@ -2,70 +2,25 @@ from __future__ import annotations
2
2
 
3
3
  from pathlib import Path
4
4
  from string import Template
5
- from typing import ClassVar, Literal, cast
5
+ from typing import ClassVar, cast
6
6
 
7
7
  import sqlglot
8
8
  from sqlglot import exp
9
9
 
10
- from fakesnow.transforms.merge import merge
10
+ from fakesnow.transforms.merge import merge as merge
11
+ from fakesnow.transforms.show import (
12
+ show_columns as show_columns,
13
+ show_databases as show_databases,
14
+ show_functions as show_functions,
15
+ show_keys as show_keys,
16
+ show_objects_tables as show_objects_tables,
17
+ show_procedures as show_procedures,
18
+ show_schemas as show_schemas,
19
+ show_users as show_users,
20
+ show_warehouses as show_warehouses,
21
+ )
11
22
  from fakesnow.variables import Variables
12
23
 
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
-
69
24
  SUCCESS_NOP = sqlglot.parse_one("SELECT 'Statement executed successfully.' as status")
70
25
 
71
26
 
@@ -621,7 +576,7 @@ def identifier(expression: exp.Expression) -> exp.Expression:
621
576
 
622
577
 
623
578
  def indices_to_json_extract(expression: exp.Expression) -> exp.Expression:
624
- """Convert indices on objects and arrays to json_extract.
579
+ """Convert indices on objects and arrays to json_extract or json_extract_string
625
580
 
626
581
  Supports Snowflake array indices, see
627
582
  https://docs.snowflake.com/en/sql-reference/data-types-semistructured#accessing-elements-of-an-array-by-index-or-by-slice
@@ -640,12 +595,16 @@ def indices_to_json_extract(expression: exp.Expression) -> exp.Expression:
640
595
  and isinstance(index, exp.Literal)
641
596
  and index.this
642
597
  ):
598
+ if isinstance(expression.parent, exp.Cast) and expression.parent.to.this == exp.DataType.Type.VARCHAR:
599
+ # If the parent is a cast to varchar, we need to use JSONExtractScalar
600
+ # to get the unquoted string value.
601
+ klass = exp.JSONExtractScalar
602
+ else:
603
+ klass = exp.JSONExtract
643
604
  if index.is_string:
644
- return exp.JSONExtract(this=expression.this, expression=exp.Literal(this=f"$.{index.this}", is_string=True))
605
+ return klass(this=expression.this, expression=exp.Literal(this=f"$.{index.this}", is_string=True))
645
606
  else:
646
- return exp.JSONExtract(
647
- this=expression.this, expression=exp.Literal(this=f"$[{index.this}]", is_string=True)
648
- )
607
+ return klass(this=expression.this, expression=exp.Literal(this=f"$[{index.this}]", is_string=True))
649
608
 
650
609
  return expression
651
610
 
@@ -1005,209 +964,6 @@ def set_schema(expression: exp.Expression, current_database: str | None) -> exp.
1005
964
  return expression
1006
965
 
1007
966
 
1008
- def show_objects_tables(expression: exp.Expression, current_database: str | None = None) -> exp.Expression:
1009
- """Transform SHOW OBJECTS/TABLES to a query against the information_schema.tables table.
1010
-
1011
- See https://docs.snowflake.com/en/sql-reference/sql/show-objects
1012
- https://docs.snowflake.com/en/sql-reference/sql/show-tables
1013
- """
1014
- if not (
1015
- isinstance(expression, exp.Show)
1016
- and isinstance(expression.this, str)
1017
- and (show := expression.this.upper())
1018
- and show in {"OBJECTS", "TABLES"}
1019
- ):
1020
- return expression
1021
-
1022
- scope_kind = expression.args.get("scope_kind")
1023
- table = expression.find(exp.Table)
1024
-
1025
- if scope_kind == "DATABASE":
1026
- catalog = (table and table.name) or current_database
1027
- schema = None
1028
- elif scope_kind == "SCHEMA" and table:
1029
- catalog = table.db or current_database
1030
- schema = table.name
1031
- else:
1032
- # all objects / tables - will show everything in the "account"
1033
- catalog = None
1034
- schema = None
1035
-
1036
- columns = [
1037
- "to_timestamp(0)::timestamptz as 'created_on'",
1038
- "table_name as 'name'",
1039
- "case when table_type='BASE TABLE' then 'TABLE' else table_type end as 'kind'",
1040
- "table_catalog as 'database_name'",
1041
- "table_schema as 'schema_name'",
1042
- ]
1043
- if not expression.args["terse"]:
1044
- columns.append('null as "comment"')
1045
- columns_clause = ", ".join(columns)
1046
-
1047
- where = ["not (table_schema == '_fs_information_schema')"] # exclude fakesnow's internal schemas
1048
- if show == "TABLES":
1049
- where.append("table_type = 'BASE TABLE'")
1050
- if catalog:
1051
- where.append(f"table_catalog = '{catalog}'")
1052
- if schema:
1053
- where.append(f"table_schema = '{schema}'")
1054
- if (like := expression.args.get("like")) and isinstance(like, exp.Expression):
1055
- where.append(f"table_name ilike {like.sql()}")
1056
- where_clause = " AND ".join(where)
1057
-
1058
- limit = limit.sql() if (limit := expression.args.get("limit")) and isinstance(limit, exp.Expression) else ""
1059
-
1060
- query = f"""
1061
- SELECT {columns_clause}
1062
- from information_schema.tables
1063
- where {where_clause}
1064
- {limit}
1065
- """
1066
-
1067
- return sqlglot.parse_one(query, read="duckdb")
1068
-
1069
-
1070
- SQL_SHOW_SCHEMAS = """
1071
- select
1072
- to_timestamp(0)::timestamptz as 'created_on',
1073
- case
1074
- when schema_name = '_fs_information_schema' then 'information_schema'
1075
- else schema_name
1076
- end as 'name',
1077
- NULL as 'kind',
1078
- catalog_name as 'database_name',
1079
- NULL as 'schema_name'
1080
- from information_schema.schemata
1081
- where not catalog_name in ('memory', 'system', 'temp', '_fs_global')
1082
- and not schema_name in ('main', 'pg_catalog')
1083
- """
1084
-
1085
-
1086
- def show_schemas(expression: exp.Expression, current_database: str | None = None) -> exp.Expression:
1087
- """Transform SHOW SCHEMAS to a query against the information_schema.schemata table.
1088
-
1089
- See https://docs.snowflake.com/en/sql-reference/sql/show-schemas
1090
- """
1091
- if isinstance(expression, exp.Show) and isinstance(expression.this, str) and expression.this.upper() == "SCHEMAS":
1092
- if (ident := expression.find(exp.Identifier)) and isinstance(ident.this, str):
1093
- database = ident.this
1094
- else:
1095
- database = current_database
1096
-
1097
- return sqlglot.parse_one(
1098
- f"{SQL_SHOW_SCHEMAS} and catalog_name = '{database}'" if database else SQL_SHOW_SCHEMAS, read="duckdb"
1099
- )
1100
-
1101
- return expression
1102
-
1103
-
1104
- SQL_SHOW_DATABASES = """
1105
- SELECT
1106
- to_timestamp(0)::timestamptz as 'created_on',
1107
- database_name as 'name',
1108
- 'N' as 'is_default',
1109
- 'N' as 'is_current',
1110
- '' as 'origin',
1111
- 'SYSADMIN' as 'owner',
1112
- comment,
1113
- '' as 'options',
1114
- 1 as 'retention_time',
1115
- 'STANDARD' as 'kind',
1116
- NULL as 'budget',
1117
- 'ROLE' as 'owner_role_type',
1118
- NULL as 'object_visibility'
1119
- FROM duckdb_databases
1120
- WHERE database_name NOT IN ('memory', '_fs_global')
1121
- """
1122
-
1123
-
1124
- def show_databases(expression: exp.Expression) -> exp.Expression:
1125
- """Transform SHOW DATABASES to a query against the information_schema.schemata table.
1126
-
1127
- See https://docs.snowflake.com/en/sql-reference/sql/show-databases
1128
- """
1129
- if isinstance(expression, exp.Show) and isinstance(expression.this, str) and expression.this.upper() == "DATABASES":
1130
- return sqlglot.parse_one(SQL_SHOW_DATABASES, read="duckdb")
1131
-
1132
- return expression
1133
-
1134
-
1135
- # returns zero rows
1136
- SQL_SHOW_FUNCTIONS = """
1137
- SELECT
1138
- '1970-01-01 00:00:00 UTC'::timestamptz as created_on,
1139
- 'SYSTIMESTAMP' as name,
1140
- '' as schema_name,
1141
- 'Y' as is_builtin,
1142
- 'N' as is_aggregate,
1143
- 'N' as is_ansi,
1144
- 0 as min_num_arguments,
1145
- 0 as max_num_arguments,
1146
- 'SYSTIMESTAMP() RETURN TIMESTAMP_LTZ' as arguments,
1147
- 'Returns the current timestamp' as description,
1148
- '' as catalog_name,
1149
- 'N' as is_table_function,
1150
- 'N' as valid_for_clustering,
1151
- NULL as is_secure,
1152
- '' as secrets,
1153
- '' as external_access_integrations,
1154
- 'N' as is_external_function,
1155
- 'SQL' as language,
1156
- 'N' as is_memoizable,
1157
- 'N' as is_data_metric
1158
- WHERE 0 = 1;
1159
- """
1160
-
1161
-
1162
- def show_functions(expression: exp.Expression) -> exp.Expression:
1163
- """Transform SHOW FUNCTIONS.
1164
-
1165
- See https://docs.snowflake.com/en/sql-reference/sql/show-functions
1166
- """
1167
- if isinstance(expression, exp.Show) and isinstance(expression.this, str) and expression.this.upper() == "FUNCTIONS":
1168
- return sqlglot.parse_one(SQL_SHOW_FUNCTIONS, read="duckdb")
1169
-
1170
- return expression
1171
-
1172
-
1173
- # returns zero rows
1174
- SQL_SHOW_PROCEDURES = """
1175
- SELECT
1176
- '2012-08-01 07:00:00 UTC'::timestamptz as 'created_on',
1177
- 'SYSTEM$CLASSIFY' as 'name',
1178
- '' as 'schema_name',
1179
- 'Y' as 'is_builtin',
1180
- 'N' as 'is_aggregate',
1181
- 'N' as 'is_ansi',
1182
- 2 as 'min_num_arguments',
1183
- 2 as 'max_num_arguments',
1184
- 'SYSTEM$CLASSIFY(VARCHAR, OBJECT) RETURN OBJECT' as 'arguments',
1185
- 'classify stored proc' as 'description',
1186
- '' as 'catalog_name',
1187
- 'N' as 'is_table_function',
1188
- 'N' as 'valid_for_clustering',
1189
- NULL as 'is_secure',
1190
- '' as 'secrets',
1191
- '' as 'external_access_integrations',
1192
- WHERE 0 = 1;
1193
- """
1194
-
1195
-
1196
- def show_procedures(expression: exp.Expression) -> exp.Expression:
1197
- """Transform SHOW PROCEDURES.
1198
-
1199
- See https://docs.snowflake.com/en/sql-reference/sql/show-procedures
1200
- """
1201
- if (
1202
- isinstance(expression, exp.Show)
1203
- and isinstance(expression.this, str)
1204
- and expression.this.upper() == "PROCEDURES"
1205
- ):
1206
- return sqlglot.parse_one(SQL_SHOW_PROCEDURES, read="duckdb")
1207
-
1208
- return expression
1209
-
1210
-
1211
967
  def split(expression: exp.Expression) -> exp.Expression:
1212
968
  """
1213
969
  Convert output of duckdb str_split from varchar[] to JSON array to match Snowflake.
@@ -1544,17 +1300,6 @@ def values_columns(expression: exp.Expression) -> exp.Expression:
1544
1300
  return expression
1545
1301
 
1546
1302
 
1547
- def show_users(expression: exp.Expression) -> exp.Expression:
1548
- """Transform SHOW USERS to a query against the global database's information_schema._fs_users table.
1549
-
1550
- https://docs.snowflake.com/en/sql-reference/sql/show-users
1551
- """
1552
- if isinstance(expression, exp.Show) and isinstance(expression.this, str) and expression.this.upper() == "USERS":
1553
- return sqlglot.parse_one("SELECT * FROM _fs_global._fs_information_schema._fs_users_ext", read="duckdb")
1554
-
1555
- return expression
1556
-
1557
-
1558
1303
  def create_user(expression: exp.Expression) -> exp.Expression:
1559
1304
  """Transform CREATE USER to a query against the global database's information_schema._fs_users table.
1560
1305
 
@@ -1575,93 +1320,6 @@ def create_user(expression: exp.Expression) -> exp.Expression:
1575
1320
  return expression
1576
1321
 
1577
1322
 
1578
- def show_keys(
1579
- expression: exp.Expression,
1580
- current_database: str | None = None,
1581
- *,
1582
- kind: Literal["PRIMARY", "UNIQUE", "FOREIGN"],
1583
- ) -> exp.Expression:
1584
- """Transform SHOW <kind> KEYS to a query against the duckdb_constraints meta-table.
1585
-
1586
- https://docs.snowflake.com/en/sql-reference/sql/show-primary-keys
1587
- """
1588
- snowflake_kind = kind
1589
- if kind == "FOREIGN":
1590
- snowflake_kind = "IMPORTED"
1591
-
1592
- if (
1593
- isinstance(expression, exp.Show)
1594
- and isinstance(expression.this, str)
1595
- and expression.this.upper() == f"{snowflake_kind} KEYS"
1596
- ):
1597
- if kind == "FOREIGN":
1598
- statement = f"""
1599
- SELECT
1600
- to_timestamp(0)::timestamptz as created_on,
1601
-
1602
- '' as pk_database_name,
1603
- '' as pk_schema_name,
1604
- '' as pk_table_name,
1605
- '' as pk_column_name,
1606
- unnest(constraint_column_names) as pk_column_name,
1607
-
1608
- database_name as fk_database_name,
1609
- schema_name as fk_schema_name,
1610
- table_name as fk_table_name,
1611
- unnest(constraint_column_names) as fk_column_name,
1612
- 1 as key_sequence,
1613
- 'NO ACTION' as update_rule,
1614
- 'NO ACTION' as delete_rule,
1615
- LOWER(CONCAT(database_name, '_', schema_name, '_', table_name, '_pkey')) AS fk_name,
1616
- LOWER(CONCAT(database_name, '_', schema_name, '_', table_name, '_pkey')) AS pk_name,
1617
- 'NOT DEFERRABLE' as deferrability,
1618
- 'false' as rely,
1619
- null as "comment"
1620
- FROM duckdb_constraints
1621
- WHERE constraint_type = 'PRIMARY KEY'
1622
- AND database_name = '{current_database}'
1623
- AND table_name NOT LIKE '_fs_%'
1624
- """
1625
- else:
1626
- statement = f"""
1627
- SELECT
1628
- to_timestamp(0)::timestamptz as created_on,
1629
- database_name as database_name,
1630
- schema_name as schema_name,
1631
- table_name as table_name,
1632
- unnest(constraint_column_names) as column_name,
1633
- 1 as key_sequence,
1634
- LOWER(CONCAT(database_name, '_', schema_name, '_', table_name, '_pkey')) AS constraint_name,
1635
- 'false' as rely,
1636
- null as "comment"
1637
- FROM duckdb_constraints
1638
- WHERE constraint_type = '{kind} KEY'
1639
- AND database_name = '{current_database}'
1640
- AND table_name NOT LIKE '_fs_%'
1641
- """
1642
-
1643
- if scope_kind := expression.args.get("scope_kind"):
1644
- table = expression.args["scope"]
1645
-
1646
- if scope_kind == "SCHEMA":
1647
- db = table and table.db
1648
- schema = table and table.name
1649
- if db:
1650
- statement += f"AND database_name = '{db}' "
1651
-
1652
- if schema:
1653
- statement += f"AND schema_name = '{schema}' "
1654
- elif scope_kind == "TABLE":
1655
- if not table:
1656
- raise ValueError(f"SHOW PRIMARY KEYS with {scope_kind} scope requires a table")
1657
-
1658
- statement += f"AND table_name = '{table.name}' "
1659
- else:
1660
- raise NotImplementedError(f"SHOW PRIMARY KEYS with {scope_kind} not yet supported")
1661
- return sqlglot.parse_one(statement)
1662
- return expression
1663
-
1664
-
1665
1323
  def update_variables(
1666
1324
  expression: exp.Expression,
1667
1325
  variables: Variables,
@@ -0,0 +1,476 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Literal
4
+
5
+ import sqlglot
6
+ from sqlglot import exp
7
+
8
+
9
+ def show_columns(
10
+ expression: exp.Expression, current_database: str | None = None, current_schema: str | None = None
11
+ ) -> exp.Expression:
12
+ """Transform SHOW COLUMNS to a query against the fs global information_schema columns table.
13
+
14
+ See https://docs.snowflake.com/en/sql-reference/sql/show-columns
15
+ """
16
+ if not (
17
+ isinstance(expression, exp.Show) and isinstance(expression.this, str) and expression.this.upper() == "COLUMNS"
18
+ ):
19
+ return expression
20
+
21
+ scope_kind = expression.args.get("scope_kind")
22
+ table = expression.find(exp.Table)
23
+
24
+ if scope_kind == "ACCOUNT" or not scope_kind:
25
+ # all columns
26
+ catalog = None
27
+ schema = None
28
+ table = None
29
+ elif scope_kind == "DATABASE" and table:
30
+ catalog = table.name
31
+ schema = None
32
+ table = None
33
+ elif scope_kind == "SCHEMA" and table:
34
+ catalog = table.db or current_database
35
+ schema = table.name
36
+ table = None
37
+ elif scope_kind in ("TABLE", "VIEW") and table:
38
+ catalog = table.catalog or current_database
39
+ schema = table.db or current_schema
40
+ table = table.name
41
+ else:
42
+ raise NotImplementedError(f"show_object_columns: {expression.sql(dialect='snowflake')}")
43
+
44
+ query = f"""
45
+ SELECT
46
+ table_name,
47
+ table_schema as "schema_name",
48
+ column_name,
49
+ CASE
50
+ WHEN data_type = 'NUMBER' THEN '{{"type":"FIXED","precision":'|| numeric_precision || ',"scale":' || numeric_scale || ',"nullable":true}}'
51
+ WHEN data_type = 'TEXT' THEN '{{"type":"TEXT","length":' || coalesce(character_maximum_length,16777216) || ',"byteLength":' || CASE WHEN character_maximum_length = 16777216 THEN 16777216 ELSE coalesce(character_maximum_length*4,16777216) END || ',"nullable":true,"fixed":false}}'
52
+ WHEN data_type in ('TIMESTAMP_NTZ','TIMESTAMP_TZ','TIME') THEN '{{"type":"' || data_type || '","precision":0,"scale":9,"nullable":true}}'
53
+ WHEN data_type = 'FLOAT' THEN '{{"type":"REAL","nullable":true}}'
54
+ WHEN data_type = 'BINARY' THEN '{{"type":"BINARY","length":8388608,"byteLength":8388608,"nullable":true,"fixed":true}}'
55
+ ELSE '{{"type":"' || data_type || '","nullable":true}}'
56
+ END as "data_type",
57
+ CASE WHEN is_nullable = 'YES' THEN 'true' ELSE 'false' END as "null?",
58
+ COALESCE(column_default, '') as "default",
59
+ 'COLUMN' as "kind",
60
+ '' as "expression",
61
+ COALESCE(comment, '') as "comment",
62
+ table_catalog as "database_name",
63
+ '' as "autoincrement",
64
+ NULL as "schema_evolution_record"
65
+ FROM _fs_global._fs_information_schema._fs_columns c
66
+ WHERE 1=1
67
+ {f"AND table_catalog = '{catalog}'" if catalog else ""}
68
+ {f"AND table_schema = '{schema}'" if schema else ""}
69
+ {f"AND table_name = '{table}'" if table else ""}
70
+ ORDER BY table_name, ordinal_position
71
+ """ # noqa: E501
72
+
73
+ return sqlglot.parse_one(query, read="duckdb")
74
+
75
+
76
+ SQL_SHOW_DATABASES = """
77
+ SELECT
78
+ to_timestamp(0)::timestamptz as 'created_on',
79
+ database_name as 'name',
80
+ 'N' as 'is_default',
81
+ 'N' as 'is_current',
82
+ '' as 'origin',
83
+ 'SYSADMIN' as 'owner',
84
+ comment,
85
+ '' as 'options',
86
+ 1 as 'retention_time',
87
+ 'STANDARD' as 'kind',
88
+ NULL as 'budget',
89
+ 'ROLE' as 'owner_role_type',
90
+ NULL as 'object_visibility'
91
+ FROM duckdb_databases
92
+ WHERE database_name NOT IN ('memory', '_fs_global')
93
+ """
94
+
95
+
96
+ def show_databases(expression: exp.Expression) -> exp.Expression:
97
+ """Transform SHOW DATABASES to a query against the information_schema.schemata table.
98
+
99
+ See https://docs.snowflake.com/en/sql-reference/sql/show-databases
100
+ """
101
+ if isinstance(expression, exp.Show) and isinstance(expression.this, str) and expression.this.upper() == "DATABASES":
102
+ return sqlglot.parse_one(SQL_SHOW_DATABASES, read="duckdb")
103
+
104
+ return expression
105
+
106
+
107
+ # returns zero rows
108
+ SQL_SHOW_FUNCTIONS = """
109
+ SELECT
110
+ '1970-01-01 00:00:00 UTC'::timestamptz as created_on,
111
+ 'SYSTIMESTAMP' as name,
112
+ '' as schema_name,
113
+ 'Y' as is_builtin,
114
+ 'N' as is_aggregate,
115
+ 'N' as is_ansi,
116
+ 0 as min_num_arguments,
117
+ 0 as max_num_arguments,
118
+ 'SYSTIMESTAMP() RETURN TIMESTAMP_LTZ' as arguments,
119
+ 'Returns the current timestamp' as description,
120
+ '' as catalog_name,
121
+ 'N' as is_table_function,
122
+ 'N' as valid_for_clustering,
123
+ NULL as is_secure,
124
+ '' as secrets,
125
+ '' as external_access_integrations,
126
+ 'N' as is_external_function,
127
+ 'SQL' as language,
128
+ 'N' as is_memoizable,
129
+ 'N' as is_data_metric
130
+ WHERE 0 = 1;
131
+ """
132
+
133
+
134
+ def show_functions(expression: exp.Expression) -> exp.Expression:
135
+ """Transform SHOW FUNCTIONS.
136
+
137
+ See https://docs.snowflake.com/en/sql-reference/sql/show-functions
138
+ """
139
+ if isinstance(expression, exp.Show) and isinstance(expression.this, str) and expression.this.upper() == "FUNCTIONS":
140
+ return sqlglot.parse_one(SQL_SHOW_FUNCTIONS, read="duckdb")
141
+
142
+ return expression
143
+
144
+
145
+ # returns zero rows
146
+ SQL_SHOW_WAREHOUSES = """
147
+ SELECT
148
+ 'FAKESNOW_WAREHOUSE' as name,
149
+ 'STARTED' as state,
150
+ 'STANDARD' as type,
151
+ 'X-Small' as size,
152
+ 1 as min_cluster_count,
153
+ 1 as max_cluster_count,
154
+ 1 as started_clusters,
155
+ 0 as running,
156
+ 0 as queued,
157
+ 'N' as is_default,
158
+ 'N' as is_current,
159
+ 600 as auto_suspend,
160
+ 'true' as auto_resume,
161
+ -- nb: deliberate space before '100' to match Snowflake's output
162
+ ' 100' as available,
163
+ '0' as provisioning,
164
+ '0' as quiescing,
165
+ '0' as other,
166
+ '1970-01-01 00:00:00.000000 UTC'::timestamptz as created_on,
167
+ '1970-01-01 00:00:00.000000 UTC'::timestamptz as resumed_on,
168
+ '1970-01-01 00:00:00.000000 UTC'::timestamptz as updated_on,
169
+ 'SYSADMIN' as owner,
170
+ '' as comment,
171
+ 'false' as enable_query_acceleration,
172
+ 8 as query_acceleration_max_scale_factor,
173
+ 'null' as resource_monitor,
174
+
175
+ -- deprecated - these 5 cols are for internal use
176
+ 0 as actives,
177
+ 0 as pendings,
178
+ 0 as failed,
179
+ 0 as suspended,
180
+ '123456789012' as uuid,
181
+
182
+ 'STANDARD' as scaling_policy,
183
+ NULL as budget,
184
+ 'ROLE' as owner_role_type,
185
+ NULL as resource_constraint;
186
+ """
187
+
188
+
189
+ def show_warehouses(expression: exp.Expression) -> exp.Expression:
190
+ """Transform SHOW WAREHOUSES.
191
+
192
+ See https://docs.snowflake.com/en/sql-reference/sql/show-warehouses
193
+ """
194
+ if (
195
+ isinstance(expression, exp.Show)
196
+ and isinstance(expression.this, str)
197
+ and expression.this.upper() == "WAREHOUSES"
198
+ ):
199
+ return sqlglot.parse_one(SQL_SHOW_WAREHOUSES, read="duckdb")
200
+
201
+ return expression
202
+
203
+
204
+ def show_keys(
205
+ expression: exp.Expression,
206
+ current_database: str | None = None,
207
+ *,
208
+ kind: Literal["PRIMARY", "UNIQUE", "FOREIGN"],
209
+ ) -> exp.Expression:
210
+ """Transform SHOW <kind> KEYS to a query against the duckdb_constraints meta-table.
211
+
212
+ https://docs.snowflake.com/en/sql-reference/sql/show-primary-keys
213
+ """
214
+ snowflake_kind = kind
215
+ if kind == "FOREIGN":
216
+ snowflake_kind = "IMPORTED"
217
+
218
+ if (
219
+ isinstance(expression, exp.Show)
220
+ and isinstance(expression.this, str)
221
+ and expression.this.upper() == f"{snowflake_kind} KEYS"
222
+ ):
223
+ if kind == "FOREIGN":
224
+ statement = f"""
225
+ SELECT
226
+ to_timestamp(0)::timestamptz as created_on,
227
+
228
+ '' as pk_database_name,
229
+ '' as pk_schema_name,
230
+ '' as pk_table_name,
231
+ '' as pk_column_name,
232
+ unnest(constraint_column_names) as pk_column_name,
233
+
234
+ database_name as fk_database_name,
235
+ schema_name as fk_schema_name,
236
+ table_name as fk_table_name,
237
+ unnest(constraint_column_names) as fk_column_name,
238
+ 1 as key_sequence,
239
+ 'NO ACTION' as update_rule,
240
+ 'NO ACTION' as delete_rule,
241
+ LOWER(CONCAT(database_name, '_', schema_name, '_', table_name, '_pkey')) AS fk_name,
242
+ LOWER(CONCAT(database_name, '_', schema_name, '_', table_name, '_pkey')) AS pk_name,
243
+ 'NOT DEFERRABLE' as deferrability,
244
+ 'false' as rely,
245
+ null as "comment"
246
+ FROM duckdb_constraints
247
+ WHERE constraint_type = 'PRIMARY KEY'
248
+ AND database_name = '{current_database}'
249
+ AND table_name NOT LIKE '_fs_%'
250
+ """
251
+ else:
252
+ statement = f"""
253
+ SELECT
254
+ to_timestamp(0)::timestamptz as created_on,
255
+ database_name as database_name,
256
+ schema_name as schema_name,
257
+ table_name as table_name,
258
+ unnest(constraint_column_names) as column_name,
259
+ 1 as key_sequence,
260
+ LOWER(CONCAT(database_name, '_', schema_name, '_', table_name, '_pkey')) AS constraint_name,
261
+ 'false' as rely,
262
+ null as "comment"
263
+ FROM duckdb_constraints
264
+ WHERE constraint_type = '{kind} KEY'
265
+ AND database_name = '{current_database}'
266
+ AND table_name NOT LIKE '_fs_%'
267
+ """
268
+
269
+ if scope_kind := expression.args.get("scope_kind"):
270
+ table = expression.args["scope"]
271
+
272
+ if scope_kind == "SCHEMA":
273
+ db = table and table.db
274
+ schema = table and table.name
275
+ if db:
276
+ statement += f"AND database_name = '{db}' "
277
+
278
+ if schema:
279
+ statement += f"AND schema_name = '{schema}' "
280
+ elif scope_kind == "TABLE":
281
+ if not table:
282
+ raise ValueError(f"SHOW PRIMARY KEYS with {scope_kind} scope requires a table")
283
+
284
+ statement += f"AND table_name = '{table.name}' "
285
+ else:
286
+ raise NotImplementedError(f"SHOW PRIMARY KEYS with {scope_kind} not yet supported")
287
+ return sqlglot.parse_one(statement)
288
+ return expression
289
+
290
+
291
+ def show_objects_tables(expression: exp.Expression, current_database: str | None = None) -> exp.Expression:
292
+ """Transform SHOW OBJECTS/TABLES to a query against the information_schema.tables table.
293
+
294
+ See https://docs.snowflake.com/en/sql-reference/sql/show-objects
295
+ https://docs.snowflake.com/en/sql-reference/sql/show-tables
296
+ """
297
+ if not (
298
+ isinstance(expression, exp.Show)
299
+ and isinstance(expression.this, str)
300
+ and (show := expression.this.upper())
301
+ and show in {"OBJECTS", "TABLES"}
302
+ ):
303
+ return expression
304
+
305
+ scope_kind = expression.args.get("scope_kind")
306
+ table = expression.find(exp.Table)
307
+
308
+ if scope_kind == "DATABASE":
309
+ catalog = (table and table.name) or current_database
310
+ schema = None
311
+ elif scope_kind == "SCHEMA" and table:
312
+ catalog = table.db or current_database
313
+ schema = table.name
314
+ else:
315
+ # all objects / tables - will show everything in the "account"
316
+ catalog = None
317
+ schema = None
318
+
319
+ columns = [
320
+ "to_timestamp(0)::timestamptz as 'created_on'",
321
+ "table_name as 'name'",
322
+ "case when table_type='BASE TABLE' then 'TABLE' else table_type end as 'kind'",
323
+ "table_catalog as 'database_name'",
324
+ "table_schema as 'schema_name'",
325
+ ]
326
+ if not expression.args["terse"]:
327
+ if show == "OBJECTS":
328
+ columns.extend(
329
+ [
330
+ "'' as 'comment'",
331
+ "'' as 'cluster_by'",
332
+ # TODO: implement rows and bytes as rows * 1024
333
+ "0 as 'rows'",
334
+ "0 as 'bytes'",
335
+ "'SYSADMIN' as 'owner'",
336
+ "1 as 'retention_time'",
337
+ "'ROLE' as 'owner_role_type'",
338
+ "null as 'budget'",
339
+ "'N' as 'is_hybrid'",
340
+ "'N' as 'is_dynamic'",
341
+ ]
342
+ )
343
+ else:
344
+ # show == "TABLES"
345
+ columns.extend(
346
+ [
347
+ "'' as 'comment'",
348
+ "'' as 'cluster_by'",
349
+ # TODO: implement rows and bytes as rows * 1024
350
+ "0 as 'rows'",
351
+ "0 as 'bytes'",
352
+ "'SYSADMIN' as 'owner'",
353
+ "1 as 'retention_time'",
354
+ "'OFF' as 'automatic_clustering'",
355
+ "'OFF' as 'change_tracking'",
356
+ "'OFF' as 'search_optimization'",
357
+ "null as 'search_optimization_progress'",
358
+ "null as 'search_optimization_bytes'",
359
+ "'N' as 'is_external'",
360
+ "'N' as 'enable_schema_evolution'",
361
+ "'ROLE' as 'owner_role_type'",
362
+ "'N' as 'is_event'",
363
+ "null as 'budget'",
364
+ "'N' as 'is_hybrid'",
365
+ "'N' as 'is_iceberg'",
366
+ "'N' as 'is_dynamic'",
367
+ "'N' as 'is_immutable'",
368
+ ]
369
+ )
370
+
371
+ columns_clause = ", ".join(columns)
372
+
373
+ where = ["not (table_schema == '_fs_information_schema')"] # exclude fakesnow's internal schemas
374
+ if show == "TABLES":
375
+ where.append("table_type = 'BASE TABLE'")
376
+ if catalog:
377
+ where.append(f"table_catalog = '{catalog}'")
378
+ if schema:
379
+ where.append(f"table_schema = '{schema}'")
380
+ if (like := expression.args.get("like")) and isinstance(like, exp.Expression):
381
+ where.append(f"table_name ilike {like.sql()}")
382
+ where_clause = " AND ".join(where)
383
+
384
+ limit = limit.sql() if (limit := expression.args.get("limit")) and isinstance(limit, exp.Expression) else ""
385
+
386
+ query = f"""
387
+ SELECT {columns_clause}
388
+ from information_schema.tables
389
+ where {where_clause}
390
+ {limit}
391
+ """
392
+
393
+ return sqlglot.parse_one(query, read="duckdb")
394
+
395
+
396
+ # returns zero rows
397
+ SQL_SHOW_PROCEDURES = """
398
+ SELECT
399
+ '2012-08-01 07:00:00 UTC'::timestamptz as 'created_on',
400
+ 'SYSTEM$CLASSIFY' as 'name',
401
+ '' as 'schema_name',
402
+ 'Y' as 'is_builtin',
403
+ 'N' as 'is_aggregate',
404
+ 'N' as 'is_ansi',
405
+ 2 as 'min_num_arguments',
406
+ 2 as 'max_num_arguments',
407
+ 'SYSTEM$CLASSIFY(VARCHAR, OBJECT) RETURN OBJECT' as 'arguments',
408
+ 'classify stored proc' as 'description',
409
+ '' as 'catalog_name',
410
+ 'N' as 'is_table_function',
411
+ 'N' as 'valid_for_clustering',
412
+ NULL as 'is_secure',
413
+ '' as 'secrets',
414
+ '' as 'external_access_integrations',
415
+ WHERE 0 = 1;
416
+ """
417
+
418
+
419
+ def show_procedures(expression: exp.Expression) -> exp.Expression:
420
+ """Transform SHOW PROCEDURES.
421
+
422
+ See https://docs.snowflake.com/en/sql-reference/sql/show-procedures
423
+ """
424
+ if (
425
+ isinstance(expression, exp.Show)
426
+ and isinstance(expression.this, str)
427
+ and expression.this.upper() == "PROCEDURES"
428
+ ):
429
+ return sqlglot.parse_one(SQL_SHOW_PROCEDURES, read="duckdb")
430
+
431
+ return expression
432
+
433
+
434
+ SQL_SHOW_SCHEMAS = """
435
+ select
436
+ to_timestamp(0)::timestamptz as 'created_on',
437
+ case
438
+ when schema_name = '_fs_information_schema' then 'information_schema'
439
+ else schema_name
440
+ end as 'name',
441
+ NULL as 'kind',
442
+ catalog_name as 'database_name',
443
+ NULL as 'schema_name'
444
+ from information_schema.schemata
445
+ where not catalog_name in ('memory', 'system', 'temp', '_fs_global')
446
+ and not schema_name in ('main', 'pg_catalog')
447
+ """
448
+
449
+
450
+ def show_schemas(expression: exp.Expression, current_database: str | None = None) -> exp.Expression:
451
+ """Transform SHOW SCHEMAS to a query against the information_schema.schemata table.
452
+
453
+ See https://docs.snowflake.com/en/sql-reference/sql/show-schemas
454
+ """
455
+ if isinstance(expression, exp.Show) and isinstance(expression.this, str) and expression.this.upper() == "SCHEMAS":
456
+ if (ident := expression.find(exp.Identifier)) and isinstance(ident.this, str):
457
+ database = ident.this
458
+ else:
459
+ database = current_database
460
+
461
+ return sqlglot.parse_one(
462
+ f"{SQL_SHOW_SCHEMAS} and catalog_name = '{database}'" if database else SQL_SHOW_SCHEMAS, read="duckdb"
463
+ )
464
+
465
+ return expression
466
+
467
+
468
+ def show_users(expression: exp.Expression) -> exp.Expression:
469
+ """Transform SHOW USERS to a query against the global database's information_schema._fs_users table.
470
+
471
+ https://docs.snowflake.com/en/sql-reference/sql/show-users
472
+ """
473
+ if isinstance(expression, exp.Show) and isinstance(expression.this, str) and expression.this.upper() == "USERS":
474
+ return sqlglot.parse_one("SELECT * FROM _fs_global._fs_information_schema._fs_users_ext", read="duckdb")
475
+
476
+ return expression
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: fakesnow
3
- Version: 0.9.31
3
+ Version: 0.9.32
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
@@ -3,24 +3,26 @@ fakesnow/__main__.py,sha256=GDrGyNTvBFuqn_UfDjKs7b3LPtU6gDv1KwosVDrukIM,76
3
3
  fakesnow/arrow.py,sha256=XjTpFyLrD9jULWOtPgpr0RyNMmO6a5yi82y6ivi2CCI,4884
4
4
  fakesnow/checks.py,sha256=be-xo0oMoAUVhlMDCu1_Rkoh_L8p_p8qo9P6reJSHIQ,2874
5
5
  fakesnow/cli.py,sha256=9qfI-Ssr6mo8UmIlXkUAOz2z2YPBgDsrEVaZv9FjGFs,2201
6
- fakesnow/conn.py,sha256=HGhFKErKWvAfVEy3QSc0tfNmzGh_T7FtvRfWuDBy_CQ,5744
7
- fakesnow/cursor.py,sha256=tlcQK_w4r6BZHs3BTNHUFGk2hZz-lSnOdIrNLaJ-rBw,21349
6
+ fakesnow/conn.py,sha256=2WClMmUgfQkQA2hFQjfMP3R-85TbTbZh_8Y1tCdcerA,6053
7
+ fakesnow/cursor.py,sha256=Nvr8TQmmTFs6i0sJwfgCocrEF9td0D0SdDG41quIudI,21621
8
8
  fakesnow/expr.py,sha256=CAxuYIUkwI339DQIBzvFF0F-m1tcVGKEPA5rDTzmH9A,892
9
9
  fakesnow/fakes.py,sha256=JQTiUkkwPeQrJ8FDWhPFPK6pGwd_aR2oiOrNzCWznlM,187
10
10
  fakesnow/fixtures.py,sha256=G-NkVeruSQAJ7fvSS2fR2oysUn0Yra1pohHlOvacKEk,455
11
11
  fakesnow/info_schema.py,sha256=xDhGy07fpc8bcy_VTfh54UzwNIaB4ZhGmjgJeoiZ0hQ,8744
12
- fakesnow/instance.py,sha256=vbg4XiAjpdglEqOM7X_HvCOnE-6Bf67nTYeBfGVUSNU,1889
12
+ fakesnow/instance.py,sha256=VsFbhVfy6EAJdEKykgavJwkMtrig01NehorptT51Jh8,2020
13
13
  fakesnow/macros.py,sha256=pX1YJDnQOkFJSHYUjQ6ErEkYIKvFI6Ncz_au0vv1csA,265
14
14
  fakesnow/pandas_tools.py,sha256=wI203UQHC8JvDzxE_VjE1NeV4rThek2P-u52oTg2foo,3481
15
15
  fakesnow/py.typed,sha256=B-DLSjYBi7pkKjwxCSdpVj2J02wgfJr-E7B1wOUyxYU,80
16
16
  fakesnow/rowtype.py,sha256=QUp8EaXD5LT0Xv8BXk5ze4WseEn52xoJ6R05pJjs5mM,2729
17
- fakesnow/server.py,sha256=-jKyEVuD2TEr88jUSA1Lu86MAymel7LQAiNlytHqhTg,5934
17
+ fakesnow/server.py,sha256=oLnWJgcxwhPw4sNebJF3B9uxk28A-a-AI8Pyl_lz2_E,5986
18
18
  fakesnow/variables.py,sha256=WXyPnkeNwD08gy52yF66CVe2twiYC50tztNfgXV4q1k,3032
19
- fakesnow/transforms/__init__.py,sha256=a-WNNqIo6IhY9p_m0zfdre7eb6KDmz-dbyrNkhpsHQk,60506
19
+ fakesnow/transforms/__init__.py,sha256=gD8wPo9QprwHkTOEMQ0-IsXSNfUruU0kBJPjO0po-J4,49377
20
20
  fakesnow/transforms/merge.py,sha256=Pg7_rwbAT_vr1U4ocBofUSyqaK8_e3qdIz_2SDm2S3s,8320
21
- fakesnow-0.9.31.dist-info/LICENSE,sha256=kW-7NWIyaRMQiDpryfSmF2DObDZHGR1cJZ39s6B1Svg,11344
22
- fakesnow-0.9.31.dist-info/METADATA,sha256=RXWi0hLbvLkUtzDw0SHtcu-N3-Ebd1BC5K7JfoPnZ38,18106
23
- fakesnow-0.9.31.dist-info/WHEEL,sha256=beeZ86-EfXScwlR_HKu4SllMC9wUEj_8Z_4FJ3egI2w,91
24
- fakesnow-0.9.31.dist-info/entry_points.txt,sha256=2riAUgu928ZIHawtO8EsfrMEJhi-EH-z_Vq7Q44xKPM,47
25
- fakesnow-0.9.31.dist-info/top_level.txt,sha256=500evXI1IFX9so82cizGIEMHAb_dJNPaZvd2H9dcKTA,24
26
- fakesnow-0.9.31.dist-info/RECORD,,
21
+ fakesnow/transforms/show.py,sha256=2qfK3Fi0RLylqTnkwSVgv5JIorXYb1y0fnf5oErRZ2o,16839
22
+ tools/decode.py,sha256=kC5kUvLQxdCkMRsnH6BqCajlKxKeN77w6rwCKsY6gqU,1781
23
+ fakesnow-0.9.32.dist-info/LICENSE,sha256=kW-7NWIyaRMQiDpryfSmF2DObDZHGR1cJZ39s6B1Svg,11344
24
+ fakesnow-0.9.32.dist-info/METADATA,sha256=HTGvul9rrgbrcSK79gmq4KndGAga0be6WzaKSUc_3c4,18106
25
+ fakesnow-0.9.32.dist-info/WHEEL,sha256=beeZ86-EfXScwlR_HKu4SllMC9wUEj_8Z_4FJ3egI2w,91
26
+ fakesnow-0.9.32.dist-info/entry_points.txt,sha256=2riAUgu928ZIHawtO8EsfrMEJhi-EH-z_Vq7Q44xKPM,47
27
+ fakesnow-0.9.32.dist-info/top_level.txt,sha256=Yos7YveA3f03xVYuURqnBsfMV2DePXfu_yGcsj3pPzI,30
28
+ fakesnow-0.9.32.dist-info/RECORD,,
@@ -1,3 +1,4 @@
1
1
  dist
2
2
  fakesnow
3
3
  notebooks
4
+ tools
tools/decode.py ADDED
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env python
2
+ import base64
3
+ import sys
4
+
5
+ import pyarrow as pa
6
+
7
+
8
+ def dump_field_metadata(field: pa.Field, index: int) -> None:
9
+ """Dump metadata for a single field."""
10
+ print(f"Field {index}: {field.name}")
11
+ print(f" Type: {field.type}")
12
+ print(f" Nullable: {field.nullable}")
13
+ print(" Metadata:")
14
+ assert field.metadata
15
+ for key, value in field.metadata.items():
16
+ try:
17
+ print(f" {key.decode('utf-8')}: {value.decode('utf-8')}")
18
+ except UnicodeDecodeError: # noqa: PERF203
19
+ print(f" {key.decode('utf-8')}: <binary data>")
20
+ print()
21
+
22
+
23
+ def main() -> None:
24
+ if len(sys.argv) > 1:
25
+ print("Usage: python dump_rowset_metadata.py < base64_encoded_file")
26
+ print(" or: cat base64_encoded_file | python dump_rowset_metadata.py")
27
+ print()
28
+ print("Dump pyarrow metadata for a base64-encoded rowset.")
29
+ sys.exit(1)
30
+
31
+ # Read base64 input from stdin
32
+ rowset_b64 = sys.stdin.read().strip()
33
+
34
+ try:
35
+ # Decode base64
36
+ data = base64.b64decode(rowset_b64)
37
+
38
+ # Parse with PyArrow
39
+ reader = pa.ipc.open_stream(data)
40
+
41
+ except Exception as e:
42
+ full_class_name = f"{e.__module__}.{e.__class__.__name__}"
43
+ print(f"Error processing rowset: {full_class_name} {e}")
44
+ sys.exit(1)
45
+
46
+ # Get the first batch
47
+ batch = next(iter(reader))
48
+
49
+ print(f"Total fields: {batch.num_columns}")
50
+ print("=" * 50)
51
+
52
+ # Dump metadata for each field
53
+ for i, field in enumerate(batch.schema):
54
+ dump_field_metadata(field, i)
55
+
56
+ # Also print a sample of the array data
57
+ print(f" Batch data: {batch[i]}")
58
+ print(f" Batch data type: {type(batch[i])}")
59
+ print("=" * 50)
60
+
61
+
62
+ if __name__ == "__main__":
63
+ main()