ob-bigquery 0.1.0__tar.gz

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.
@@ -0,0 +1,35 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *$py.class
4
+ *.so
5
+ dist/
6
+ build/
7
+ *.egg-info/
8
+ *.egg
9
+ .eggs/
10
+ .venv/
11
+ venv/
12
+ .env
13
+ *.db
14
+ *.duckdb
15
+ *.sqlite3
16
+ .mypy_cache/
17
+ .ruff_cache/
18
+ .pytest_cache/
19
+ htmlcov/
20
+ .coverage
21
+ .coverage.*
22
+ *.log
23
+ .DS_Store
24
+ .claude/
25
+ security/
26
+ infra/
27
+ site/
28
+ scripts/
29
+ .mcpregistry_github_token
30
+ .mcpregistry_registry_token
31
+ _*/
32
+ blog/
33
+ review*
34
+ design/
35
+ examples/test*
@@ -0,0 +1,70 @@
1
+ Business Source License 1.1
2
+
3
+ Parameters
4
+
5
+ Licensor: RALFORION d.o.o.
6
+ Licensed Work: OrionBelt Semantic Layer 1.0
7
+ The Licensed Work is (c) 2025 RALFORION d.o.o.
8
+ Additional Use Grant: You may use the Licensed Work for any purpose,
9
+ including production use, PROVIDED THAT you may
10
+ not embed OrionBelt in a commercial product or
11
+ provide it as part of a commercial service
12
+ without purchasing a commercial license from
13
+ the Licensor. Contact licensing@ralforion.com
14
+ for commercial licensing inquiries.
15
+ Change Date: 2030-03-16
16
+ Change License: Apache License, Version 2.0
17
+
18
+ -----------------------------------------------------------------------------
19
+
20
+ Business Source License 1.1
21
+
22
+ License text copyright (c) 2017 MariaDB Corporation Ab, All Rights Reserved.
23
+ "Business Source License" is a trademark of MariaDB Corporation Ab.
24
+
25
+ Terms
26
+
27
+ The Licensor hereby grants you the right to copy, modify, create derivative
28
+ works, redistribute, and make non-production use of the Licensed Work. The
29
+ Licensor may make an Additional Use Grant, above, permitting limited
30
+ production use.
31
+
32
+ Effective on the Change Date, or the fourth anniversary of the first publicly
33
+ available distribution of a specific version of the Licensed Work under this
34
+ License, whichever comes first, the Licensor hereby grants you rights under
35
+ the terms of the Change License, and the rights granted in the paragraph
36
+ above terminate.
37
+
38
+ If your use of the Licensed Work does not comply with the requirements
39
+ currently in effect as described in this License, you must purchase a
40
+ commercial license from the Licensor, its affiliated entities, or authorized
41
+ resellers, or you must refrain from using the Licensed Work.
42
+
43
+ All copies of the original and modified Licensed Work, and derivative works
44
+ of the Licensed Work, are subject to this License. This License applies
45
+ separately for each version of the Licensed Work and the Change Date may vary
46
+ for each version of the Licensed Work released by Licensor.
47
+
48
+ You must conspicuously display this License on each original or modified copy
49
+ of the Licensed Work. If you receive the Licensed Work in original or
50
+ modified form from a third party, the terms and conditions set forth in this
51
+ License apply to your use of that work.
52
+
53
+ Any use of the Licensed Work in violation of this License will automatically
54
+ terminate your rights under this License for the current and all other
55
+ versions of the Licensed Work.
56
+
57
+ This License does not grant you any right in any trademark or logo of
58
+ Licensor or its affiliates (provided that you may use a trademark or logo of
59
+ Licensor as expressly required by this License).
60
+
61
+ TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON
62
+ AN "AS IS" BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS,
63
+ EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF
64
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND
65
+ TITLE.
66
+
67
+ MariaDB hereby grants you permission to use this License's text to license
68
+ your works, and to refer to it using the trademark "Business Source License",
69
+ as long as you comply with the Covenants of Licensor of MariaDB's Business Source
70
+ License Trademark Policy available at https://mariadb.com/bsl11/.
@@ -0,0 +1,35 @@
1
+ Metadata-Version: 2.4
2
+ Name: ob-bigquery
3
+ Version: 0.1.0
4
+ Summary: OrionBelt Semantic Layer driver for BigQuery (DB-API 2.0)
5
+ Author-email: Ralf Becher <info@orionbelt.ai>
6
+ License: BSL-1.1
7
+ License-File: LICENSE
8
+ Keywords: bigquery,db-api,obml,orionbelt,semantic-layer
9
+ Requires-Python: >=3.12
10
+ Requires-Dist: google-cloud-bigquery[pandas]>=3.20
11
+ Requires-Dist: ob-driver-core>=0.1
12
+ Requires-Dist: pyarrow>=16.0
13
+ Provides-Extra: dev
14
+ Requires-Dist: mypy>=1.10; extra == 'dev'
15
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
16
+ Requires-Dist: pytest>=8.0; extra == 'dev'
17
+ Requires-Dist: respx>=0.21; extra == 'dev'
18
+ Requires-Dist: ruff>=0.4; extra == 'dev'
19
+ Description-Content-Type: text/markdown
20
+
21
+ # ob-bigquery
22
+
23
+ OrionBelt Semantic Layer driver for BigQuery (DB-API 2.0)
24
+
25
+ Part of the [OrionBelt Semantic Layer](https://github.com/ralfbecher/orionbelt-semantic-layer) project.
26
+
27
+ ## Installation
28
+
29
+ ```bash
30
+ pip install ob-bigquery
31
+ ```
32
+
33
+ ## License
34
+
35
+ BSL-1.1 — see [LICENSE](LICENSE) for details.
@@ -0,0 +1,15 @@
1
+ # ob-bigquery
2
+
3
+ OrionBelt Semantic Layer driver for BigQuery (DB-API 2.0)
4
+
5
+ Part of the [OrionBelt Semantic Layer](https://github.com/ralfbecher/orionbelt-semantic-layer) project.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ pip install ob-bigquery
11
+ ```
12
+
13
+ ## License
14
+
15
+ BSL-1.1 — see [LICENSE](LICENSE) for details.
@@ -0,0 +1,48 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "ob-bigquery"
7
+ version = "0.1.0"
8
+ description = "OrionBelt Semantic Layer driver for BigQuery (DB-API 2.0)"
9
+ license = { text = "BSL-1.1" }
10
+ readme = "README.md"
11
+ authors = [{ name = "Ralf Becher", email = "info@orionbelt.ai" }]
12
+ keywords = ["bigquery", "semantic-layer", "obml", "db-api", "orionbelt"]
13
+ requires-python = ">=3.12"
14
+ dependencies = [
15
+ "ob-driver-core>=0.1",
16
+ "google-cloud-bigquery[pandas]>=3.20",
17
+ "pyarrow>=16.0",
18
+ ]
19
+
20
+ [project.optional-dependencies]
21
+ dev = [
22
+ "pytest>=8.0",
23
+ "pytest-asyncio>=0.23",
24
+ "mypy>=1.10",
25
+ "ruff>=0.4",
26
+ "respx>=0.21",
27
+ ]
28
+
29
+ [tool.hatch.build.targets.wheel]
30
+ packages = ["src/ob_bigquery"]
31
+
32
+ [tool.ruff]
33
+ line-length = 100
34
+ target-version = "py312"
35
+
36
+ [tool.ruff.lint]
37
+ select = ["E", "F", "I", "UP", "B", "SIM", "ANN"]
38
+
39
+ [tool.mypy]
40
+ python_version = "3.12"
41
+ strict = true
42
+ files = ["src/ob_bigquery"]
43
+
44
+ [tool.pytest.ini_options]
45
+ asyncio_mode = "auto"
46
+ testpaths = ["tests"]
47
+ markers = ["integration: requires live DB + OB API"]
48
+ addopts = "-m 'not integration'"
@@ -0,0 +1,109 @@
1
+ """ob-bigquery — OrionBelt Semantic Layer driver for BigQuery (PEP 249 DB-API 2.0).
2
+
3
+ Requires the OrionBelt REST API running in single-model mode (MODEL_FILE set).
4
+ OBML queries are compiled to SQL via ``POST /v1/query/sql``.
5
+
6
+ Usage::
7
+
8
+ import ob_bigquery
9
+
10
+ conn = ob_bigquery.connect(project="my-gcp-project")
11
+ with conn.cursor() as cur:
12
+ cur.execute("select:\\n dimensions:\\n - Region\\n measures:\\n - Revenue")
13
+ print(cur.fetchall())
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from typing import Any
19
+
20
+ from google.cloud import bigquery
21
+
22
+ from ob_bigquery.connection import Connection
23
+ from ob_bigquery.exceptions import (
24
+ DataError,
25
+ DatabaseError,
26
+ Error,
27
+ IntegrityError,
28
+ InterfaceError,
29
+ InternalError,
30
+ NotSupportedError,
31
+ OperationalError,
32
+ ProgrammingError,
33
+ Warning,
34
+ )
35
+
36
+ # PEP 249 module-level constants
37
+ apilevel = "2.0"
38
+ threadsafety = 1 # threads may share the module but not connections
39
+ paramstyle = "pyformat" # BigQuery uses %(name)s placeholders
40
+
41
+
42
+ def connect(
43
+ *,
44
+ project: str | None = None,
45
+ credentials: Any = None,
46
+ credentials_file: str | None = None,
47
+ location: str | None = None,
48
+ # OrionBelt parameters
49
+ ob_api_url: str = "http://localhost:8000",
50
+ ob_timeout: int = 30,
51
+ ) -> Connection:
52
+ """Open a BigQuery connection with OBML support.
53
+
54
+ Parameters
55
+ ----------
56
+ project : str, optional
57
+ GCP project ID. If not set, uses Application Default Credentials project.
58
+ credentials : google.auth.credentials.Credentials, optional
59
+ Explicit credentials object. If not set, uses ADC.
60
+ credentials_file : str, optional
61
+ Path to a service account JSON key file. Alternative to setting
62
+ ``GOOGLE_APPLICATION_CREDENTIALS`` env var.
63
+ location : str, optional
64
+ Default dataset location (e.g. ``US``, ``EU``).
65
+ ob_api_url : str
66
+ OrionBelt REST API URL (must be running in single-model mode).
67
+ ob_timeout : int
68
+ HTTP timeout in seconds for OBML compilation.
69
+ """
70
+ if credentials_file is not None and credentials is None:
71
+ from google.oauth2 import service_account
72
+
73
+ credentials = service_account.Credentials.from_service_account_file(
74
+ credentials_file
75
+ )
76
+
77
+ client_kwargs: dict[str, Any] = {}
78
+ if project is not None:
79
+ client_kwargs["project"] = project
80
+ if credentials is not None:
81
+ client_kwargs["credentials"] = credentials
82
+ if location is not None:
83
+ client_kwargs["location"] = location
84
+
85
+ native = bigquery.Client(**client_kwargs)
86
+ return Connection(
87
+ native,
88
+ ob_api_url=ob_api_url,
89
+ ob_timeout=ob_timeout,
90
+ )
91
+
92
+
93
+ __all__ = [
94
+ "apilevel",
95
+ "threadsafety",
96
+ "paramstyle",
97
+ "connect",
98
+ "Connection",
99
+ "Warning",
100
+ "Error",
101
+ "InterfaceError",
102
+ "DatabaseError",
103
+ "DataError",
104
+ "OperationalError",
105
+ "IntegrityError",
106
+ "InternalError",
107
+ "ProgrammingError",
108
+ "NotSupportedError",
109
+ ]
@@ -0,0 +1,8 @@
1
+ """OrionBelt compilation bridge — delegates to ob-driver-core."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from ob_driver_core.compiler import compile_obml
6
+ from ob_driver_core.detection import is_obml, parse_obml
7
+
8
+ __all__ = ["compile_obml", "is_obml", "parse_obml"]
@@ -0,0 +1,59 @@
1
+ """PEP 249 Connection wrapping ``google.cloud.bigquery.Client``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from ob_bigquery.cursor import Cursor
8
+ from ob_bigquery.exceptions import ProgrammingError
9
+
10
+
11
+ class Connection:
12
+ """DB-API 2.0 connection that wraps a native BigQuery client.
13
+
14
+ OBML queries are compiled to SQL via the OrionBelt REST API
15
+ (single-model mode, ``/v1/query/sql`` shortcut).
16
+ """
17
+
18
+ def __init__(
19
+ self,
20
+ native: Any,
21
+ *,
22
+ ob_api_url: str = "http://localhost:8000",
23
+ ob_timeout: int = 30,
24
+ ) -> None:
25
+ self._native = native
26
+ self._closed = False
27
+ self._ob_api_url = ob_api_url
28
+ self._ob_timeout = ob_timeout
29
+
30
+ def _check_open(self) -> None:
31
+ if self._closed:
32
+ raise ProgrammingError("Connection is closed.")
33
+
34
+ def cursor(self) -> Cursor:
35
+ """Return a new Cursor for this connection."""
36
+ self._check_open()
37
+ return Cursor(
38
+ self._native,
39
+ ob_api_url=self._ob_api_url,
40
+ ob_timeout=self._ob_timeout,
41
+ )
42
+
43
+ def commit(self) -> None:
44
+ """No-op — BigQuery auto-commits all statements."""
45
+
46
+ def rollback(self) -> None:
47
+ """No-op — BigQuery does not support transactions via this driver."""
48
+
49
+ def close(self) -> None:
50
+ """Close the connection."""
51
+ if not self._closed:
52
+ self._native.close()
53
+ self._closed = True
54
+
55
+ def __enter__(self) -> Connection:
56
+ return self
57
+
58
+ def __exit__(self, *exc: object) -> None:
59
+ self.close()
@@ -0,0 +1,209 @@
1
+ """PEP 249 Cursor with OBML interception for BigQuery.
2
+
3
+ ``execute()`` fetches results as an Arrow table via ``RowIterator.to_arrow()``
4
+ for zero-copy Arrow Flight support. ``fetch_arrow_table()`` returns the
5
+ native Arrow table directly.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any
11
+
12
+ import pyarrow as pa
13
+
14
+ from ob_bigquery.compiler import compile_obml, is_obml, parse_obml
15
+ from ob_bigquery.exceptions import NotSupportedError, ProgrammingError
16
+ from ob_bigquery.type_codes import BQ_TYPE_MAP, STRING
17
+
18
+
19
+ class Cursor:
20
+ """DB-API 2.0 cursor that intercepts OBML YAML queries.
21
+
22
+ If the query is OBML, it is compiled to BigQuery SQL via the OrionBelt
23
+ REST API before execution. Plain SQL is passed through unchanged.
24
+
25
+ Results are kept in Arrow columnar format via ``to_arrow()``.
26
+ ``fetch_arrow_table()`` returns the Arrow table directly for efficient
27
+ Arrow Flight SQL streaming.
28
+ """
29
+
30
+ arraysize: int = 1
31
+
32
+ def __init__(
33
+ self,
34
+ native: Any,
35
+ *,
36
+ ob_api_url: str = "http://localhost:8000",
37
+ ob_timeout: int = 30,
38
+ ) -> None:
39
+ self._native = native
40
+ self._closed = False
41
+ self._ob_api_url = ob_api_url
42
+ self._ob_timeout = ob_timeout
43
+ self._results: Any = None
44
+ self._rows: list[tuple[Any, ...]] = []
45
+ self._index: int = 0
46
+ self._arrow_table: pa.Table | None = None
47
+
48
+ @property
49
+ def description(
50
+ self,
51
+ ) -> tuple[tuple[str, Any, None, None, None, None, None], ...] | None:
52
+ """PEP 249 cursor description — 7-item tuples per column."""
53
+ if self._results is None:
54
+ return None
55
+ schema = self._results.schema
56
+ if not schema:
57
+ return None
58
+ cols: list[tuple[str, Any, None, None, None, None, None]] = []
59
+ for field in schema:
60
+ type_code = BQ_TYPE_MAP.get(field.field_type, STRING)
61
+ cols.append((field.name, type_code, None, None, None, None, None))
62
+ return tuple(cols)
63
+
64
+ @property
65
+ def rowcount(self) -> int:
66
+ if self._results is None:
67
+ return -1
68
+ return self._results.total_rows or -1
69
+
70
+ def _check_open(self) -> None:
71
+ if self._closed:
72
+ raise ProgrammingError("Cursor is closed.")
73
+
74
+ def execute(self, operation: str, parameters: Any = None) -> Cursor:
75
+ """Execute a query — OBML YAML or plain SQL.
76
+
77
+ Fetches results as an Arrow table via ``to_arrow()`` for zero-copy
78
+ Arrow Flight support.
79
+ """
80
+ self._check_open()
81
+ sql = self._resolve_sql(operation)
82
+ if parameters is not None:
83
+ query_params = [
84
+ _to_query_parameter(k, v) for k, v in parameters.items()
85
+ ]
86
+ from google.cloud.bigquery import QueryJobConfig
87
+
88
+ job_config = QueryJobConfig(query_parameters=query_params)
89
+ self._results = self._native.query(sql, job_config=job_config)
90
+ else:
91
+ self._results = self._native.query(sql)
92
+
93
+ self._arrow_table = self._results.to_arrow()
94
+ pydict = self._arrow_table.to_pydict()
95
+ col_names = list(pydict.keys())
96
+ n = self._arrow_table.num_rows
97
+ self._rows = [tuple(pydict[c][i] for c in col_names) for i in range(n)]
98
+ self._index = 0
99
+ return self
100
+
101
+ def executemany(self, operation: str, seq_of_parameters: Any) -> None:
102
+ """Execute against all parameter sequences.
103
+
104
+ OBML queries are not supported with executemany — raises NotSupportedError.
105
+ """
106
+ self._check_open()
107
+ if is_obml(operation):
108
+ raise NotSupportedError("executemany() is not supported for OBML queries.")
109
+ for params in seq_of_parameters:
110
+ self.execute(operation, params)
111
+
112
+ def _resolve_sql(self, operation: str) -> str:
113
+ """Compile OBML to SQL or return plain SQL unchanged."""
114
+ if not is_obml(operation):
115
+ return operation
116
+ obml = parse_obml(operation)
117
+ return compile_obml(
118
+ obml,
119
+ dialect="bigquery",
120
+ ob_api_url=self._ob_api_url,
121
+ ob_timeout=self._ob_timeout,
122
+ )
123
+
124
+ def fetchone(self) -> tuple[Any, ...] | None:
125
+ """Fetch the next row."""
126
+ self._check_open()
127
+ if self._index >= len(self._rows):
128
+ return None
129
+ row = self._rows[self._index]
130
+ self._index += 1
131
+ return row
132
+
133
+ def fetchmany(self, size: int | None = None) -> list[tuple[Any, ...]]:
134
+ """Fetch the next *size* rows."""
135
+ self._check_open()
136
+ n = size if size is not None else self.arraysize
137
+ end = min(self._index + n, len(self._rows))
138
+ rows = self._rows[self._index : end]
139
+ self._index = end
140
+ return rows
141
+
142
+ def fetchall(self) -> list[tuple[Any, ...]]:
143
+ """Fetch all remaining rows."""
144
+ self._check_open()
145
+ rows = self._rows[self._index :]
146
+ self._index = len(self._rows)
147
+ return rows
148
+
149
+ def fetch_arrow_table(self) -> pa.Table:
150
+ """Fetch all remaining rows as a PyArrow Table.
151
+
152
+ BigQuery results are fetched via ``RowIterator.to_arrow()`` during
153
+ ``execute()``, providing zero-copy Arrow access. Significantly more
154
+ memory-efficient for large result sets and enables efficient Arrow
155
+ Flight SQL streaming.
156
+ """
157
+ self._check_open()
158
+ if self._arrow_table is None:
159
+ raise ProgrammingError("No Arrow result available — already consumed.")
160
+ table = self._arrow_table
161
+ self._arrow_table = None
162
+ return table
163
+
164
+ def close(self) -> None:
165
+ """Close the cursor."""
166
+ if not self._closed:
167
+ self._results = None
168
+ self._rows = []
169
+ self._arrow_table = None
170
+ self._closed = True
171
+
172
+ def setinputsizes(self, sizes: Any) -> None:
173
+ """No-op — required by PEP 249."""
174
+
175
+ def setoutputsize(self, size: int, column: int | None = None) -> None:
176
+ """No-op — required by PEP 249."""
177
+
178
+ @property
179
+ def lastrowid(self) -> None:
180
+ """BigQuery does not support lastrowid."""
181
+ return None
182
+
183
+ def __enter__(self) -> Cursor:
184
+ return self
185
+
186
+ def __exit__(self, *exc: object) -> None:
187
+ self.close()
188
+
189
+ def __iter__(self) -> Cursor:
190
+ return self
191
+
192
+ def __next__(self) -> tuple[Any, ...]:
193
+ row = self.fetchone()
194
+ if row is None:
195
+ raise StopIteration
196
+ return row
197
+
198
+
199
+ def _to_query_parameter(name: str, value: Any) -> Any:
200
+ """Convert a Python value to a BigQuery ScalarQueryParameter."""
201
+ from google.cloud.bigquery import ScalarQueryParameter
202
+
203
+ if isinstance(value, int):
204
+ return ScalarQueryParameter(name, "INT64", value)
205
+ if isinstance(value, float):
206
+ return ScalarQueryParameter(name, "FLOAT64", value)
207
+ if isinstance(value, bool):
208
+ return ScalarQueryParameter(name, "BOOL", value)
209
+ return ScalarQueryParameter(name, "STRING", str(value))
@@ -0,0 +1,32 @@
1
+ """PEP 249 exceptions — re-exported from ob-driver-core for convenience.
2
+
3
+ Allows users to do: ``from ob_bigquery.exceptions import ProgrammingError``
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from ob_driver_core.exceptions import (
9
+ DataError,
10
+ DatabaseError,
11
+ Error,
12
+ IntegrityError,
13
+ InterfaceError,
14
+ InternalError,
15
+ NotSupportedError,
16
+ OperationalError,
17
+ ProgrammingError,
18
+ Warning,
19
+ )
20
+
21
+ __all__ = [
22
+ "Warning",
23
+ "Error",
24
+ "InterfaceError",
25
+ "DatabaseError",
26
+ "DataError",
27
+ "OperationalError",
28
+ "IntegrityError",
29
+ "InternalError",
30
+ "ProgrammingError",
31
+ "NotSupportedError",
32
+ ]
@@ -0,0 +1,40 @@
1
+ """PEP 249 type objects — re-exported from ob-driver-core plus BigQuery type mapping.
2
+
3
+ google-cloud-bigquery cursor.description returns string type names.
4
+ The BQ_TYPE_MAP maps them to PEP 249 type objects.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from ob_driver_core.type_codes import BINARY, DATETIME, NUMBER, ROWID, STRING
10
+
11
+ __all__ = ["STRING", "BINARY", "NUMBER", "DATETIME", "ROWID", "BQ_TYPE_MAP"]
12
+
13
+ # BigQuery standard SQL type names
14
+ # https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types
15
+ BQ_TYPE_MAP: dict[str, object] = {
16
+ # Numeric types
17
+ "INTEGER": NUMBER,
18
+ "INT64": NUMBER,
19
+ "FLOAT": NUMBER,
20
+ "FLOAT64": NUMBER,
21
+ "NUMERIC": NUMBER,
22
+ "BIGNUMERIC": NUMBER,
23
+ "BOOLEAN": STRING,
24
+ "BOOL": STRING,
25
+ # String types
26
+ "STRING": STRING,
27
+ "JSON": STRING,
28
+ # Date/time types
29
+ "DATE": DATETIME,
30
+ "TIME": DATETIME,
31
+ "DATETIME": DATETIME,
32
+ "TIMESTAMP": DATETIME,
33
+ # Binary types
34
+ "BYTES": BINARY,
35
+ # Geo/struct/array — map to STRING for display
36
+ "GEOGRAPHY": STRING,
37
+ "STRUCT": STRING,
38
+ "RECORD": STRING,
39
+ "ARRAY": STRING,
40
+ }