Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support read_only connections #125

Merged
merged 4 commits into from
Oct 11, 2021
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,16 @@ eng = create_engine("spanner:///projects/project-id/instances/instance-id/databa
autocommit_engine = eng.execution_options(isolation_level="AUTOCOMMIT")
```

**ReadOnly transactions**
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:
```python
with engine.connect().execution_options(read_only=True) as connection:
connection.execute(select(["*"], from_obj=table)).fetchall()
```
Note that execution options are applied lazily - on the `execute()` method call, right before it.

ReadOnly/ReadWrite mode of a connection can't be changed while a transaction is in progress - first you must commit or rollback it.

**DDL and transactions**
DDL statements are executed outside the regular transactions mechanism, which means DDL statements will not be rolled back on normal transaction rollback.

Expand Down
16 changes: 15 additions & 1 deletion google/cloud/sqlalchemy_spanner/sqlalchemy_spanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

from sqlalchemy import types, ForeignKeyConstraint
from sqlalchemy.engine.base import Engine
from sqlalchemy.engine.default import DefaultDialect
from sqlalchemy.engine.default import DefaultDialect, DefaultExecutionContext
from sqlalchemy import util
from sqlalchemy.sql.compiler import (
selectable,
Expand Down Expand Up @@ -108,6 +108,19 @@ def wrapper(self, connection, *args, **kwargs):
return wrapper


class SpannerExecutionContext(DefaultExecutionContext):
def pre_exec(self):
"""
Apply execution options to the DB API connection before
executing the next SQL operation.
"""
super(SpannerExecutionContext, self).pre_exec()

read_only = self.execution_options.get("read_only", None)
if read_only is not None:
self._dbapi_connection.connection.read_only = read_only


class SpannerIdentifierPreparer(IdentifierPreparer):
"""Identifiers compiler.

Expand Down Expand Up @@ -385,6 +398,7 @@ class SpannerDialect(DefaultDialect):
preparer = SpannerIdentifierPreparer
statement_compiler = SpannerSQLCompiler
type_compiler = SpannerTypeCompiler
execution_ctx_cls = SpannerExecutionContext

@classmethod
def dbapi(cls):
Expand Down
28 changes: 28 additions & 0 deletions test/test_suite.py
Original file line number Diff line number Diff line change
Expand Up @@ -1574,3 +1574,31 @@ def test_user_agent(self):
connection.connection.instance._client._client_info.user_agent
== dist.project_name + "/" + dist.version
)


class ExecutionOptionsTest(fixtures.TestBase):
"""
Check that `execution_options()` method correctly
sets parameters on the underlying DB API connection.
"""

def setUp(self):
self._engine = create_engine(
"spanner:///projects/appdev-soda-spanner-staging/instances/"
"sqlalchemy-dialect-test/databases/compliance-test"
)
self._metadata = MetaData(bind=self._engine)

self._table = Table(
"execution_options",
self._metadata,
Column("opt_id", Integer, primary_key=True),
Column("opt_name", String(16), nullable=False),
)

self._metadata.create_all(self._engine)

def test_read_only(self):
with self._engine.connect().execution_options(read_only=True) as connection:
connection.execute(select(["*"], from_obj=self._table)).fetchall()
assert connection.connection.read_only is True