Skip to content

Commit 352c47d

Browse files
author
Ilya Gurov
authored
feat: support read_only connections (#125)
1 parent c433cda commit 352c47d

File tree

3 files changed

+53
-1
lines changed

3 files changed

+53
-1
lines changed

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,16 @@ eng = create_engine("spanner:///projects/project-id/instances/instance-id/databa
158158
autocommit_engine = eng.execution_options(isolation_level="AUTOCOMMIT")
159159
```
160160

161+
**ReadOnly transactions**
162+
By default, transactions produced by a Spanner connection are in ReadWrite mode. However, some applications require an ability to grant ReadOnly access to users/methods; for these cases Spanner dialect supports the `read_only` execution option, which switches a connection into ReadOnly mode:
163+
```python
164+
with engine.connect().execution_options(read_only=True) as connection:
165+
connection.execute(select(["*"], from_obj=table)).fetchall()
166+
```
167+
Note that execution options are applied lazily - on the `execute()` method call, right before it.
168+
169+
ReadOnly/ReadWrite mode of a connection can't be changed while a transaction is in progress - first you must commit or rollback it.
170+
161171
**DDL and transactions**
162172
DDL statements are executed outside the regular transactions mechanism, which means DDL statements will not be rolled back on normal transaction rollback.
163173

google/cloud/sqlalchemy_spanner/sqlalchemy_spanner.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
)
2525
from sqlalchemy import ForeignKeyConstraint, types, util
2626
from sqlalchemy.engine.base import Engine
27-
from sqlalchemy.engine.default import DefaultDialect
27+
from sqlalchemy.engine.default import DefaultDialect, DefaultExecutionContext
2828
from sqlalchemy.ext.compiler import compiles
2929
from sqlalchemy.sql.compiler import (
3030
selectable,
@@ -116,6 +116,19 @@ def wrapper(self, connection, *args, **kwargs):
116116
return wrapper
117117

118118

119+
class SpannerExecutionContext(DefaultExecutionContext):
120+
def pre_exec(self):
121+
"""
122+
Apply execution options to the DB API connection before
123+
executing the next SQL operation.
124+
"""
125+
super(SpannerExecutionContext, self).pre_exec()
126+
127+
read_only = self.execution_options.get("read_only", None)
128+
if read_only is not None:
129+
self._dbapi_connection.connection.read_only = read_only
130+
131+
119132
class SpannerIdentifierPreparer(IdentifierPreparer):
120133
"""Identifiers compiler.
121134
@@ -393,6 +406,7 @@ class SpannerDialect(DefaultDialect):
393406
preparer = SpannerIdentifierPreparer
394407
statement_compiler = SpannerSQLCompiler
395408
type_compiler = SpannerTypeCompiler
409+
execution_ctx_cls = SpannerExecutionContext
396410

397411
@classmethod
398412
def dbapi(cls):

test/test_suite.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1574,3 +1574,31 @@ def test_user_agent(self):
15741574
connection.connection.instance._client._client_info.user_agent
15751575
== dist.project_name + "/" + dist.version
15761576
)
1577+
1578+
1579+
class ExecutionOptionsTest(fixtures.TestBase):
1580+
"""
1581+
Check that `execution_options()` method correctly
1582+
sets parameters on the underlying DB API connection.
1583+
"""
1584+
1585+
def setUp(self):
1586+
self._engine = create_engine(
1587+
"spanner:///projects/appdev-soda-spanner-staging/instances/"
1588+
"sqlalchemy-dialect-test/databases/compliance-test"
1589+
)
1590+
self._metadata = MetaData(bind=self._engine)
1591+
1592+
self._table = Table(
1593+
"execution_options",
1594+
self._metadata,
1595+
Column("opt_id", Integer, primary_key=True),
1596+
Column("opt_name", String(16), nullable=False),
1597+
)
1598+
1599+
self._metadata.create_all(self._engine)
1600+
1601+
def test_read_only(self):
1602+
with self._engine.connect().execution_options(read_only=True) as connection:
1603+
connection.execute(select(["*"], from_obj=self._table)).fetchall()
1604+
assert connection.connection.read_only is True

0 commit comments

Comments
 (0)