sqlrooms 0.1.0__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.
Files changed (57) hide show
  1. sqlrooms/cli.py +576 -0
  2. sqlrooms/web/__init__.py +0 -0
  3. sqlrooms/web/db_bridge/__init__.py +28 -0
  4. sqlrooms/web/db_bridge/connectors/__init__.py +9 -0
  5. sqlrooms/web/db_bridge/connectors/base.py +59 -0
  6. sqlrooms/web/db_bridge/connectors/postgres.py +118 -0
  7. sqlrooms/web/db_bridge/connectors/snowflake.py +161 -0
  8. sqlrooms/web/db_bridge/factory.py +91 -0
  9. sqlrooms/web/db_bridge/registry.py +113 -0
  10. sqlrooms/web/db_bridge/types.py +29 -0
  11. sqlrooms/web/db_bridge/utils.py +45 -0
  12. sqlrooms/web/launcher.py +1215 -0
  13. sqlrooms/web/static/assets/AiSlice-x2gVCmwI.js +137 -0
  14. sqlrooms/web/static/assets/CommandSlice-DPSuuiIV.js +23 -0
  15. sqlrooms/web/static/assets/DockLayout-DhgcIQET.js +1 -0
  16. sqlrooms/web/static/assets/GridLayout-CBVgs-6H.css +1 -0
  17. sqlrooms/web/static/assets/GridLayout-fXJZYHbE.js +253 -0
  18. sqlrooms/web/static/assets/LayoutRendererContext-BKO2wB-W.js +1 -0
  19. sqlrooms/web/static/assets/LeafLayout-DPFHUP6B.js +1 -0
  20. sqlrooms/web/static/assets/LeafLayout-ekhNDEEg.js +1 -0
  21. sqlrooms/web/static/assets/RenderNodeContext-BdrX8FaE.js +1 -0
  22. sqlrooms/web/static/assets/RendererSwitcher-DnVbhqg4.js +1 -0
  23. sqlrooms/web/static/assets/SplitLayout-fPLAPJN-.js +1 -0
  24. sqlrooms/web/static/assets/TabsLayout-C0N-7wmx.js +1 -0
  25. sqlrooms/web/static/assets/TabsLayout-T3iApyr5.js +41 -0
  26. sqlrooms/web/static/assets/chunk-jRWAZmH_.js +1 -0
  27. sqlrooms/web/static/assets/codicon-ngg6Pgfi.ttf +0 -0
  28. sqlrooms/web/static/assets/core.esm-DdCldPzV.js +5 -0
  29. sqlrooms/web/static/assets/css.worker-Wv5dxAWO.js +89 -0
  30. sqlrooms/web/static/assets/devtools-BNUn8Jb2.js +2 -0
  31. sqlrooms/web/static/assets/dist-dwKeDPoe.js +1 -0
  32. sqlrooms/web/static/assets/html.worker-CQP8QQsS.js +502 -0
  33. sqlrooms/web/static/assets/index-D9UP9D4f.js +316286 -0
  34. sqlrooms/web/static/assets/index-DioDnqnf.css +1 -0
  35. sqlrooms/web/static/assets/json.worker-DzV-CpCQ.js +58 -0
  36. sqlrooms/web/static/assets/loro_wasm_bg-DP4dC0x3.wasm +0 -0
  37. sqlrooms/web/static/assets/loro_wasm_bg-VQ4j4Qa9.js +9 -0
  38. sqlrooms/web/static/assets/loro_wasm_bg-oL0xMWtE.js +3630 -0
  39. sqlrooms/web/static/assets/maplibre-gl-C-a91wbz.js +748 -0
  40. sqlrooms/web/static/assets/node-sql-parser-ChfKIXD7.js +68 -0
  41. sqlrooms/web/static/assets/prop-types-DybOnnvg.js +1 -0
  42. sqlrooms/web/static/assets/react-dom-liMHu8hH.js +1 -0
  43. sqlrooms/web/static/assets/resizable-DYr7VLR3.js +1 -0
  44. sqlrooms/web/static/assets/scroll-area-ZmzNHGEm.js +1 -0
  45. sqlrooms/web/static/assets/tooltip-mgpsA9tW.js +1 -0
  46. sqlrooms/web/static/assets/ts.worker-Dth06zuC.js +67734 -0
  47. sqlrooms/web/static/assets/utils-yJ4l7ARz.js +1 -0
  48. sqlrooms/web/static/assets/webgl-device-CgQl7NRd.js +1 -0
  49. sqlrooms/web/static/assets/webgl-device-CtgDFnYR.js +13 -0
  50. sqlrooms/web/static/index.html +32 -0
  51. sqlrooms/web/static/logo.png +0 -0
  52. sqlrooms/web/ui.py +37 -0
  53. sqlrooms-0.1.0.dist-info/METADATA +274 -0
  54. sqlrooms-0.1.0.dist-info/RECORD +57 -0
  55. sqlrooms-0.1.0.dist-info/WHEEL +4 -0
  56. sqlrooms-0.1.0.dist-info/entry_points.txt +2 -0
  57. sqlrooms-0.1.0.dist-info/licenses/LICENSE +9 -0
@@ -0,0 +1,118 @@
1
+ from __future__ import annotations
2
+
3
+ import importlib.util
4
+ from dataclasses import dataclass
5
+ from typing import Any
6
+ from urllib.parse import quote
7
+
8
+ from .base import BaseSqlBridgeConnector
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class PostgresConnectorSettings:
13
+ connection_id: str = "postgres-default"
14
+ title: str = "Postgres"
15
+ host: str = "localhost"
16
+ port: str = "5432"
17
+ database: str = ""
18
+ user: str = ""
19
+ password: str | None = None
20
+
21
+ def resolve_dsn(self) -> str:
22
+ pw = f":{quote(self.password, safe='')}" if self.password else ""
23
+ return f"postgresql://{quote(self.user, safe='')}{pw}@{self.host}:{self.port}/{self.database}"
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class PostgresBridgeConnector(BaseSqlBridgeConnector):
28
+ settings: PostgresConnectorSettings
29
+ engine_id: str = "postgres"
30
+
31
+ @property
32
+ def connection_id(self) -> str:
33
+ return self.settings.connection_id
34
+
35
+ @property
36
+ def title(self) -> str:
37
+ return self.settings.title
38
+
39
+ def config_dict(self) -> dict[str, Any]:
40
+ s = self.settings
41
+ d: dict[str, Any] = {}
42
+ for k in ("host", "port", "database", "user", "password"):
43
+ v = getattr(s, k)
44
+ if v:
45
+ d[k] = v
46
+ return d
47
+
48
+ def _connect(self):
49
+ try:
50
+ import psycopg # type: ignore
51
+ except ImportError as exc:
52
+ raise RuntimeError(
53
+ "Postgres bridge requires `psycopg`. Install it to enable Postgres."
54
+ ) from exc
55
+ return psycopg.connect(self.settings.resolve_dsn())
56
+
57
+ def dependency_diagnostics(self) -> dict[str, Any]:
58
+ available = importlib.util.find_spec("psycopg") is not None
59
+ if available:
60
+ return {"available": True}
61
+ return {
62
+ "available": False,
63
+ "error": "Missing Python dependency: psycopg",
64
+ "requiredPackages": ["psycopg[binary]>=3.2.0"],
65
+ "installCommands": {
66
+ "uvProject": "uv sync --extra postgres",
67
+ "uvxRelaunch": (
68
+ 'uvx --from "sqlrooms[postgres]" sqlrooms --db-path :memory:'
69
+ ),
70
+ "uvxWith": (
71
+ 'uvx --from sqlrooms --with "psycopg[binary]>=3.2.0" '
72
+ "sqlrooms --db-path :memory:"
73
+ ),
74
+ },
75
+ }
76
+
77
+ def list_catalog(self) -> dict[str, list[dict[str, Any]]]:
78
+ with self._connect() as conn:
79
+ with conn.cursor() as cur:
80
+ cur.execute("SELECT current_database()")
81
+ result = cur.fetchone()
82
+ if result is None:
83
+ raise RuntimeError(
84
+ "Postgres bridge could not resolve current database."
85
+ )
86
+ current_db = result[0]
87
+ cur.execute(
88
+ """
89
+ SELECT schema_name
90
+ FROM information_schema.schemata
91
+ ORDER BY schema_name
92
+ """
93
+ )
94
+ schemas = [
95
+ {"database": current_db, "schema": row[0]} for row in cur.fetchall()
96
+ ]
97
+ cur.execute(
98
+ """
99
+ SELECT table_schema, table_name, table_type
100
+ FROM information_schema.tables
101
+ WHERE table_schema NOT IN ('pg_catalog', 'information_schema')
102
+ ORDER BY table_schema, table_name
103
+ """
104
+ )
105
+ tables = [
106
+ {
107
+ "database": current_db,
108
+ "schema": row[0],
109
+ "table": row[1],
110
+ "isView": str(row[2]).upper().endswith("VIEW"),
111
+ }
112
+ for row in cur.fetchall()
113
+ ]
114
+ return {
115
+ "databases": [{"database": current_db}],
116
+ "schemas": schemas,
117
+ "tables": tables,
118
+ }
@@ -0,0 +1,161 @@
1
+ from __future__ import annotations
2
+
3
+ import importlib.util
4
+ from dataclasses import dataclass
5
+ from typing import Any
6
+
7
+ from ..utils import cursor_columns, quoted_ident, rows_to_json_rows
8
+ from .base import BaseSqlBridgeConnector
9
+
10
+ SNOWFLAKE_INSTALL_COMMANDS = {
11
+ "uvProject": "uv sync --extra snowflake",
12
+ "uvxRelaunch": 'uvx --from "sqlrooms[snowflake]" sqlrooms --db-path :memory:',
13
+ "uvxWith": (
14
+ 'uvx --from sqlrooms --with "snowflake-connector-python>=4.3.0" '
15
+ "sqlrooms --db-path :memory:"
16
+ ),
17
+ }
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class SnowflakeConnectorSettings:
22
+ account: str | None = None
23
+ user: str | None = None
24
+ password: str | None = None
25
+ warehouse: str | None = None
26
+ database: str | None = None
27
+ schema: str | None = None
28
+ role: str | None = None
29
+ authenticator: str | None = None
30
+ connection_id: str = "snowflake-default"
31
+ title: str = "Snowflake"
32
+
33
+ def is_enabled(self) -> bool:
34
+ return bool(self.account and self.user)
35
+
36
+ def to_connect_kwargs(self) -> dict[str, Any]:
37
+ kwargs: dict[str, Any] = {}
38
+ for key in (
39
+ "account",
40
+ "user",
41
+ "password",
42
+ "warehouse",
43
+ "database",
44
+ "schema",
45
+ "role",
46
+ "authenticator",
47
+ ):
48
+ value = getattr(self, key)
49
+ if value:
50
+ kwargs[key] = value
51
+ return kwargs
52
+
53
+
54
+ @dataclass(frozen=True)
55
+ class SnowflakeBridgeConnector(BaseSqlBridgeConnector):
56
+ settings: SnowflakeConnectorSettings
57
+ engine_id: str = "snowflake"
58
+
59
+ @property
60
+ def connection_id(self) -> str:
61
+ return self.settings.connection_id
62
+
63
+ @property
64
+ def title(self) -> str:
65
+ return self.settings.title
66
+
67
+ def config_dict(self) -> dict[str, Any]:
68
+ return self.settings.to_connect_kwargs()
69
+
70
+ def _connect(self):
71
+ try:
72
+ import snowflake.connector # type: ignore
73
+ except ImportError as exc:
74
+ raise RuntimeError(
75
+ "Snowflake bridge requires `snowflake-connector-python`."
76
+ ) from exc
77
+
78
+ kwargs = self.settings.to_connect_kwargs()
79
+ if not kwargs.get("account") or not kwargs.get("user"):
80
+ raise RuntimeError(
81
+ "Snowflake bridge requires both account and user configuration."
82
+ )
83
+ return snowflake.connector.connect(**kwargs)
84
+
85
+ def dependency_diagnostics(self) -> dict[str, Any]:
86
+ try:
87
+ available = importlib.util.find_spec("snowflake.connector") is not None
88
+ except ModuleNotFoundError as exc:
89
+ return {
90
+ "available": False,
91
+ "reason": (
92
+ f"Failed to inspect dependency via "
93
+ f"find_spec('snowflake.connector'): {exc}"
94
+ ),
95
+ "error": "Missing Python dependency: snowflake-connector-python",
96
+ "requiredPackages": ["snowflake-connector-python>=4.3.0"],
97
+ "installCommands": SNOWFLAKE_INSTALL_COMMANDS,
98
+ }
99
+ if available:
100
+ return {"available": True}
101
+ return {
102
+ "available": False,
103
+ "error": "Missing Python dependency: snowflake-connector-python",
104
+ "requiredPackages": ["snowflake-connector-python>=4.3.0"],
105
+ "installCommands": SNOWFLAKE_INSTALL_COMMANDS,
106
+ }
107
+
108
+ def list_catalog(self) -> dict[str, list[dict[str, Any]]]:
109
+ with self._connect() as conn:
110
+ with conn.cursor() as cur:
111
+ cur.execute("SHOW DATABASES")
112
+ db_rows = cur.fetchall()
113
+ db_columns = cursor_columns(cur)
114
+ databases: list[dict[str, Any]] = []
115
+ for row in rows_to_json_rows(db_rows, db_columns):
116
+ normalized = {str(k).lower(): v for k, v in row.items()}
117
+ name = normalized.get("name") or normalized.get("database_name")
118
+ if name:
119
+ databases.append({"database": str(name)})
120
+
121
+ current_db = self.settings.database
122
+ if not current_db:
123
+ cur.execute("SELECT CURRENT_DATABASE()")
124
+ maybe_db = cur.fetchone()
125
+ if maybe_db and maybe_db[0]:
126
+ current_db = str(maybe_db[0])
127
+
128
+ schemas: list[dict[str, Any]] = []
129
+ tables: list[dict[str, Any]] = []
130
+
131
+ if current_db:
132
+ db_ref = quoted_ident(current_db)
133
+ cur.execute(
134
+ f"""
135
+ SELECT SCHEMA_NAME
136
+ FROM {db_ref}.INFORMATION_SCHEMA.SCHEMATA
137
+ ORDER BY SCHEMA_NAME
138
+ """
139
+ )
140
+ schemas = [
141
+ {"database": current_db, "schema": row[0]}
142
+ for row in cur.fetchall()
143
+ ]
144
+ cur.execute(
145
+ f"""
146
+ SELECT TABLE_SCHEMA, TABLE_NAME, TABLE_TYPE
147
+ FROM {db_ref}.INFORMATION_SCHEMA.TABLES
148
+ ORDER BY TABLE_SCHEMA, TABLE_NAME
149
+ """
150
+ )
151
+ tables = [
152
+ {
153
+ "database": current_db,
154
+ "schema": row[0],
155
+ "table": row[1],
156
+ "isView": str(row[2]).upper().endswith("VIEW"),
157
+ }
158
+ for row in cur.fetchall()
159
+ ]
160
+
161
+ return {"databases": databases, "schemas": schemas, "tables": tables}
@@ -0,0 +1,91 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from .connectors import (
6
+ PostgresBridgeConnector,
7
+ PostgresConnectorSettings,
8
+ SnowflakeBridgeConnector,
9
+ SnowflakeConnectorSettings,
10
+ )
11
+ from .connectors.base import BaseSqlBridgeConnector
12
+ from .registry import DbBridgeRegistry
13
+
14
+ SUPPORTED_ENGINES: list[str] = ["postgres", "snowflake"]
15
+
16
+ ENGINE_CONFIG_FIELDS: dict[str, list[dict[str, Any]]] = {
17
+ "postgres": [
18
+ {"key": "host", "label": "Host", "placeholder": "localhost", "required": True},
19
+ {"key": "port", "label": "Port", "placeholder": "5432"},
20
+ {
21
+ "key": "database",
22
+ "label": "Database",
23
+ "placeholder": "my_db",
24
+ "required": True,
25
+ },
26
+ {"key": "user", "label": "User", "placeholder": "postgres", "required": True},
27
+ {"key": "password", "label": "Password", "placeholder": "", "secret": True},
28
+ ],
29
+ "snowflake": [
30
+ {
31
+ "key": "account",
32
+ "label": "Account",
33
+ "placeholder": "xy12345.us-east-1",
34
+ "required": True,
35
+ },
36
+ {"key": "user", "label": "User", "placeholder": "my_user", "required": True},
37
+ {"key": "password", "label": "Password", "placeholder": "", "secret": True},
38
+ {"key": "warehouse", "label": "Warehouse", "placeholder": "COMPUTE_WH"},
39
+ {"key": "database", "label": "Database", "placeholder": "MY_DB"},
40
+ {"key": "schema", "label": "Schema", "placeholder": "PUBLIC"},
41
+ {"key": "role", "label": "Role", "placeholder": "ACCOUNTADMIN"},
42
+ {"key": "authenticator", "label": "Authenticator", "placeholder": "snowflake"},
43
+ ],
44
+ }
45
+
46
+
47
+ def build_cli_db_bridge_registry(
48
+ *,
49
+ bridge_id: str,
50
+ connector_settings: list[PostgresConnectorSettings | SnowflakeConnectorSettings]
51
+ | None = None,
52
+ ) -> DbBridgeRegistry:
53
+ registry = DbBridgeRegistry(bridge_id=bridge_id)
54
+ for settings in connector_settings or []:
55
+ if isinstance(settings, PostgresConnectorSettings):
56
+ registry.register(PostgresBridgeConnector(settings=settings))
57
+ continue
58
+ if isinstance(settings, SnowflakeConnectorSettings) and settings.is_enabled():
59
+ registry.register(SnowflakeBridgeConnector(settings=settings))
60
+ return registry
61
+
62
+
63
+ def build_ephemeral_connector(
64
+ engine: str,
65
+ config: dict[str, str],
66
+ ) -> BaseSqlBridgeConnector:
67
+ """Create a throwaway connector from raw config for ad-hoc connection tests."""
68
+ if engine == "postgres":
69
+ return PostgresBridgeConnector(
70
+ settings=PostgresConnectorSettings(
71
+ host=config.get("host", "localhost"),
72
+ port=config.get("port", "5432"),
73
+ database=config.get("database", ""),
74
+ user=config.get("user", ""),
75
+ password=config.get("password"),
76
+ )
77
+ )
78
+ if engine == "snowflake":
79
+ return SnowflakeBridgeConnector(
80
+ settings=SnowflakeConnectorSettings(
81
+ account=config.get("account"),
82
+ user=config.get("user"),
83
+ password=config.get("password"),
84
+ warehouse=config.get("warehouse"),
85
+ database=config.get("database"),
86
+ schema=config.get("schema"),
87
+ role=config.get("role"),
88
+ authenticator=config.get("authenticator"),
89
+ )
90
+ )
91
+ raise ValueError(f"Unsupported engine: {engine!r}")
@@ -0,0 +1,113 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from .types import DbBridgeConnector
6
+
7
+ _SECRET_KEYS: frozenset[str] | None = None
8
+
9
+
10
+ def _get_secret_keys() -> frozenset[str]:
11
+ global _SECRET_KEYS
12
+ if _SECRET_KEYS is None:
13
+ from .factory import ENGINE_CONFIG_FIELDS
14
+
15
+ keys: set[str] = set()
16
+ for fields in ENGINE_CONFIG_FIELDS.values():
17
+ for f in fields:
18
+ if f.get("secret"):
19
+ keys.add(f["key"])
20
+ _SECRET_KEYS = frozenset(keys)
21
+ return _SECRET_KEYS
22
+
23
+
24
+ class UnknownBridgeConnectionError(KeyError):
25
+ """Raised when a bridge request references an unknown connection id."""
26
+
27
+
28
+ class DbBridgeRegistry:
29
+ def __init__(self, *, bridge_id: str):
30
+ self.bridge_id = bridge_id
31
+ self._connectors: dict[str, DbBridgeConnector] = {}
32
+
33
+ def register(self, connector: DbBridgeConnector) -> None:
34
+ if connector.connection_id in self._connectors:
35
+ raise ValueError(
36
+ f"Duplicate DB bridge connection id: {connector.connection_id}"
37
+ )
38
+ self._connectors[connector.connection_id] = connector
39
+
40
+ def has_connections(self) -> bool:
41
+ return bool(self._connectors)
42
+
43
+ def has_engine(self, engine_id: str) -> bool:
44
+ return any(conn.engine_id == engine_id for conn in self._connectors.values())
45
+
46
+ def runtime_connections(self) -> list[dict[str, Any]]:
47
+ result = []
48
+ for conn in self._connectors.values():
49
+ entry: dict[str, Any] = {
50
+ "id": conn.connection_id,
51
+ "engineId": conn.engine_id,
52
+ "title": conn.title,
53
+ "runtimeSupport": "server",
54
+ "requiresBridge": True,
55
+ "bridgeId": self.bridge_id,
56
+ "isCore": False,
57
+ }
58
+ cfg = conn.config_dict()
59
+ if cfg:
60
+ secret_keys = _get_secret_keys()
61
+ entry["config"] = {k: v for k, v in cfg.items() if k not in secret_keys}
62
+ result.append(entry)
63
+ return result
64
+
65
+ def runtime_diagnostics(self) -> list[dict[str, Any]]:
66
+ diagnostics: list[dict[str, Any]] = []
67
+ for conn in self._connectors.values():
68
+ payload = conn.dependency_diagnostics()
69
+ payload["id"] = conn.connection_id
70
+ payload["engineId"] = conn.engine_id
71
+ payload["title"] = conn.title
72
+ diagnostics.append(payload)
73
+ return diagnostics
74
+
75
+ def test_connection(self, connection_id: str) -> bool:
76
+ return self._get_connector(connection_id).test_connection()
77
+
78
+ def list_catalog(self, connection_id: str) -> dict[str, list[dict[str, Any]]]:
79
+ return self._get_connector(connection_id).list_catalog()
80
+
81
+ def execute_query(
82
+ self,
83
+ connection_id: str,
84
+ sql: str,
85
+ query_type: str,
86
+ ) -> dict[str, Any]:
87
+ return self._get_connector(connection_id).execute_query(sql, query_type)
88
+
89
+ def fetch_arrow_bytes(self, connection_id: str, sql: str) -> bytes:
90
+ return self._get_connector(connection_id).fetch_arrow_bytes(sql)
91
+
92
+ def stream_arrow_batches(
93
+ self,
94
+ connection_id: str,
95
+ sql: str,
96
+ *,
97
+ chunk_rows: int = 5000,
98
+ query_id: str | None = None,
99
+ ):
100
+ return self._get_connector(connection_id).stream_arrow_batches(
101
+ sql, chunk_rows=chunk_rows, query_id=query_id
102
+ )
103
+
104
+ def cancel_query(self, connection_id: str, query_id: str) -> bool:
105
+ return self._get_connector(connection_id).cancel_query(query_id)
106
+
107
+ def _get_connector(self, connection_id: str) -> DbBridgeConnector:
108
+ connector = self._connectors.get(connection_id)
109
+ if connector is None:
110
+ raise UnknownBridgeConnectionError(
111
+ f"Unknown DB bridge connection: {connection_id}"
112
+ )
113
+ return connector
@@ -0,0 +1,29 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Iterable, Protocol
4
+
5
+
6
+ class DbBridgeConnector(Protocol):
7
+ connection_id: str
8
+ engine_id: str
9
+ title: str
10
+
11
+ def config_dict(self) -> dict[str, Any]:
12
+ """Return engine-specific configuration as a flat dict."""
13
+ ...
14
+
15
+ def test_connection(self) -> bool: ...
16
+
17
+ def list_catalog(self) -> dict[str, list[dict[str, Any]]]: ...
18
+
19
+ def execute_query(self, sql: str, query_type: str) -> dict[str, Any]: ...
20
+
21
+ def fetch_arrow_bytes(self, sql: str) -> bytes: ...
22
+
23
+ def stream_arrow_batches(
24
+ self, sql: str, chunk_rows: int = 5000, query_id: str | None = None
25
+ ) -> Iterable[bytes]: ...
26
+
27
+ def cancel_query(self, query_id: str) -> bool: ...
28
+
29
+ def dependency_diagnostics(self) -> dict[str, Any]: ...
@@ -0,0 +1,45 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Iterable
4
+
5
+ import pyarrow as pa
6
+
7
+
8
+ def cursor_columns(cursor: Any) -> list[str]:
9
+ description = getattr(cursor, "description", None) or []
10
+ columns: list[str] = []
11
+ for item in description:
12
+ if hasattr(item, "name"):
13
+ columns.append(str(item.name))
14
+ elif isinstance(item, (list, tuple)) and item:
15
+ columns.append(str(item[0]))
16
+ return columns
17
+
18
+
19
+ def rows_to_json_rows(
20
+ rows: Iterable[Iterable[Any]],
21
+ columns: list[str],
22
+ ) -> list[dict[str, Any]]:
23
+ return [dict(zip(columns, row)) for row in rows]
24
+
25
+
26
+ def rows_to_arrow_bytes(
27
+ rows: Iterable[Iterable[Any]],
28
+ columns: list[str],
29
+ ) -> bytes:
30
+ json_rows = rows_to_json_rows(rows, columns)
31
+ if json_rows:
32
+ table = pa.Table.from_pylist(json_rows)
33
+ elif columns:
34
+ table = pa.table({column: pa.array([], type=pa.null()) for column in columns})
35
+ else:
36
+ table = pa.table({})
37
+
38
+ sink = pa.BufferOutputStream()
39
+ with pa.ipc.new_stream(sink, table.schema) as writer:
40
+ writer.write_table(table)
41
+ return sink.getvalue().to_pybytes()
42
+
43
+
44
+ def quoted_ident(ident: str) -> str:
45
+ return '"' + ident.replace('"', '""') + '"'