spanner-adbc 0.8.0__py3-none-win_amd64.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.
- spanner_adbc/__init__.py +80 -0
- spanner_adbc/_options.py +176 -0
- spanner_adbc/_version.py +1 -0
- spanner_adbc/dbapi.py +69 -0
- spanner_adbc/manifest.py +284 -0
- spanner_adbc/py.typed +0 -0
- spanner_adbc/spanner_adbc.dll +0 -0
- spanner_adbc-0.8.0.dist-info/METADATA +361 -0
- spanner_adbc-0.8.0.dist-info/RECORD +13 -0
- spanner_adbc-0.8.0.dist-info/WHEEL +5 -0
- spanner_adbc-0.8.0.dist-info/entry_points.txt +2 -0
- spanner_adbc-0.8.0.dist-info/licenses/LICENSE +202 -0
- spanner_adbc-0.8.0.dist-info/top_level.txt +1 -0
spanner_adbc/__init__.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""ADBC driver for Google Cloud Spanner.
|
|
2
|
+
|
|
3
|
+
This package bundles the prebuilt Spanner ADBC driver shared library and exposes
|
|
4
|
+
a thin Python wrapper around it. The heavy lifting lives in the Rust cdylib; this
|
|
5
|
+
module just locates the bundled library and hands it to ``adbc_driver_manager``,
|
|
6
|
+
which loads it over the ADBC C ABI.
|
|
7
|
+
|
|
8
|
+
For a DBAPI 2.0 (PEP 249) connection with pandas/polars/Arrow helpers, use
|
|
9
|
+
:func:`spanner_adbc.dbapi.connect` instead of the low-level
|
|
10
|
+
:func:`connect` here.
|
|
11
|
+
|
|
12
|
+
To reach the driver through the *generic* ``adbc_driver_manager`` instead — as
|
|
13
|
+
``driver="spanner"``, or by URI scheme alone from ``uri="spanner:///..."`` —
|
|
14
|
+
install an ADBC driver manifest once with ``python -m
|
|
15
|
+
spanner_adbc.manifest install`` (see :mod:`spanner_adbc.manifest`).
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import functools
|
|
19
|
+
import pathlib
|
|
20
|
+
import typing
|
|
21
|
+
|
|
22
|
+
import adbc_driver_manager
|
|
23
|
+
|
|
24
|
+
from ._options import ConnectionOptions, DatabaseOptions, StatementOptions
|
|
25
|
+
from ._version import __version__
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"connect",
|
|
29
|
+
"ENTRYPOINT",
|
|
30
|
+
"DatabaseOptions",
|
|
31
|
+
"ConnectionOptions",
|
|
32
|
+
"StatementOptions",
|
|
33
|
+
"__version__",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
#: C entrypoint exported by the shared library (see src/ffi.rs).
|
|
37
|
+
ENTRYPOINT = "AdbcSpannerInit"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def connect(
|
|
41
|
+
db_kwargs: typing.Optional[typing.Mapping[str, str]] = None,
|
|
42
|
+
) -> adbc_driver_manager.AdbcDatabase:
|
|
43
|
+
"""Create a low-level ADBC database handle for Spanner.
|
|
44
|
+
|
|
45
|
+
Parameters
|
|
46
|
+
----------
|
|
47
|
+
db_kwargs:
|
|
48
|
+
Database-level driver options, keyed by their raw option strings — best
|
|
49
|
+
written with the :class:`DatabaseOptions` constants, e.g.
|
|
50
|
+
``{DatabaseOptions.URI.value: "spanner:///projects/<p>/instances/<i>/databases/<d>"}``
|
|
51
|
+
(see src/lib.rs / docs/options.md). The ``uri`` option requires the
|
|
52
|
+
``spanner://`` scheme; a bare database path is rejected. Credentials, the
|
|
53
|
+
emulator, endpoint overrides, and every other setting are all passed here
|
|
54
|
+
as their raw keys — for example
|
|
55
|
+
``{DatabaseOptions.URI.value: "...", DatabaseOptions.KEYFILE.value: "/path/key.json"}``
|
|
56
|
+
or ``{DatabaseOptions.URI.value: "...", DatabaseOptions.EMULATOR.value: "true"}``.
|
|
57
|
+
|
|
58
|
+
For a DBAPI 2.0 connection, prefer :func:`spanner_adbc.dbapi.connect`.
|
|
59
|
+
"""
|
|
60
|
+
# ** unpacking accepts the dotted, non-identifier option keys; they land in
|
|
61
|
+
# AdbcDatabase's **kwargs and are forwarded as ADBC options.
|
|
62
|
+
options = dict(db_kwargs) if db_kwargs else {}
|
|
63
|
+
return adbc_driver_manager.AdbcDatabase(
|
|
64
|
+
driver=_driver_path(), entrypoint=ENTRYPOINT, **options
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@functools.cache
|
|
69
|
+
def _driver_path() -> str:
|
|
70
|
+
"""Absolute path to the shared library bundled in this wheel."""
|
|
71
|
+
here = pathlib.Path(__file__).resolve().parent
|
|
72
|
+
for name in ("libspanner_adbc.so", "libspanner_adbc.dylib", "spanner_adbc.dll"):
|
|
73
|
+
candidate = here / name
|
|
74
|
+
if candidate.is_file():
|
|
75
|
+
return str(candidate)
|
|
76
|
+
raise RuntimeError(
|
|
77
|
+
"spanner_adbc: no bundled Spanner driver library found next to "
|
|
78
|
+
f"{here}. This usually means a source/sdist install without a matching "
|
|
79
|
+
"platform wheel; install a prebuilt wheel for your platform instead."
|
|
80
|
+
)
|
spanner_adbc/_options.py
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""Typed option-key constants for the Spanner ADBC driver.
|
|
2
|
+
|
|
3
|
+
These enums mirror the style of the BigQuery ADBC driver's ``DatabaseOptions`` /
|
|
4
|
+
``StatementOptions``: each member's ``.value`` is the raw option-key string the
|
|
5
|
+
driver understands. Every driver setting is passed as one of these keys — through
|
|
6
|
+
``db_kwargs=`` / ``conn_kwargs=`` on :func:`spanner_adbc.dbapi.connect`, or
|
|
7
|
+
``adbc_stmt_kwargs=`` on ``conn.cursor(...)`` — and these enums make that
|
|
8
|
+
discoverable and typo-safe.
|
|
9
|
+
|
|
10
|
+
Example::
|
|
11
|
+
|
|
12
|
+
import spanner_adbc.dbapi as spanner
|
|
13
|
+
from spanner_adbc import ConnectionOptions, DatabaseOptions, StatementOptions
|
|
14
|
+
|
|
15
|
+
with spanner.connect(
|
|
16
|
+
db_kwargs={DatabaseOptions.URI.value: "spanner:///projects/p/instances/i/databases/d"},
|
|
17
|
+
conn_kwargs={ConnectionOptions.READ_STALENESS.value: "max:10s"},
|
|
18
|
+
) as conn:
|
|
19
|
+
cur = conn.cursor(
|
|
20
|
+
adbc_stmt_kwargs={StatementOptions.ROWS_PER_BATCH.value: "1024"}
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
Standard ADBC option keys (``adbc.*``) live in ``adbc_driver_manager``; a few that
|
|
24
|
+
the driver honours are included here for convenience. The authoritative reference
|
|
25
|
+
for every option, its type, default and behaviour is ``docs/options.md``.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
import enum
|
|
29
|
+
|
|
30
|
+
__all__ = ["DatabaseOptions", "ConnectionOptions", "StatementOptions"]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class DatabaseOptions(enum.Enum):
|
|
34
|
+
"""Database-level options (set before connecting, via ``db_kwargs``)."""
|
|
35
|
+
|
|
36
|
+
#: **Standard ADBC.** A ``spanner://`` connection URI whose path is the fully-qualified
|
|
37
|
+
#: database path, ``spanner:///projects/<p>/instances/<i>/databases/<d>``. The scheme is
|
|
38
|
+
#: required; a bare path is rejected. Also settable as the ``uri`` kwarg on ``connect``.
|
|
39
|
+
URI = "uri"
|
|
40
|
+
#: Override the Spanner gRPC endpoint (e.g. an emulator ``host:port``).
|
|
41
|
+
ENDPOINT = "spanner.endpoint"
|
|
42
|
+
#: ``"true"`` to connect with anonymous credentials (emulator mode).
|
|
43
|
+
EMULATOR = "spanner.emulator"
|
|
44
|
+
#: Path to a service-account / credential JSON file.
|
|
45
|
+
KEYFILE = "spanner.auth.keyfile"
|
|
46
|
+
#: The same credential JSON passed inline as a string. Write-only: never
|
|
47
|
+
#: readable back via ``get_option``, and not accepted as a ``URI`` query
|
|
48
|
+
#: parameter (URIs get logged) — pass it here instead.
|
|
49
|
+
KEYFILE_JSON = "spanner.auth.keyfile_json"
|
|
50
|
+
#: A caller-supplied OAuth 2.0 bearer token, sent verbatim (no refresh).
|
|
51
|
+
#: Write-only: never readable back via ``get_option``, and not accepted as a
|
|
52
|
+
#: ``URI`` query parameter (URIs get logged) — pass it here instead.
|
|
53
|
+
ACCESS_TOKEN = "spanner.auth.access_token"
|
|
54
|
+
#: Service account to impersonate; setting it enables impersonation.
|
|
55
|
+
IMPERSONATE_TARGET_PRINCIPAL = "spanner.auth.impersonate.target_principal"
|
|
56
|
+
#: Optional impersonation delegation chain (comma-separated emails).
|
|
57
|
+
IMPERSONATE_DELEGATES = "spanner.auth.impersonate.delegates"
|
|
58
|
+
#: Optional OAuth scopes for the impersonated token (comma-separated).
|
|
59
|
+
IMPERSONATE_SCOPES = "spanner.auth.impersonate.scopes"
|
|
60
|
+
#: Optional impersonated-token lifetime, in seconds (default ``3600``).
|
|
61
|
+
IMPERSONATE_LIFETIME = "spanner.auth.impersonate.lifetime"
|
|
62
|
+
#: The GCP project charged for API quota (the ``x-goog-user-project`` header),
|
|
63
|
+
#: decoupled from the project owning the data. ``""`` unsets.
|
|
64
|
+
QUOTA_PROJECT = "spanner.auth.quota_project"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class ConnectionOptions(enum.Enum):
|
|
68
|
+
"""Connection-level options (set via ``conn_kwargs``)."""
|
|
69
|
+
|
|
70
|
+
#: Standard ADBC. ``"false"`` enters buffer-and-commit manual-transaction mode.
|
|
71
|
+
AUTOCOMMIT = "adbc.connection.autocommit"
|
|
72
|
+
#: Standard ADBC. ``"true"`` rejects all writes on the connection.
|
|
73
|
+
READONLY = "adbc.connection.readonly"
|
|
74
|
+
#: Standard ADBC. Isolation level for read/write transactions.
|
|
75
|
+
ISOLATION_LEVEL = "adbc.connection.transaction.isolation_level"
|
|
76
|
+
#: Read bound: one of ``exact:<duration>`` / ``max:<duration>`` / ``read:<rfc3339>`` /
|
|
77
|
+
#: ``min:<rfc3339>``.
|
|
78
|
+
READ_STALENESS = "spanner.read.staleness"
|
|
79
|
+
#: How ``TIMESTAMP`` maps to Arrow (``nanoseconds_error_on_overflow`` / ``microseconds``).
|
|
80
|
+
MAX_TIMESTAMP_PRECISION = "spanner.max_timestamp_precision"
|
|
81
|
+
#: Request priority (``low`` / ``medium`` / ``high``).
|
|
82
|
+
REQUEST_PRIORITY = "spanner.request.priority"
|
|
83
|
+
#: Request tag attached to every query/DML request.
|
|
84
|
+
REQUEST_TAG = "spanner.request.tag"
|
|
85
|
+
#: Transaction tag attached to every read/write transaction (connection-only).
|
|
86
|
+
TRANSACTION_TAG = "spanner.transaction.tag"
|
|
87
|
+
#: Directed-read replica selection for read-only queries.
|
|
88
|
+
DIRECTED_READ = "spanner.directed_read"
|
|
89
|
+
#: Query optimizer version.
|
|
90
|
+
QUERY_OPTIMIZER_VERSION = "spanner.query.optimizer_version"
|
|
91
|
+
#: Query optimizer statistics package.
|
|
92
|
+
QUERY_OPTIMIZER_STATISTICS_PACKAGE = "spanner.query.optimizer_statistics_package"
|
|
93
|
+
#: Maximum commit delay (``0..=500ms``) Spanner may add to batch commits.
|
|
94
|
+
MAX_COMMIT_DELAY = "spanner.commit.max_delay"
|
|
95
|
+
#: ``"true"`` requests commit statistics on read/write commits.
|
|
96
|
+
COMMIT_STATS = "spanner.commit_stats"
|
|
97
|
+
#: ``"true"`` excludes the transaction's writes from change-stream capture.
|
|
98
|
+
EXCLUDE_TXN_FROM_CHANGE_STREAMS = "spanner.transaction.exclude_from_change_streams"
|
|
99
|
+
#: Overall deadline (seconds) on a query's initial execution; ``0`` disables.
|
|
100
|
+
RPC_TIMEOUT_QUERY = "spanner.rpc.timeout_seconds.query"
|
|
101
|
+
#: Overall deadline (seconds) on each write operation; ``0`` disables.
|
|
102
|
+
RPC_TIMEOUT_UPDATE = "spanner.rpc.timeout_seconds.update"
|
|
103
|
+
#: Overall deadline (seconds) on each subsequent chunk fetch; ``0`` disables.
|
|
104
|
+
RPC_TIMEOUT_FETCH = "spanner.rpc.timeout_seconds.fetch"
|
|
105
|
+
#: Cap on retry attempts (first try + retries) for a retryable RPC.
|
|
106
|
+
RETRY_MAX_ATTEMPTS = "spanner.retry.max_attempts"
|
|
107
|
+
#: Cap (seconds) on total wall-clock time spent retrying a retryable RPC.
|
|
108
|
+
RETRY_MAX_ELAPSED_SECONDS = "spanner.retry.max_elapsed_seconds"
|
|
109
|
+
#: Initial delay (seconds) of the exponential backoff between retry attempts.
|
|
110
|
+
RETRY_BACKOFF_INITIAL_SECONDS = "spanner.retry.backoff.initial_seconds"
|
|
111
|
+
#: Ceiling (seconds) the growing retry backoff delay is truncated at.
|
|
112
|
+
RETRY_BACKOFF_MAX_SECONDS = "spanner.retry.backoff.max_seconds"
|
|
113
|
+
#: Per-attempt growth factor for the retry backoff delay.
|
|
114
|
+
RETRY_BACKOFF_MULTIPLIER = "spanner.retry.backoff.multiplier"
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class StatementOptions(enum.Enum):
|
|
118
|
+
"""Statement-level options (set via ``conn.cursor(adbc_stmt_kwargs=...)``).
|
|
119
|
+
|
|
120
|
+
The options shared with :class:`ConnectionOptions` are per-statement overrides
|
|
121
|
+
of the value inherited from the connection.
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
#: Rows converted into each streamed Arrow ``RecordBatch`` (default ``8192``).
|
|
125
|
+
ROWS_PER_BATCH = "spanner.rows_per_batch"
|
|
126
|
+
#: ``"true"`` runs ``execute_partitions`` partitions on Data Boost.
|
|
127
|
+
DATA_BOOST = "spanner.data_boost"
|
|
128
|
+
#: Per-statement override of :attr:`ConnectionOptions.READ_STALENESS`.
|
|
129
|
+
READ_STALENESS = "spanner.read.staleness"
|
|
130
|
+
#: Per-statement override of :attr:`ConnectionOptions.MAX_TIMESTAMP_PRECISION`.
|
|
131
|
+
MAX_TIMESTAMP_PRECISION = "spanner.max_timestamp_precision"
|
|
132
|
+
#: Per-statement override of :attr:`ConnectionOptions.REQUEST_PRIORITY`.
|
|
133
|
+
REQUEST_PRIORITY = "spanner.request.priority"
|
|
134
|
+
#: Per-statement override of :attr:`ConnectionOptions.REQUEST_TAG`.
|
|
135
|
+
REQUEST_TAG = "spanner.request.tag"
|
|
136
|
+
#: Per-statement override of :attr:`ConnectionOptions.DIRECTED_READ`.
|
|
137
|
+
DIRECTED_READ = "spanner.directed_read"
|
|
138
|
+
#: Per-statement override of :attr:`ConnectionOptions.MAX_COMMIT_DELAY`.
|
|
139
|
+
MAX_COMMIT_DELAY = "spanner.commit.max_delay"
|
|
140
|
+
#: Per-statement override of :attr:`ConnectionOptions.COMMIT_STATS`.
|
|
141
|
+
COMMIT_STATS = "spanner.commit_stats"
|
|
142
|
+
#: Per-statement override of
|
|
143
|
+
#: :attr:`ConnectionOptions.EXCLUDE_TXN_FROM_CHANGE_STREAMS`.
|
|
144
|
+
EXCLUDE_TXN_FROM_CHANGE_STREAMS = "spanner.transaction.exclude_from_change_streams"
|
|
145
|
+
#: Per-statement override of :attr:`ConnectionOptions.QUERY_OPTIMIZER_VERSION`.
|
|
146
|
+
QUERY_OPTIMIZER_VERSION = "spanner.query.optimizer_version"
|
|
147
|
+
#: Per-statement override of :attr:`ConnectionOptions.QUERY_OPTIMIZER_STATISTICS_PACKAGE`.
|
|
148
|
+
QUERY_OPTIMIZER_STATISTICS_PACKAGE = "spanner.query.optimizer_statistics_package"
|
|
149
|
+
#: Per-statement override of :attr:`ConnectionOptions.RPC_TIMEOUT_QUERY`.
|
|
150
|
+
RPC_TIMEOUT_QUERY = "spanner.rpc.timeout_seconds.query"
|
|
151
|
+
#: Per-statement override of :attr:`ConnectionOptions.RPC_TIMEOUT_UPDATE`.
|
|
152
|
+
RPC_TIMEOUT_UPDATE = "spanner.rpc.timeout_seconds.update"
|
|
153
|
+
#: Per-statement override of :attr:`ConnectionOptions.RPC_TIMEOUT_FETCH`.
|
|
154
|
+
RPC_TIMEOUT_FETCH = "spanner.rpc.timeout_seconds.fetch"
|
|
155
|
+
#: Per-statement override of :attr:`ConnectionOptions.RETRY_MAX_ATTEMPTS`.
|
|
156
|
+
RETRY_MAX_ATTEMPTS = "spanner.retry.max_attempts"
|
|
157
|
+
#: Per-statement override of :attr:`ConnectionOptions.RETRY_MAX_ELAPSED_SECONDS`.
|
|
158
|
+
RETRY_MAX_ELAPSED_SECONDS = "spanner.retry.max_elapsed_seconds"
|
|
159
|
+
#: Per-statement override of :attr:`ConnectionOptions.RETRY_BACKOFF_INITIAL_SECONDS`.
|
|
160
|
+
RETRY_BACKOFF_INITIAL_SECONDS = "spanner.retry.backoff.initial_seconds"
|
|
161
|
+
#: Per-statement override of :attr:`ConnectionOptions.RETRY_BACKOFF_MAX_SECONDS`.
|
|
162
|
+
RETRY_BACKOFF_MAX_SECONDS = "spanner.retry.backoff.max_seconds"
|
|
163
|
+
#: Per-statement override of :attr:`ConnectionOptions.RETRY_BACKOFF_MULTIPLIER`.
|
|
164
|
+
RETRY_BACKOFF_MULTIPLIER = "spanner.retry.backoff.multiplier"
|
|
165
|
+
#: How bound Arrow columns pair with ``@name`` parameters (``"true"`` = by name).
|
|
166
|
+
BIND_BY_NAME = "adbc.statement.bind_by_name"
|
|
167
|
+
#: ``"true"`` routes an autocommit bulk ingest's chunks through Spanner's
|
|
168
|
+
#: non-atomic **BatchWrite** RPC instead of a write-only transaction.
|
|
169
|
+
#: Ignored in manual-transaction mode.
|
|
170
|
+
INGEST_BATCH_WRITE = "spanner.ingest.batch_write"
|
|
171
|
+
#: ``"true"`` runs the statement's DML as Partitioned DML: Spanner splits it
|
|
172
|
+
#: across partitions and applies each independently, so a large ``UPDATE`` or
|
|
173
|
+
#: ``DELETE`` never hits the per-commit mutation limit. Not atomic — a partition
|
|
174
|
+
#: may be applied more than once, so the statement must be idempotent — and
|
|
175
|
+
#: ``rowcount`` is a lower bound, not an exact count.
|
|
176
|
+
DML_PARTITIONED = "spanner.dml.partitioned"
|
spanner_adbc/_version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.8.0"
|
spanner_adbc/dbapi.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""DBAPI 2.0 (PEP 249) interface for the Spanner ADBC driver.
|
|
2
|
+
|
|
3
|
+
This is the layer most users want: it returns a standard DBAPI connection with
|
|
4
|
+
cursors, plus the ADBC Arrow extensions (``fetch_arrow_table``, ``fetch_df``,
|
|
5
|
+
``adbc_ingest``) that pandas / polars / DuckDB consume directly.
|
|
6
|
+
|
|
7
|
+
import spanner_adbc.dbapi as spanner
|
|
8
|
+
with spanner.connect(uri="spanner:///projects/p/instances/i/databases/d") as conn:
|
|
9
|
+
df = conn.cursor().execute("SELECT * FROM Singers").fetch_df()
|
|
10
|
+
|
|
11
|
+
Note: DBAPI is autocommit-off by default, which puts this driver into its
|
|
12
|
+
buffer-and-commit manual-transaction mode — call ``conn.commit()`` to apply DML.
|
|
13
|
+
Pass ``autocommit=True`` to keep the driver's default single-statement mode.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import typing
|
|
17
|
+
|
|
18
|
+
import adbc_driver_manager.dbapi
|
|
19
|
+
|
|
20
|
+
from . import (
|
|
21
|
+
ENTRYPOINT,
|
|
22
|
+
ConnectionOptions,
|
|
23
|
+
DatabaseOptions,
|
|
24
|
+
StatementOptions,
|
|
25
|
+
_driver_path,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"connect",
|
|
30
|
+
"DatabaseOptions",
|
|
31
|
+
"ConnectionOptions",
|
|
32
|
+
"StatementOptions",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def connect(
|
|
37
|
+
db_kwargs: typing.Optional[typing.Mapping[str, str]] = None,
|
|
38
|
+
conn_kwargs: typing.Optional[typing.Mapping[str, str]] = None,
|
|
39
|
+
autocommit: bool = False,
|
|
40
|
+
) -> adbc_driver_manager.dbapi.Connection:
|
|
41
|
+
"""Open a DBAPI 2.0 connection to a Spanner database.
|
|
42
|
+
|
|
43
|
+
Parameters
|
|
44
|
+
----------
|
|
45
|
+
db_kwargs:
|
|
46
|
+
Database-level driver options, keyed by their raw option strings — best
|
|
47
|
+
written with the :class:`DatabaseOptions` constants, e.g.
|
|
48
|
+
``{DatabaseOptions.URI.value: "spanner:///projects/<p>/instances/<i>/databases/<d>"}``
|
|
49
|
+
(see src/lib.rs / docs/options.md). The ``uri`` option requires the
|
|
50
|
+
``spanner://`` scheme; a bare database path is rejected. Credentials, the
|
|
51
|
+
emulator, endpoint overrides, and every other setting are passed here as
|
|
52
|
+
their raw keys — and the secret-holding ``KEYFILE_JSON`` / ``ACCESS_TOKEN``
|
|
53
|
+
options must be, since a ``uri`` may not carry them as query parameters.
|
|
54
|
+
conn_kwargs:
|
|
55
|
+
Connection-level options, keyed with the :class:`ConnectionOptions`
|
|
56
|
+
constants (``adbc.connection.*`` / ``spanner.*``).
|
|
57
|
+
autocommit:
|
|
58
|
+
Toggles PEP 249 autocommit; ``False`` (the default) puts the driver into
|
|
59
|
+
its buffer-and-commit manual-transaction mode.
|
|
60
|
+
"""
|
|
61
|
+
# The driver manager builds and owns the database/connection handles here and
|
|
62
|
+
# tears them down if the connection fails, so no manual cleanup is needed.
|
|
63
|
+
return adbc_driver_manager.dbapi.connect(
|
|
64
|
+
driver=_driver_path(),
|
|
65
|
+
entrypoint=ENTRYPOINT,
|
|
66
|
+
db_kwargs=dict(db_kwargs) if db_kwargs else None,
|
|
67
|
+
conn_kwargs=conn_kwargs,
|
|
68
|
+
autocommit=autocommit,
|
|
69
|
+
)
|
spanner_adbc/manifest.py
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
"""Generate and install an ADBC *driver manifest* for the bundled Spanner driver.
|
|
2
|
+
|
|
3
|
+
A driver manifest is a small TOML file that lets any ADBC driver manager find a
|
|
4
|
+
driver **by name** instead of by an explicit path to a shared library::
|
|
5
|
+
|
|
6
|
+
adbc_driver_manager.dbapi.connect(driver="spanner", uri="spanner:///...")
|
|
7
|
+
|
|
8
|
+
and — because a driver manager with no ``driver`` option falls back to the
|
|
9
|
+
*scheme* of the ``uri`` option, and this driver's connection URIs already use
|
|
10
|
+
the ``spanner://`` scheme — by URI alone::
|
|
11
|
+
|
|
12
|
+
adbc_driver_manager.dbapi.connect(uri="spanner:///projects/p/instances/i/databases/d")
|
|
13
|
+
|
|
14
|
+
See the ADBC specification, ``docs/source/format/driver_manifests.rst``.
|
|
15
|
+
|
|
16
|
+
Why this is not a plain data file in the wheel
|
|
17
|
+
----------------------------------------------
|
|
18
|
+
|
|
19
|
+
The manifest's ``Driver.shared`` value is passed to ``dlopen``/``LoadLibraryExW``
|
|
20
|
+
**verbatim**: neither the C nor the Rust driver manager resolves it relative to
|
|
21
|
+
the manifest file. So a manifest must contain the *absolute* path of the shared
|
|
22
|
+
library, which is only known once the wheel is installed. A manifest shipped
|
|
23
|
+
pre-baked in the wheel would therefore point nowhere. Instead this module writes
|
|
24
|
+
one at install time::
|
|
25
|
+
|
|
26
|
+
python -m spanner_adbc.manifest install
|
|
27
|
+
# or, equivalently, the console script:
|
|
28
|
+
spanner-adbc-install-manifest
|
|
29
|
+
|
|
30
|
+
By default it writes to a directory the driver manager already searches (see
|
|
31
|
+
:func:`default_install_dir`).
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
import argparse
|
|
37
|
+
import os
|
|
38
|
+
import pathlib
|
|
39
|
+
import platform
|
|
40
|
+
import sys
|
|
41
|
+
import sysconfig
|
|
42
|
+
import typing
|
|
43
|
+
|
|
44
|
+
from . import ENTRYPOINT, __version__, _driver_path
|
|
45
|
+
|
|
46
|
+
__all__ = [
|
|
47
|
+
"MANIFEST_NAME",
|
|
48
|
+
"default_install_dir",
|
|
49
|
+
"install",
|
|
50
|
+
"manifest_text",
|
|
51
|
+
"platform_tuple",
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
#: The manifest's file name. The *base name* is what a driver manager matches
|
|
55
|
+
#: against the driver name (and against a connection URI's scheme), so calling
|
|
56
|
+
#: it anything else breaks ``driver="spanner"``.
|
|
57
|
+
MANIFEST_NAME = "spanner.toml"
|
|
58
|
+
|
|
59
|
+
# Architecture names from the spec's authoritative platform-tuple table, keyed by
|
|
60
|
+
# the values `platform.machine()` reports. Only the architectures this project
|
|
61
|
+
# builds for are listed; anything else is reported as an error rather than
|
|
62
|
+
# guessed at.
|
|
63
|
+
_ARCHES = {
|
|
64
|
+
"x86_64": "amd64",
|
|
65
|
+
"amd64": "amd64",
|
|
66
|
+
"aarch64": "arm64",
|
|
67
|
+
"arm64": "arm64",
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
_OSES = {
|
|
71
|
+
"linux": "linux",
|
|
72
|
+
"darwin": "macos",
|
|
73
|
+
"win32": "windows",
|
|
74
|
+
"cygwin": "windows",
|
|
75
|
+
"freebsd": "freebsd",
|
|
76
|
+
"openbsd": "openbsd",
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _is_musl() -> bool:
|
|
81
|
+
"""Whether this interpreter runs against musl libc rather than glibc.
|
|
82
|
+
|
|
83
|
+
The driver manager appends ``_musl`` to the platform tuple when *it* was
|
|
84
|
+
compiled against musl. The driver manager reaching this code is the
|
|
85
|
+
``adbc_driver_manager`` wheel installed next to this package, so it matches
|
|
86
|
+
the interpreter's own libc.
|
|
87
|
+
"""
|
|
88
|
+
if sys.platform != "linux":
|
|
89
|
+
return False
|
|
90
|
+
# The platform tag is the most direct signal: CPython built on Alpine
|
|
91
|
+
# reports e.g. "linux-x86_64" but its wheel tags are musllinux_*, and
|
|
92
|
+
# `sysconfig.get_platform()` on a musl build carries "musl" on the
|
|
93
|
+
# distributions that set it.
|
|
94
|
+
if "musl" in (sysconfig.get_platform() or ""):
|
|
95
|
+
return True
|
|
96
|
+
try:
|
|
97
|
+
# glibc reports e.g. ("glibc", "2.35"); a musl build reports ("", "").
|
|
98
|
+
name, _version = platform.libc_ver()
|
|
99
|
+
except OSError: # pragma: no cover - platform.libc_ver is very forgiving
|
|
100
|
+
return False
|
|
101
|
+
return not name
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def platform_tuple() -> str:
|
|
105
|
+
"""The ``<os>_<arch>[_<env>]`` tuple the driver manager looks up.
|
|
106
|
+
|
|
107
|
+
Mirrors ``InternalAdbcCurrentArch()`` in the C driver manager
|
|
108
|
+
(``c/driver_manager/adbc_driver_manager.cc``) and ``arch_triplet()`` in the
|
|
109
|
+
Rust one.
|
|
110
|
+
"""
|
|
111
|
+
os_name = _OSES.get(sys.platform)
|
|
112
|
+
if os_name is None and sys.platform.startswith("freebsd"):
|
|
113
|
+
os_name = "freebsd"
|
|
114
|
+
machine = platform.machine().lower()
|
|
115
|
+
arch = _ARCHES.get(machine)
|
|
116
|
+
if os_name is None or arch is None:
|
|
117
|
+
raise RuntimeError(
|
|
118
|
+
"spanner_adbc: cannot derive an ADBC platform tuple for "
|
|
119
|
+
f"sys.platform={sys.platform!r} machine={platform.machine()!r}; "
|
|
120
|
+
"this platform is not one the Spanner driver ships binaries for."
|
|
121
|
+
)
|
|
122
|
+
suffix = "_musl" if os_name == "linux" and _is_musl() else ""
|
|
123
|
+
return f"{os_name}_{arch}{suffix}"
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _toml_string(value: str) -> str:
|
|
127
|
+
"""Render a TOML basic string.
|
|
128
|
+
|
|
129
|
+
Basic (double-quoted) strings are used rather than literal ones because a
|
|
130
|
+
Windows install path contains backslashes *and* may contain a single quote,
|
|
131
|
+
which a TOML literal string cannot escape.
|
|
132
|
+
"""
|
|
133
|
+
escaped = value.replace("\\", "\\\\").replace('"', '\\"')
|
|
134
|
+
return f'"{escaped}"'
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def manifest_text(library: typing.Union[str, "os.PathLike[str]", None] = None) -> str:
|
|
138
|
+
"""Render a driver manifest for this installation.
|
|
139
|
+
|
|
140
|
+
Parameters
|
|
141
|
+
----------
|
|
142
|
+
library:
|
|
143
|
+
Absolute path to the driver shared library. Defaults to the library
|
|
144
|
+
bundled in this wheel.
|
|
145
|
+
|
|
146
|
+
Notes
|
|
147
|
+
-----
|
|
148
|
+
``Driver.shared`` is emitted as a plain *string* rather than a table of
|
|
149
|
+
platform tuples. The spec allows either ("``Driver.shared`` ... must either
|
|
150
|
+
be a string (single path) or a table of platform-specific paths"), and a
|
|
151
|
+
manifest generated for one installed wheel describes exactly one platform,
|
|
152
|
+
so the string form cannot disagree with the driver manager about how this
|
|
153
|
+
platform is spelled.
|
|
154
|
+
"""
|
|
155
|
+
path = pathlib.Path(library) if library is not None else pathlib.Path(_driver_path())
|
|
156
|
+
path = path.resolve()
|
|
157
|
+
return f"""\
|
|
158
|
+
# ADBC driver manifest for Google Cloud Spanner.
|
|
159
|
+
#
|
|
160
|
+
# Generated by `python -m spanner_adbc.manifest install` for the library
|
|
161
|
+
# bundled in the spanner-adbc wheel. Regenerate it after upgrading,
|
|
162
|
+
# reinstalling or moving the Python environment: the path below is absolute and
|
|
163
|
+
# is passed to the dynamic loader verbatim.
|
|
164
|
+
#
|
|
165
|
+
# Platform tuple at generation time: {platform_tuple()}
|
|
166
|
+
|
|
167
|
+
manifest_version = 1
|
|
168
|
+
|
|
169
|
+
name = "Google Cloud Spanner"
|
|
170
|
+
publisher = "fornwall"
|
|
171
|
+
license = "Apache-2.0"
|
|
172
|
+
version = {_toml_string(__version__)}
|
|
173
|
+
url = "https://github.com/fornwall/spanner-adbc"
|
|
174
|
+
source = "spanner-adbc (Python wheel)"
|
|
175
|
+
|
|
176
|
+
[ADBC]
|
|
177
|
+
version = "1.1.0"
|
|
178
|
+
|
|
179
|
+
[Driver]
|
|
180
|
+
entrypoint = {_toml_string(ENTRYPOINT)}
|
|
181
|
+
shared = {_toml_string(str(path))}
|
|
182
|
+
"""
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def default_install_dir() -> pathlib.Path:
|
|
186
|
+
"""The directory to install the manifest into by default.
|
|
187
|
+
|
|
188
|
+
Inside a virtual environment this is ``<sys.prefix>/etc/adbc/drivers``,
|
|
189
|
+
which the Python driver manager adds to its search paths automatically
|
|
190
|
+
(``adbc_driver_manager/_lib.pyx`` does this whenever
|
|
191
|
+
``sys.prefix != sys.base_prefix``). That keeps the manifest scoped to the
|
|
192
|
+
same environment as the wheel it points into.
|
|
193
|
+
|
|
194
|
+
Outside a venv it is the driver manager's *user* config directory, which is
|
|
195
|
+
searched when the ``SEARCH_USER`` load flag is set (it is, by default):
|
|
196
|
+
|
|
197
|
+
* Linux: ``$XDG_CONFIG_HOME/adbc/drivers``, else ``~/.config/adbc/drivers``
|
|
198
|
+
* macOS: ``~/Library/Application Support/ADBC/Drivers``
|
|
199
|
+
* Windows: ``%LOCALAPPDATA%\\ADBC\\Drivers``
|
|
200
|
+
"""
|
|
201
|
+
if sys.prefix != sys.base_prefix:
|
|
202
|
+
return pathlib.Path(sys.prefix) / "etc" / "adbc" / "drivers"
|
|
203
|
+
|
|
204
|
+
if sys.platform == "win32":
|
|
205
|
+
local = os.environ.get("LOCALAPPDATA")
|
|
206
|
+
if not local:
|
|
207
|
+
raise RuntimeError(
|
|
208
|
+
"spanner_adbc: %LOCALAPPDATA% is not set, so the driver "
|
|
209
|
+
"manager's user config directory cannot be located; pass an "
|
|
210
|
+
"explicit directory instead."
|
|
211
|
+
)
|
|
212
|
+
return pathlib.Path(local) / "ADBC" / "Drivers"
|
|
213
|
+
|
|
214
|
+
if sys.platform == "darwin":
|
|
215
|
+
return pathlib.Path.home() / "Library" / "Application Support" / "ADBC" / "Drivers"
|
|
216
|
+
|
|
217
|
+
xdg = os.environ.get("XDG_CONFIG_HOME")
|
|
218
|
+
base = pathlib.Path(xdg) if xdg else pathlib.Path.home() / ".config"
|
|
219
|
+
return base / "adbc" / "drivers"
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def install(
|
|
223
|
+
directory: typing.Union[str, "os.PathLike[str]", None] = None,
|
|
224
|
+
*,
|
|
225
|
+
library: typing.Union[str, "os.PathLike[str]", None] = None,
|
|
226
|
+
) -> pathlib.Path:
|
|
227
|
+
"""Write ``spanner.toml`` into *directory* and return the path written.
|
|
228
|
+
|
|
229
|
+
*directory* defaults to :func:`default_install_dir` and is created if it
|
|
230
|
+
does not exist. An existing manifest is overwritten, so re-running this
|
|
231
|
+
after an upgrade is the supported way to refresh a stale path.
|
|
232
|
+
"""
|
|
233
|
+
target_dir = pathlib.Path(directory) if directory is not None else default_install_dir()
|
|
234
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
235
|
+
target = target_dir / MANIFEST_NAME
|
|
236
|
+
target.write_text(manifest_text(library), encoding="utf-8")
|
|
237
|
+
return target
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def main(argv: typing.Optional[typing.Sequence[str]] = None) -> int:
|
|
241
|
+
parser = argparse.ArgumentParser(
|
|
242
|
+
prog="python -m spanner_adbc.manifest",
|
|
243
|
+
description=(
|
|
244
|
+
"Install an ADBC driver manifest so the Spanner driver resolves by "
|
|
245
|
+
'name (driver="spanner") or by URI scheme (uri="spanner:///...").'
|
|
246
|
+
),
|
|
247
|
+
)
|
|
248
|
+
sub = parser.add_subparsers(dest="command")
|
|
249
|
+
|
|
250
|
+
p_install = sub.add_parser("install", help="write the manifest (default)")
|
|
251
|
+
p_install.add_argument(
|
|
252
|
+
"-d",
|
|
253
|
+
"--dir",
|
|
254
|
+
dest="directory",
|
|
255
|
+
default=None,
|
|
256
|
+
help=(
|
|
257
|
+
f"directory to write {MANIFEST_NAME} into (default: a directory the "
|
|
258
|
+
"driver manager searches; see the `path` subcommand)"
|
|
259
|
+
),
|
|
260
|
+
)
|
|
261
|
+
sub.add_parser("print", help="print the manifest to stdout without writing it")
|
|
262
|
+
sub.add_parser("path", help="print the default install path and exit")
|
|
263
|
+
|
|
264
|
+
args = parser.parse_args(argv)
|
|
265
|
+
command = args.command or "install"
|
|
266
|
+
|
|
267
|
+
if command == "print":
|
|
268
|
+
sys.stdout.write(manifest_text())
|
|
269
|
+
return 0
|
|
270
|
+
if command == "path":
|
|
271
|
+
print(default_install_dir() / MANIFEST_NAME)
|
|
272
|
+
return 0
|
|
273
|
+
|
|
274
|
+
written = install(getattr(args, "directory", None))
|
|
275
|
+
print(f"Wrote {written}")
|
|
276
|
+
print(
|
|
277
|
+
'The Spanner driver now resolves as driver="spanner" and from '
|
|
278
|
+
'uri="spanner:///..." for this environment.'
|
|
279
|
+
)
|
|
280
|
+
return 0
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
if __name__ == "__main__": # pragma: no cover - exercised via the console script
|
|
284
|
+
raise SystemExit(main())
|
spanner_adbc/py.typed
ADDED
|
File without changes
|
|
Binary file
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: spanner-adbc
|
|
3
|
+
Version: 0.8.0
|
|
4
|
+
Summary: ADBC (Arrow Database Connectivity) driver for Google Cloud Spanner
|
|
5
|
+
Author-email: Fredrik Fornwall <fredrik@fornwall.net>
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/fornwall/spanner-adbc
|
|
8
|
+
Project-URL: Repository, https://github.com/fornwall/spanner-adbc
|
|
9
|
+
Keywords: adbc,arrow,spanner,database,gcp
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Classifier: Programming Language :: Rust
|
|
18
|
+
Classifier: Topic :: Database
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: adbc-driver-manager>=1.0
|
|
23
|
+
Provides-Extra: dbapi
|
|
24
|
+
Requires-Dist: adbc-driver-manager[dbapi]>=1.0; extra == "dbapi"
|
|
25
|
+
Requires-Dist: pyarrow>=14.0.1; extra == "dbapi"
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
# spanner-adbc
|
|
29
|
+
|
|
30
|
+
[](https://pypi.org/project/spanner-adbc/)
|
|
31
|
+
[](https://pypi.org/project/spanner-adbc/)
|
|
32
|
+
[](https://pypi.org/project/spanner-adbc/#files)
|
|
33
|
+
[](https://github.com/fornwall/spanner-adbc/blob/main/LICENSE)
|
|
34
|
+
[](https://github.com/fornwall/spanner-adbc/actions/workflows/libraries.yml)
|
|
35
|
+
|
|
36
|
+
A Python [ADBC](https://arrow.apache.org/adbc/) driver for **Google Cloud Spanner**.
|
|
37
|
+
|
|
38
|
+
Query Spanner through [DBAPI 2.0](https://peps.python.org/pep-0249/) and fetch
|
|
39
|
+
[Apache Arrow](https://arrow.apache.org/) results for pandas, Polars, DuckDB, or PyArrow.
|
|
40
|
+
|
|
41
|
+
## Install
|
|
42
|
+
|
|
43
|
+
Requires Python 3.11 or later. Wheels bundle the native driver; see
|
|
44
|
+
[Supported platforms](#supported-platforms) for OS requirements.
|
|
45
|
+
|
|
46
|
+
```sh
|
|
47
|
+
pip install "spanner-adbc[dbapi]" # includes PyArrow and pandas
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
For the low-level ADBC API without DataFrame dependencies, install `spanner-adbc`.
|
|
51
|
+
The Python import name is `spanner_adbc`.
|
|
52
|
+
|
|
53
|
+
## Quickstart
|
|
54
|
+
|
|
55
|
+
This example assumes an existing `Singers` table with `SingerId` and `FirstName` columns.
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
import spanner_adbc.dbapi as spanner
|
|
59
|
+
from spanner_adbc import DatabaseOptions
|
|
60
|
+
|
|
61
|
+
with spanner.connect(
|
|
62
|
+
db_kwargs={DatabaseOptions.URI.value: "spanner:///projects/my-project/instances/my-instance/databases/my-db"},
|
|
63
|
+
) as conn:
|
|
64
|
+
with conn.cursor() as cur:
|
|
65
|
+
cur.execute("SELECT SingerId, FirstName FROM Singers")
|
|
66
|
+
df = cur.fetch_df() # -> pandas.DataFrame
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Use GoogleSQL `@name` parameters with `cur.execute(...)`; `?` placeholders are unsupported.
|
|
70
|
+
`fetchone()` and `fetchall()` return Python rows; Arrow and DataFrame helpers are shown below.
|
|
71
|
+
|
|
72
|
+
## Driver manifest (`driver="spanner"`)
|
|
73
|
+
|
|
74
|
+
The package's `spanner.connect()` loads its bundled library directly. To use the generic
|
|
75
|
+
`adbc_driver_manager` by driver name, install an [ADBC driver manifest][manifests].
|
|
76
|
+
Use a current driver manager; Python manifest support requires
|
|
77
|
+
[version 1.8.0 or later](https://arrow.apache.org/blog/2025/09/12/adbc-20-release/).
|
|
78
|
+
|
|
79
|
+
```sh
|
|
80
|
+
pip install --upgrade adbc-driver-manager
|
|
81
|
+
python -m spanner_adbc.manifest install
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The equivalent console command is `spanner-adbc-install-manifest`.
|
|
85
|
+
|
|
86
|
+
```python docs-test: skip
|
|
87
|
+
import adbc_driver_manager.dbapi
|
|
88
|
+
|
|
89
|
+
with adbc_driver_manager.dbapi.connect(
|
|
90
|
+
driver="spanner",
|
|
91
|
+
uri="spanner:///projects/my-project/instances/my-instance/databases/my-db",
|
|
92
|
+
) as conn:
|
|
93
|
+
...
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Current driver managers can also infer `spanner` from the URI scheme when `driver` is omitted.
|
|
97
|
+
|
|
98
|
+
- Re-run the installer after upgrading, reinstalling, or moving the environment: the manifest
|
|
99
|
+
contains the library's absolute path.
|
|
100
|
+
- The default directory is `<sys.prefix>/etc/adbc/drivers` inside a virtual environment, or the
|
|
101
|
+
platform's user configuration directory otherwise. `python -m spanner_adbc.manifest path`
|
|
102
|
+
prints the target path.
|
|
103
|
+
- Use `install --dir /path/to/drivers` for a custom directory on `ADBC_DRIVER_PATH`.
|
|
104
|
+
- For a standalone shared library, edit the `Driver.shared` paths in the repository's
|
|
105
|
+
[spanner.toml][manifest-file].
|
|
106
|
+
|
|
107
|
+
[manifests]: https://arrow.apache.org/adbc/current/format/driver_manifests.html
|
|
108
|
+
[manifest-file]: https://github.com/fornwall/spanner-adbc/blob/main/spanner.toml
|
|
109
|
+
|
|
110
|
+
## Authentication
|
|
111
|
+
|
|
112
|
+
With no credential options, the driver uses
|
|
113
|
+
[Application Default Credentials](https://cloud.google.com/docs/authentication/application-default-credentials):
|
|
114
|
+
run `gcloud auth application-default login` locally, set `GOOGLE_APPLICATION_CREDENTIALS`, or use
|
|
115
|
+
an attached service account on Google Cloud.
|
|
116
|
+
|
|
117
|
+
Pass explicit credentials in `db_kwargs` alongside `DatabaseOptions.URI.value`:
|
|
118
|
+
|
|
119
|
+
| Option | Value |
|
|
120
|
+
| --- | --- |
|
|
121
|
+
| `DatabaseOptions.KEYFILE` | Credential JSON file path |
|
|
122
|
+
| `DatabaseOptions.KEYFILE_JSON` | Credential JSON contents |
|
|
123
|
+
| `DatabaseOptions.ACCESS_TOKEN` | OAuth bearer token; never refreshed |
|
|
124
|
+
| `DatabaseOptions.IMPERSONATE_TARGET_PRINCIPAL` | Service account to impersonate using ADC or explicit base credentials |
|
|
125
|
+
|
|
126
|
+
Use each enum member's `.value` as the key. `ACCESS_TOKEN` cannot be combined with key-file
|
|
127
|
+
credentials or impersonation. See the [option reference][options] for scopes and other auth settings.
|
|
128
|
+
|
|
129
|
+
For the [Spanner emulator](https://cloud.google.com/spanner/docs/emulator), use anonymous mode:
|
|
130
|
+
|
|
131
|
+
```python docs-test: skip
|
|
132
|
+
import spanner_adbc.dbapi as spanner
|
|
133
|
+
from spanner_adbc import DatabaseOptions
|
|
134
|
+
|
|
135
|
+
with spanner.connect(db_kwargs={
|
|
136
|
+
DatabaseOptions.URI.value: "spanner:///projects/p/instances/i/databases/d",
|
|
137
|
+
DatabaseOptions.ENDPOINT.value: "localhost:9010",
|
|
138
|
+
DatabaseOptions.EMULATOR.value: "true",
|
|
139
|
+
}) as conn:
|
|
140
|
+
...
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Emulator mode rejects explicit credential options; ambient ADC is ignored.
|
|
144
|
+
|
|
145
|
+
## Options
|
|
146
|
+
|
|
147
|
+
| `connect()` argument | Purpose |
|
|
148
|
+
| --- | --- |
|
|
149
|
+
| `db_kwargs` | Database options, including the required `uri` |
|
|
150
|
+
| `conn_kwargs` | Connection options |
|
|
151
|
+
| `autocommit` | `False` by default; see [Transactions](#transactions) |
|
|
152
|
+
|
|
153
|
+
Use `DatabaseOptions`, `ConnectionOptions`, and `StatementOptions` enums, or raw option keys.
|
|
154
|
+
The [option reference][options] lists types, defaults, and accepted values. Set cursor options with
|
|
155
|
+
`conn.cursor(adbc_stmt_kwargs={...})` or `cur.adbc_statement.set_options(**{...})`.
|
|
156
|
+
|
|
157
|
+
[options]: https://github.com/fornwall/spanner-adbc/blob/main/docs/options.md
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
import spanner_adbc.dbapi as spanner
|
|
161
|
+
from spanner_adbc import ConnectionOptions, DatabaseOptions, StatementOptions
|
|
162
|
+
|
|
163
|
+
with spanner.connect(
|
|
164
|
+
db_kwargs={DatabaseOptions.URI.value: "spanner:///projects/p/instances/i/databases/d"},
|
|
165
|
+
conn_kwargs={ConnectionOptions.READ_STALENESS.value: "max:10s"},
|
|
166
|
+
autocommit=True, # bounded staleness on a single-use read
|
|
167
|
+
) as conn:
|
|
168
|
+
with conn.cursor(
|
|
169
|
+
adbc_stmt_kwargs={StatementOptions.ROWS_PER_BATCH.value: "1024"}
|
|
170
|
+
) as cur:
|
|
171
|
+
cur.execute("SELECT * FROM Singers")
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
In manual query transactions, `max:<duration>` and `min:<timestamp>` bounds become exact
|
|
175
|
+
staleness and a fixed read timestamp respectively. Set `ConnectionOptions.READONLY.value` to
|
|
176
|
+
`"true"` to reject DML, DDL, and ingest; rollback remains available.
|
|
177
|
+
|
|
178
|
+
## Transactions
|
|
179
|
+
|
|
180
|
+
DBAPI defaults to `autocommit=False`. A manual transaction accepts either queries or writes,
|
|
181
|
+
chosen by its first query or write; mixing them raises `adbc_driver_manager.ProgrammingError` until
|
|
182
|
+
commit or rollback. Queries share a snapshot. DML is buffered until `conn.commit()`, so queries
|
|
183
|
+
cannot read buffered writes.
|
|
184
|
+
|
|
185
|
+
DDL always executes immediately: rollback cannot undo it, and it runs before buffered DML.
|
|
186
|
+
Use `autocommit=True` for immediately committed DML, including `THEN RETURN` statements.
|
|
187
|
+
See [transactions](https://github.com/fornwall/spanner-adbc/blob/main/docs/transactions.md) for
|
|
188
|
+
bulk-ingest and partitioned-DML behavior.
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
import spanner_adbc.dbapi as spanner
|
|
192
|
+
from adbc_driver_manager import ProgrammingError
|
|
193
|
+
from spanner_adbc import DatabaseOptions
|
|
194
|
+
|
|
195
|
+
with spanner.connect(
|
|
196
|
+
db_kwargs={DatabaseOptions.URI.value: "spanner:///projects/my-project/instances/my-instance/databases/my-db"},
|
|
197
|
+
) as conn:
|
|
198
|
+
with conn.cursor() as cur:
|
|
199
|
+
# DDL applies immediately.
|
|
200
|
+
cur.execute("DROP TABLE IF EXISTS Albums")
|
|
201
|
+
cur.execute("CREATE TABLE Albums (Id INT64 NOT NULL) PRIMARY KEY (Id)")
|
|
202
|
+
|
|
203
|
+
cur.execute("INSERT INTO Albums (Id) VALUES (1)") # a DML transaction: buffered
|
|
204
|
+
# Commit before querying the inserted row.
|
|
205
|
+
try:
|
|
206
|
+
cur.execute("SELECT COUNT(*) FROM Albums")
|
|
207
|
+
raise AssertionError("expected the guarded query to raise")
|
|
208
|
+
except ProgrammingError:
|
|
209
|
+
pass
|
|
210
|
+
conn.commit()
|
|
211
|
+
|
|
212
|
+
with conn.cursor() as cur:
|
|
213
|
+
cur.execute("SELECT COUNT(*) FROM Albums")
|
|
214
|
+
assert cur.fetchone()[0] == 1
|
|
215
|
+
conn.rollback() # end the query snapshot
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## Working with DataFrames
|
|
219
|
+
|
|
220
|
+
The quickstart uses pandas. The examples below use the same `Singers` table with `SingerId` and
|
|
221
|
+
`FirstName` columns. Install `polars` or `duckdb` separately for those examples.
|
|
222
|
+
|
|
223
|
+
**pyarrow — results as a native Arrow table:**
|
|
224
|
+
|
|
225
|
+
```python
|
|
226
|
+
import spanner_adbc.dbapi as spanner
|
|
227
|
+
from spanner_adbc import DatabaseOptions
|
|
228
|
+
|
|
229
|
+
with spanner.connect(
|
|
230
|
+
db_kwargs={DatabaseOptions.URI.value: "spanner:///projects/my-project/instances/my-instance/databases/my-db"},
|
|
231
|
+
) as conn:
|
|
232
|
+
with conn.cursor() as cur:
|
|
233
|
+
cur.execute("SELECT SingerId, FirstName FROM Singers ORDER BY SingerId")
|
|
234
|
+
table = cur.fetch_arrow_table() # -> pyarrow.Table
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
**polars — read straight from the connection:**
|
|
238
|
+
|
|
239
|
+
```python
|
|
240
|
+
import polars as pl
|
|
241
|
+
import spanner_adbc.dbapi as spanner
|
|
242
|
+
from spanner_adbc import DatabaseOptions
|
|
243
|
+
|
|
244
|
+
with spanner.connect(
|
|
245
|
+
db_kwargs={DatabaseOptions.URI.value: "spanner:///projects/my-project/instances/my-instance/databases/my-db"},
|
|
246
|
+
) as conn:
|
|
247
|
+
df = pl.read_database(
|
|
248
|
+
"SELECT SingerId, FirstName FROM Singers ORDER BY SingerId",
|
|
249
|
+
connection=conn, # an ADBC connection, not a URI
|
|
250
|
+
)
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
**DuckDB — query the fetched Arrow table in-process:**
|
|
254
|
+
|
|
255
|
+
```python
|
|
256
|
+
import duckdb
|
|
257
|
+
import spanner_adbc.dbapi as spanner
|
|
258
|
+
from spanner_adbc import DatabaseOptions
|
|
259
|
+
|
|
260
|
+
with spanner.connect(
|
|
261
|
+
db_kwargs={DatabaseOptions.URI.value: "spanner:///projects/my-project/instances/my-instance/databases/my-db"},
|
|
262
|
+
) as conn:
|
|
263
|
+
with conn.cursor() as cur:
|
|
264
|
+
cur.execute("SELECT SingerId, FirstName FROM Singers")
|
|
265
|
+
singers = cur.fetch_arrow_table()
|
|
266
|
+
|
|
267
|
+
# DuckDB can query the Arrow table by variable name.
|
|
268
|
+
top = duckdb.sql("SELECT COUNT(*) AS n, MIN(FirstName) AS first FROM singers").fetchone()
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
## Bulk insert a DataFrame
|
|
272
|
+
|
|
273
|
+
`cur.adbc_ingest(table, data, mode=...)` inserts Arrow-compatible data in bulk. Convert pandas
|
|
274
|
+
DataFrames to Arrow as shown here:
|
|
275
|
+
|
|
276
|
+
```python
|
|
277
|
+
import pandas as pd
|
|
278
|
+
import pyarrow as pa
|
|
279
|
+
import spanner_adbc.dbapi as spanner
|
|
280
|
+
from spanner_adbc import DatabaseOptions
|
|
281
|
+
|
|
282
|
+
frame = pd.DataFrame({"SingerId": [10, 11], "FirstName": ["Carol", "Dave"]})
|
|
283
|
+
|
|
284
|
+
with spanner.connect(
|
|
285
|
+
db_kwargs={DatabaseOptions.URI.value: "spanner:///projects/my-project/instances/my-instance/databases/my-db"},
|
|
286
|
+
autocommit=True, # apply immediately; returns the row count
|
|
287
|
+
) as conn:
|
|
288
|
+
with conn.cursor() as cur:
|
|
289
|
+
# `append` inserts into an existing table (the default mode is `create`).
|
|
290
|
+
rows = cur.adbc_ingest("Singers", pa.Table.from_pandas(frame), mode="append")
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
The `mode` selects how the target table is handled:
|
|
294
|
+
|
|
295
|
+
- `create` — create the table from the data's Arrow schema first, erroring if it already exists (the default).
|
|
296
|
+
- `append` — insert into an existing table.
|
|
297
|
+
- `create_append` — create the table only if it is absent, then insert.
|
|
298
|
+
- `replace` — drop any existing table, recreate it from the schema, then insert.
|
|
299
|
+
|
|
300
|
+
Create modes omit a primary key, so Spanner supplies a [hidden `rowid` column][no-pk].
|
|
301
|
+
For an explicit primary key, create the table with SQL first and use `mode="append"`.
|
|
302
|
+
In manual mode, inserts need `conn.commit()`; table creation or replacement still happens immediately.
|
|
303
|
+
Autocommit loads may commit in multiple chunks, so a failed load can leave rows written.
|
|
304
|
+
|
|
305
|
+
[no-pk]: https://cloud.google.com/spanner/docs/primary-key-default-value#tables-without-primary-keys
|
|
306
|
+
|
|
307
|
+
## Partitioned reads and Data Boost
|
|
308
|
+
|
|
309
|
+
A large scan can be split into independent partitions and read in parallel — optionally on Spanner's
|
|
310
|
+
serverless [Data Boost] compute, so the work is isolated from your provisioned instance. This uses
|
|
311
|
+
the ADBC partitioned-execution extension (`adbc_execute_partitions` / `adbc_read_partition`):
|
|
312
|
+
|
|
313
|
+
```python
|
|
314
|
+
import spanner_adbc.dbapi as spanner
|
|
315
|
+
from spanner_adbc import DatabaseOptions, StatementOptions
|
|
316
|
+
|
|
317
|
+
with spanner.connect(
|
|
318
|
+
db_kwargs={DatabaseOptions.URI.value: "spanner:///projects/my-project/instances/my-instance/databases/my-db"},
|
|
319
|
+
) as conn:
|
|
320
|
+
with conn.cursor() as cur:
|
|
321
|
+
# Optional statement options, set on the underlying ADBC statement:
|
|
322
|
+
cur.adbc_statement.set_options(**{
|
|
323
|
+
StatementOptions.DATA_BOOST.value: "true", # run on Data Boost
|
|
324
|
+
})
|
|
325
|
+
partitions, schema = cur.adbc_execute_partitions("SELECT SingerId FROM Singers")
|
|
326
|
+
|
|
327
|
+
# Each descriptor is opaque bytes; it can be shipped to another worker,
|
|
328
|
+
# process, or connection and read independently.
|
|
329
|
+
for token in partitions:
|
|
330
|
+
with conn.cursor() as cur:
|
|
331
|
+
cur.adbc_read_partition(token)
|
|
332
|
+
table = cur.fetch_arrow_table()
|
|
333
|
+
...
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
Spanner decides partitionability from the query plan. Simple scans are suitable; joins, ordering,
|
|
337
|
+
and aggregation can prevent partitioning. See the [partitionability rules][parallel-reads].
|
|
338
|
+
|
|
339
|
+
Descriptors contain SQL and transaction identifiers and are unauthenticated. Only read descriptors
|
|
340
|
+
from trusted sources: they execute using the receiving connection's credentials.
|
|
341
|
+
|
|
342
|
+
[Data Boost]: https://cloud.google.com/spanner/docs/databoost/databoost-overview
|
|
343
|
+
[parallel-reads]: https://cloud.google.com/spanner/docs/reads#read_data_in_parallel
|
|
344
|
+
|
|
345
|
+
## Supported platforms
|
|
346
|
+
|
|
347
|
+
`pip` selects a wheel matching the OS and architecture:
|
|
348
|
+
|
|
349
|
+
| Platform | Wheel tag | Minimum requirement |
|
|
350
|
+
| -------------- | ------------------------ | -------------------------------------------- |
|
|
351
|
+
| Linux x86-64 | `manylinux_2_35_x86_64` | glibc >= 2.35 |
|
|
352
|
+
| Linux aarch64 | `manylinux_2_35_aarch64` | glibc >= 2.35 |
|
|
353
|
+
| Linux x86-64 musl | `musllinux_1_2_x86_64` | musl libc >= 1.2 |
|
|
354
|
+
| Linux aarch64 musl | `musllinux_1_2_aarch64` | musl libc >= 1.2 |
|
|
355
|
+
| macOS arm64 | `macosx_11_0_arm64` | macOS >= 11.0 |
|
|
356
|
+
| macOS x86-64 | `macosx_10_15_x86_64` | macOS >= 10.15 |
|
|
357
|
+
| Windows x86-64 | `win_amd64` | 64-bit Windows |
|
|
358
|
+
| Windows arm64 | `win_arm64` | ARM64 Windows |
|
|
359
|
+
|
|
360
|
+
Building for an older OS requires a compatible native driver and Python dependencies.
|
|
361
|
+
For local builds and releases, see [CONTRIBUTING.md](https://github.com/fornwall/spanner-adbc/blob/main/CONTRIBUTING.md).
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
spanner_adbc/__init__.py,sha256=JaM_rJCT-CHATYa7-uJ8YwPUbbiYGbGRw-smVB_B6nw,3131
|
|
2
|
+
spanner_adbc/_options.py,sha256=E7CmaEoTXI2aTdeqcezKCm8NpabOdIk-w9mIHztr6-A,10169
|
|
3
|
+
spanner_adbc/_version.py,sha256=iPlYCcIzuzW7T2HKDkmYlMkRI51dBLfNRxPPiWrfw9U,22
|
|
4
|
+
spanner_adbc/dbapi.py,sha256=386EflvG4UZuOhAuPXurbaR4IS7YLZJ4PKx3JjAQm48,2676
|
|
5
|
+
spanner_adbc/manifest.py,sha256=gzkRLvApR8Qr_pSOt3eJ28exp8UuwH38EbhiNgjNFF8,10214
|
|
6
|
+
spanner_adbc/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
spanner_adbc/spanner_adbc.dll,sha256=WNceT_RFScRiNq3zIVl1XVskAVQpuVhevnUrJTh_ib8,19527168
|
|
8
|
+
spanner_adbc-0.8.0.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
|
|
9
|
+
spanner_adbc-0.8.0.dist-info/METADATA,sha256=swV8k52jxUjSLmpSrShJHl4vE7DX4yTMTJQ2dwMEA-8,15354
|
|
10
|
+
spanner_adbc-0.8.0.dist-info/WHEEL,sha256=Zx98gwb_dQKckJK3HEOVKnxxD52EfU_PXDG_usMJ2ng,98
|
|
11
|
+
spanner_adbc-0.8.0.dist-info/entry_points.txt,sha256=S9FfsSQaBxlssjyACrt16kcxWdYq3ptxDVXCuYkCF58,77
|
|
12
|
+
spanner_adbc-0.8.0.dist-info/top_level.txt,sha256=sBmJlDBf448Pz4G5lANkoLq6qyRvZ8tawyxyOqDmlFk,13
|
|
13
|
+
spanner_adbc-0.8.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
|
|
2
|
+
Apache License
|
|
3
|
+
Version 2.0, January 2004
|
|
4
|
+
http://www.apache.org/licenses/
|
|
5
|
+
|
|
6
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
7
|
+
|
|
8
|
+
1. Definitions.
|
|
9
|
+
|
|
10
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
11
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
12
|
+
|
|
13
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
14
|
+
the copyright owner that is granting the License.
|
|
15
|
+
|
|
16
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
17
|
+
other entities that control, are controlled by, or are under common
|
|
18
|
+
control with that entity. For the purposes of this definition,
|
|
19
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
20
|
+
direction or management of such entity, whether by contract or
|
|
21
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
22
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
23
|
+
|
|
24
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
25
|
+
exercising permissions granted by this License.
|
|
26
|
+
|
|
27
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
28
|
+
including but not limited to software source code, documentation
|
|
29
|
+
source, and configuration files.
|
|
30
|
+
|
|
31
|
+
"Object" form shall mean any form resulting from mechanical
|
|
32
|
+
transformation or translation of a Source form, including but
|
|
33
|
+
not limited to compiled object code, generated documentation,
|
|
34
|
+
and conversions to other media types.
|
|
35
|
+
|
|
36
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
37
|
+
Object form, made available under the License, as indicated by a
|
|
38
|
+
copyright notice that is included in or attached to the work
|
|
39
|
+
(an example is provided in the Appendix below).
|
|
40
|
+
|
|
41
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
42
|
+
form, that is based on (or derived from) the Work and for which the
|
|
43
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
44
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
45
|
+
of this License, Derivative Works shall not include works that remain
|
|
46
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
47
|
+
the Work and Derivative Works thereof.
|
|
48
|
+
|
|
49
|
+
"Contribution" shall mean any work of authorship, including
|
|
50
|
+
the original version of the Work and any modifications or additions
|
|
51
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
52
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
53
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
54
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
55
|
+
means any form of electronic, verbal, or written communication sent
|
|
56
|
+
to the Licensor or its representatives, including but not limited to
|
|
57
|
+
communication on electronic mailing lists, source code control systems,
|
|
58
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
59
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
60
|
+
excluding communication that is conspicuously marked or otherwise
|
|
61
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
62
|
+
|
|
63
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
64
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
65
|
+
subsequently incorporated within the Work.
|
|
66
|
+
|
|
67
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
68
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
69
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
70
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
71
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
72
|
+
Work and such Derivative Works in Source or Object form.
|
|
73
|
+
|
|
74
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
75
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
76
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
77
|
+
(except as stated in this section) patent license to make, have made,
|
|
78
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
79
|
+
where such license applies only to those patent claims licensable
|
|
80
|
+
by such Contributor that are necessarily infringed by their
|
|
81
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
82
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
83
|
+
institute patent litigation against any entity (including a
|
|
84
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
85
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
86
|
+
or contributory patent infringement, then any patent licenses
|
|
87
|
+
granted to You under this License for that Work shall terminate
|
|
88
|
+
as of the date such litigation is filed.
|
|
89
|
+
|
|
90
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
91
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
92
|
+
modifications, and in Source or Object form, provided that You
|
|
93
|
+
meet the following conditions:
|
|
94
|
+
|
|
95
|
+
(a) You must give any other recipients of the Work or
|
|
96
|
+
Derivative Works a copy of this License; and
|
|
97
|
+
|
|
98
|
+
(b) You must cause any modified files to carry prominent notices
|
|
99
|
+
stating that You changed the files; and
|
|
100
|
+
|
|
101
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
102
|
+
that You distribute, all copyright, patent, trademark, and
|
|
103
|
+
attribution notices from the Source form of the Work,
|
|
104
|
+
excluding those notices that do not pertain to any part of
|
|
105
|
+
the Derivative Works; and
|
|
106
|
+
|
|
107
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
108
|
+
distribution, then any Derivative Works that You distribute must
|
|
109
|
+
include a readable copy of the attribution notices contained
|
|
110
|
+
within such NOTICE file, excluding those notices that do not
|
|
111
|
+
pertain to any part of the Derivative Works, in at least one
|
|
112
|
+
of the following places: within a NOTICE text file distributed
|
|
113
|
+
as part of the Derivative Works; within the Source form or
|
|
114
|
+
documentation, if provided along with the Derivative Works; or,
|
|
115
|
+
within a display generated by the Derivative Works, if and
|
|
116
|
+
wherever such third-party notices normally appear. The contents
|
|
117
|
+
of the NOTICE file are for informational purposes only and
|
|
118
|
+
do not modify the License. You may add Your own attribution
|
|
119
|
+
notices within Derivative Works that You distribute, alongside
|
|
120
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
121
|
+
that such additional attribution notices cannot be construed
|
|
122
|
+
as modifying the License.
|
|
123
|
+
|
|
124
|
+
You may add Your own copyright statement to Your modifications and
|
|
125
|
+
may provide additional or different license terms and conditions
|
|
126
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
127
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
128
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
129
|
+
the conditions stated in this License.
|
|
130
|
+
|
|
131
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
132
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
133
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
134
|
+
this License, without any additional terms or conditions.
|
|
135
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
136
|
+
the terms of any separate license agreement you may have executed
|
|
137
|
+
with Licensor regarding such Contributions.
|
|
138
|
+
|
|
139
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
140
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
141
|
+
except as required for reasonable and customary use in describing the
|
|
142
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
143
|
+
|
|
144
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
145
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
146
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
147
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
148
|
+
implied, including, without limitation, any warranties or conditions
|
|
149
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
150
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
151
|
+
appropriateness of using or redistributing the Work and assume any
|
|
152
|
+
risks associated with Your exercise of permissions under this License.
|
|
153
|
+
|
|
154
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
155
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
156
|
+
unless required by applicable law (such as deliberate and grossly
|
|
157
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
158
|
+
liable to You for damages, including any direct, indirect, special,
|
|
159
|
+
incidental, or consequential damages of any character arising as a
|
|
160
|
+
result of this License or out of the use or inability to use the
|
|
161
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
162
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
163
|
+
other commercial damages or losses), even if such Contributor
|
|
164
|
+
has been advised of the possibility of such damages.
|
|
165
|
+
|
|
166
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
167
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
168
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
169
|
+
or other liability obligations and/or rights consistent with this
|
|
170
|
+
License. However, in accepting such obligations, You may act only
|
|
171
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
172
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
173
|
+
defend, and hold each Contributor harmless for any liability
|
|
174
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
175
|
+
of your accepting any such warranty or additional liability.
|
|
176
|
+
|
|
177
|
+
END OF TERMS AND CONDITIONS
|
|
178
|
+
|
|
179
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
180
|
+
|
|
181
|
+
To apply the Apache License to your work, attach the following
|
|
182
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
183
|
+
replaced with your own identifying information. (Don't include
|
|
184
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
185
|
+
comment syntax for the file format. We also recommend that a
|
|
186
|
+
file or class name and description of purpose be included on the
|
|
187
|
+
same "printed page" as the copyright notice for easier
|
|
188
|
+
identification within third-party archives.
|
|
189
|
+
|
|
190
|
+
Copyright [yyyy] [name of copyright owner]
|
|
191
|
+
|
|
192
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
193
|
+
you may not use this file except in compliance with the License.
|
|
194
|
+
You may obtain a copy of the License at
|
|
195
|
+
|
|
196
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
197
|
+
|
|
198
|
+
Unless required by applicable law or agreed to in writing, software
|
|
199
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
200
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
201
|
+
See the License for the specific language governing permissions and
|
|
202
|
+
limitations under the License.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
spanner_adbc
|