diff --git a/edb/buildmeta.py b/edb/buildmeta.py index ff4bbf45d6f..996b1b0f421 100644 --- a/edb/buildmeta.py +++ b/edb/buildmeta.py @@ -60,7 +60,7 @@ # The merge conflict there is a nice reminder that you probably need # to write a patch in edb/pgsql/patches.py, and then you should preserve # the old value. -EDGEDB_CATALOG_VERSION = 2024_02_03_00_00 +EDGEDB_CATALOG_VERSION = 2025_02_04_00_00 EDGEDB_MAJOR_VERSION = 7 diff --git a/edb/ir/statypes.py b/edb/ir/statypes.py index 8e86853f3b4..09f122161e9 100644 --- a/edb/ir/statypes.py +++ b/edb/ir/statypes.py @@ -27,6 +27,7 @@ Optional, Self, TypeVar, + TYPE_CHECKING, ) import dataclasses @@ -47,6 +48,9 @@ from edb.schema import name as s_name from edb.schema import objects as s_obj +if TYPE_CHECKING: + from edb.edgeql import qltypes + MISSING: Any = object() @@ -736,3 +740,74 @@ def get_translation_map(cls) -> Mapping[EnabledDisabledEnum, str]: EnabledDisabledEnum.Enabled: "true", EnabledDisabledEnum.Disabled: "false", } + + +class TransactionAccessModeEnum(enum.StrEnum): + ReadOnly = "ReadOnly" + ReadWrite = "ReadWrite" + + +class TransactionAccessMode( + EnumScalarType[TransactionAccessModeEnum], + edgeql_type="sys::TransactionAccessMode", +): + @classmethod + def get_translation_map(cls) -> Mapping[TransactionAccessModeEnum, str]: + return { + TransactionAccessModeEnum.ReadOnly: "true", + TransactionAccessModeEnum.ReadWrite: "false", + } + + def to_qltypes(self) -> qltypes.TransactionAccessMode: + from edb.edgeql import qltypes + match self._val: + case TransactionAccessModeEnum.ReadOnly: + return qltypes.TransactionAccessMode.READ_ONLY + case TransactionAccessModeEnum.ReadWrite: + return qltypes.TransactionAccessMode.READ_WRITE + case _: + raise AssertionError(f"unexpected value: {self._val!r}") + + +class TransactionDeferrabilityEnum(enum.StrEnum): + Deferrable = "Deferrable" + NotDeferrable = "NotDeferrable" + + +class TransactionDeferrability( + EnumScalarType[TransactionDeferrabilityEnum], + edgeql_type="sys::TransactionDeferrability", +): + @classmethod + def get_translation_map(cls) -> Mapping[TransactionDeferrabilityEnum, str]: + return { + TransactionDeferrabilityEnum.Deferrable: "true", + TransactionDeferrabilityEnum.NotDeferrable: "false", + } + + +class TransactionIsolationEnum(enum.StrEnum): + Serializable = "Serializable" + RepeatableRead = "RepeatableRead" + + +class TransactionIsolation( + EnumScalarType[TransactionIsolationEnum], + edgeql_type="sys::TransactionIsolation", +): + @classmethod + def get_translation_map(cls) -> Mapping[TransactionIsolationEnum, str]: + return { + TransactionIsolationEnum.Serializable: "serializable", + TransactionIsolationEnum.RepeatableRead: "repeatable read", + } + + def to_qltypes(self) -> qltypes.TransactionIsolationLevel: + from edb.edgeql import qltypes + match self._val: + case TransactionIsolationEnum.Serializable: + return qltypes.TransactionIsolationLevel.SERIALIZABLE + case TransactionIsolationEnum.RepeatableRead: + return qltypes.TransactionIsolationLevel.REPEATABLE_READ + case _: + raise AssertionError(f"unexpected value: {self._val!r}") diff --git a/edb/lib/cfg.edgeql b/edb/lib/cfg.edgeql index 9b2d672c626..bf8cb298c1f 100644 --- a/edb/lib/cfg.edgeql +++ b/edb/lib/cfg.edgeql @@ -188,6 +188,47 @@ ALTER TYPE cfg::AbstractConfig { SET default := '60 seconds'; }; + CREATE REQUIRED PROPERTY default_transaction_isolation + -> sys::TransactionIsolation + { + CREATE ANNOTATION cfg::affects_compilation := 'true'; + CREATE ANNOTATION cfg::backend_setting := + '"default_transaction_isolation"'; + CREATE ANNOTATION std::description := + 'Controls the default isolation level of each new transaction, \ + including implicit transactions. Defaults to `Serializable`. \ + Note that changing this to a lower isolation level implies \ + that the transactions are also read-only by default regardless \ + of the value of the `default_transaction_access_mode` setting.'; + SET default := sys::TransactionIsolation.Serializable; + }; + + CREATE REQUIRED PROPERTY default_transaction_access_mode + -> sys::TransactionAccessMode + { + CREATE ANNOTATION cfg::affects_compilation := 'true'; + CREATE ANNOTATION std::description := + 'Controls the default read-only status of each new transaction, \ + including implicit transactions. Defaults to `ReadWrite`. \ + Note that if `default_transaction_isolation` is set to any value \ + other than Serializable this parameter is implied to be \ + `ReadOnly` regardless of the actual value.'; + SET default := sys::TransactionAccessMode.ReadWrite; + }; + + CREATE REQUIRED PROPERTY default_transaction_deferrable + -> sys::TransactionDeferrability + { + CREATE ANNOTATION cfg::backend_setting := + '"default_transaction_deferrable"'; + CREATE ANNOTATION std::description := + 'Controls the default deferrable status of each new transaction. \ + It currently has no effect on read-write transactions or those \ + operating at isolation levels lower than `Serializable`. \ + The default is `NotDeferrable`.'; + SET default := sys::TransactionDeferrability.NotDeferrable; + }; + CREATE REQUIRED PROPERTY session_idle_transaction_timeout -> std::duration { CREATE ANNOTATION cfg::backend_setting := '"idle_in_transaction_session_timeout"'; diff --git a/edb/lib/sys.edgeql b/edb/lib/sys.edgeql index 67ebe6c846f..57385e822c6 100644 --- a/edb/lib/sys.edgeql +++ b/edb/lib/sys.edgeql @@ -24,6 +24,14 @@ CREATE SCALAR TYPE sys::TransactionIsolation EXTENDING enum; +CREATE SCALAR TYPE sys::TransactionAccessMode + EXTENDING enum; + + +CREATE SCALAR TYPE sys::TransactionDeferrability + EXTENDING enum; + + CREATE SCALAR TYPE sys::VersionStage EXTENDING enum; diff --git a/edb/pgsql/compiler/clauses.py b/edb/pgsql/compiler/clauses.py index 31e531edad4..7d1622d34f4 100644 --- a/edb/pgsql/compiler/clauses.py +++ b/edb/pgsql/compiler/clauses.py @@ -489,7 +489,7 @@ def scan_check_ctes( name='flag', val=pgast.BooleanConstant(val=True) )], relation=pgast.RelRangeVar(relation=pgast.Relation( - schemaname='edgedb', name='_dml_dummy')), + name='_dml_dummy')), where_clause=pgast.Expr( name="=", lexpr=pgast.ColumnRef(name=["id"]), diff --git a/edb/pgsql/metaschema.py b/edb/pgsql/metaschema.py index c814655a4ab..ee1f0ba647e 100644 --- a/edb/pgsql/metaschema.py +++ b/edb/pgsql/metaschema.py @@ -186,32 +186,6 @@ def __init__(self) -> None: ) -class DMLDummyTable(dbops.Table): - """A empty dummy table used when we need to emit no-op DML. - - This is used by scan_check_ctes in the pgsql compiler to - force the evaluation of error checking. - """ - def __init__(self) -> None: - super().__init__(name=('edgedb', '_dml_dummy')) - - self.add_columns([ - dbops.Column(name='id', type='int8'), - dbops.Column(name='flag', type='bool'), - ]) - - self.add_constraint( - dbops.UniqueConstraint( - table_name=('edgedb', '_dml_dummy'), - columns=['id'], - ), - ) - - SETUP_QUERY = ''' - INSERT INTO edgedb._dml_dummy VALUES (0, false) - ''' - - class QueryCacheTable(dbops.Table): def __init__(self) -> None: super().__init__(name=('edgedb', '_query_cache')) @@ -5156,12 +5130,8 @@ def get_fixed_bootstrap_commands() -> dbops.CommandGroup: DBConfigTable(), ), # TODO: SHOULD THIS BE VERSIONED? - dbops.CreateTable(DMLDummyTable()), - # TODO: SHOULD THIS BE VERSIONED? dbops.CreateTable(QueryCacheTable()), - dbops.Query(DMLDummyTable.SETUP_QUERY), - dbops.CreateDomain(BigintDomain()), dbops.CreateDomain(ConfigMemoryDomain()), dbops.CreateDomain(TimestampTzDomain()), diff --git a/edb/server/bootstrap.py b/edb/server/bootstrap.py index 6d25f557b45..d580ac36239 100644 --- a/edb/server/bootstrap.py +++ b/edb/server/bootstrap.py @@ -2591,9 +2591,11 @@ async def _bootstrap( await tpl_ctx.conn.sql_execute(b"SELECT pg_advisory_lock(3987734529)") try: - # Some of the views need access to the _edgecon_state table, - # so set it up. - tmp_table_query = pgcon.SETUP_TEMP_TABLE_SCRIPT + # Some of the views need access to the _edgecon_state table and the + # _dml_dummy table, so set it up. + tmp_table_query = ( + pgcon.SETUP_TEMP_TABLE_SCRIPT + pgcon.SETUP_DML_DUMMY_TABLE_SCRIPT + ) await _execute(tpl_ctx.conn, tmp_table_query) stdlib, config_spec, compiler = await _init_stdlib( diff --git a/edb/server/compiler/compiler.py b/edb/server/compiler/compiler.py index 922d7ecb445..6f2eb95ceb4 100644 --- a/edb/server/compiler/compiler.py +++ b/edb/server/compiler/compiler.py @@ -66,6 +66,7 @@ from edb.edgeql import qltypes from edb.ir import staeval as ireval +from edb.ir import statypes from edb.ir import ast as irast from edb.schema import ddl as s_ddl @@ -2152,22 +2153,39 @@ def _compile_ql_transaction( ctx.state.start_tx() - sqls = 'START TRANSACTION' + # Compute the effective isolation level + iso_config: statypes.TransactionIsolation = _get_config_val( + ctx, "default_transaction_isolation" + ) + default_iso = iso_config.to_qltypes() iso = ql.isolation - if iso is not None: - if ( - iso is not qltypes.TransactionIsolationLevel.SERIALIZABLE - and ql.access is not qltypes.TransactionAccessMode.READ_ONLY - ): - raise errors.TransactionError( - f"{iso.value} transaction isolation level is only " - "supported in read-only transactions", - span=ql.span, - hint=f"specify READ ONLY access mode", + if iso is None: + iso = default_iso + + # Compute the effective access mode + access = ql.access + if access is None: + if default_iso is qltypes.TransactionIsolationLevel.SERIALIZABLE: + access_mode: statypes.TransactionAccessMode = _get_config_val( + ctx, "default_transaction_access_mode" ) - sqls += f' ISOLATION LEVEL {iso.value}' - if ql.access is not None: - sqls += f' {ql.access.value}' + access = access_mode.to_qltypes() + else: + access = qltypes.TransactionAccessMode.READ_ONLY + + # Guard against unsupported isolation + access combinations + if ( + iso is not qltypes.TransactionIsolationLevel.SERIALIZABLE + and access is not qltypes.TransactionAccessMode.READ_ONLY + ): + raise errors.TransactionError( + f"{iso.value} transaction isolation level is only " + "supported in read-only transactions", + span=ql.span, + hint=f"specify READ ONLY access mode", + ) + + sqls = f'START TRANSACTION ISOLATION LEVEL {iso.value} {access.value}' if ql.deferrable is not None: sqls += f' {ql.deferrable.value}' sqls += ';' @@ -2344,7 +2362,7 @@ def _inject_config_cache_clear(sql_ast: pgast.Base) -> pgast.Base: name='flag', val=pgast.BooleanConstant(val=True) )], relation=pgast.RelRangeVar(relation=pgast.Relation( - schemaname='edgedb', name='_dml_dummy')), + name='_dml_dummy')), where_clause=pgast.Expr( name="=", lexpr=pgast.ColumnRef(name=["id"]), diff --git a/edb/server/compiler/explain/to_json.py b/edb/server/compiler/explain/to_json.py index fe4a8da5b3b..16bd7ad7ddb 100644 --- a/edb/server/compiler/explain/to_json.py +++ b/edb/server/compiler/explain/to_json.py @@ -21,6 +21,8 @@ import enum import uuid +from edb.ir import statypes + class ToJson: def to_json(self) -> Any: @@ -36,4 +38,6 @@ def json_hook(value: Any) -> Any: return value.value elif isinstance(value, (frozenset, set)): return list(value) + elif isinstance(value, statypes.ScalarType): + return value.to_json() raise TypeError(f"Cannot serialize {value!r}") diff --git a/edb/server/dbview/dbview.pxd b/edb/server/dbview/dbview.pxd index 133c661877c..2183482c71f 100644 --- a/edb/server/dbview/dbview.pxd +++ b/edb/server/dbview/dbview.pxd @@ -237,6 +237,7 @@ cdef class DatabaseConnectionView: cdef get_system_config(self) cpdef get_compilation_system_config(self) + cdef config_lookup(self, name) cdef set_modaliases(self, new_aliases) cpdef get_modaliases(self) diff --git a/edb/server/dbview/dbview.pyx b/edb/server/dbview/dbview.pyx index 99da15a7191..2c626dc5e85 100644 --- a/edb/server/dbview/dbview.pyx +++ b/edb/server/dbview/dbview.pyx @@ -1182,17 +1182,22 @@ cdef class DatabaseConnectionView: self._reset_tx_state() return side_effects + cdef config_lookup(self, name): + return self.server.config_lookup( + name, + self.get_session_config(), + self.get_database_config(), + self.get_system_config(), + ) + async def recompile_cached_queries(self, user_schema, schema_version): compiler_pool = self.server.get_compiler_pool() compile_concurrency = max(1, compiler_pool.get_size_hint() // 2) concurrency_control = asyncio.Semaphore(compile_concurrency) rv = [] - recompile_timeout = self.server.config_lookup( + recompile_timeout = self.config_lookup( "auto_rebuild_query_cache_timeout", - self.get_session_config(), - self.get_database_config(), - self.get_system_config(), ) loop = asyncio.get_running_loop() @@ -1418,9 +1423,6 @@ cdef class DatabaseConnectionView: user_schema_version = unit.user_schema_version if user_schema and not self.server.config_lookup( "auto_rebuild_query_cache", - self.get_session_config(), - self.get_database_config(), - self.get_system_config(), ): user_schema = None if user_schema: @@ -1686,6 +1688,23 @@ cdef class DatabaseConnectionView: msg, ) + if not self.in_tx() and query_capabilities & enums.Capability.WRITE: + isolation = self.config_lookup("default_transaction_isolation") + if isolation and isolation.to_str() != "Serializable": + raise query_capabilities.make_error( + ~enums.Capability.WRITE, + errors.TransactionError, + f"default_transaction_isolation is set to " + f"{isolation.to_str()}", + ) + access_mode = self.config_lookup("default_transaction_access_mode") + if access_mode and access_mode.to_str() == "ReadOnly": + raise query_capabilities.make_error( + ~enums.Capability.WRITE, + errors.TransactionError, + "default_transaction_access_mode is set to ReadOnly", + ) + async def reload_state_serializer(self): # This should only happen once when a different protocol version is # used after schema change, or non-current version of protocol is used diff --git a/edb/server/pgcon/__init__.py b/edb/server/pgcon/__init__.py index 8babc785151..7fde1af6075 100644 --- a/edb/server/pgcon/__init__.py +++ b/edb/server/pgcon/__init__.py @@ -33,6 +33,7 @@ pg_connect, SETUP_TEMP_TABLE_SCRIPT, SETUP_CONFIG_CACHE_SCRIPT, + SETUP_DML_DUMMY_TABLE_SCRIPT, RESET_STATIC_CFG_SCRIPT, ) @@ -45,5 +46,6 @@ 'BackendCatalogNameError', 'SETUP_TEMP_TABLE_SCRIPT', 'SETUP_CONFIG_CACHE_SCRIPT', + 'SETUP_DML_DUMMY_TABLE_SCRIPT', 'RESET_STATIC_CFG_SCRIPT' ) diff --git a/edb/server/pgcon/connect.py b/edb/server/pgcon/connect.py index aafe1532f3f..24d890ca8a1 100644 --- a/edb/server/pgcon/connect.py +++ b/edb/server/pgcon/connect.py @@ -61,6 +61,20 @@ value edgedb._sys_config_val_t NOT NULL ); '''.strip() + +# A empty dummy table used when we need to emit no-op DML. +# +# This is used by scan_check_ctes in the pgsql compiler to +# force the evaluation of error checking. +SETUP_DML_DUMMY_TABLE_SCRIPT = ''' + CREATE TEMPORARY TABLE _dml_dummy ( + id int8, + flag bool, + unique(id) + ); + INSERT INTO _dml_dummy VALUES (0, false); +'''.strip() + RESET_STATIC_CFG_SCRIPT: bytes = b''' WITH x1 AS ( DELETE FROM _config_cache @@ -88,6 +102,7 @@ def _build_init_con_script(*, check_pg_is_in_recovery: bool) -> bytes: {SETUP_TEMP_TABLE_SCRIPT} {SETUP_CONFIG_CACHE_SCRIPT} + {SETUP_DML_DUMMY_TABLE_SCRIPT} PREPARE _clear_state AS WITH x1 AS ( diff --git a/edb/server/pgcon/pgcon.pyi b/edb/server/pgcon/pgcon.pyi index 8e961dfdc62..536eb4c5ec8 100644 --- a/edb/server/pgcon/pgcon.pyi +++ b/edb/server/pgcon/pgcon.pyi @@ -96,3 +96,4 @@ class PGConnection(asyncio.Protocol): SETUP_TEMP_TABLE_SCRIPT: str SETUP_CONFIG_CACHE_SCRIPT: str +SETUP_DML_DUMMY_TABLE_SCRIPT: str diff --git a/tests/test_server_proto.py b/tests/test_server_proto.py index 9d5f0f4a53a..38dc61580b0 100644 --- a/tests/test_server_proto.py +++ b/tests/test_server_proto.py @@ -2069,6 +2069,304 @@ async def test_server_proto_tx_22(self): self.assertEqual(await self.con.query_single('SELECT 42'), 42) + async def test_server_proto_tx_23(self): + # Test that default_transaction_isolation is respected + + await self.con.query(''' + CONFIGURE SESSION + SET default_transaction_isolation := 'RepeatableRead'; + ''') + + try: + self.assertEqual( + await self.con.query( + 'select sys::get_transaction_isolation();', + ), + ["RepeatableRead"], + ) + finally: + await self.con.query(''' + CONFIGURE SESSION + RESET default_transaction_isolation; + ''') + + async def test_server_proto_tx_24(self): + # default_transaction_isolation < Serializable enforces read-only + + await self.con.query(''' + CONFIGURE SESSION + SET default_transaction_isolation := 'RepeatableRead'; + ''') + + try: + with self.assertRaisesRegex( + edgedb.TransactionError, + 'cannot execute.*RepeatableRead', + ): + await self.con.query(''' + INSERT Tmp { + tmp := 'aaa' + }; + ''') + finally: + await self.con.query(''' + CONFIGURE SESSION + RESET default_transaction_isolation; + ''') + + self.assertEqual( + await self.con.query('SELECT 42'), + [42]) + + async def test_server_proto_tx_25(self): + # default_transaction_isolation < Serializable overrides read-write + + try: + await self.con.query(''' + CONFIGURE SESSION + SET default_transaction_isolation := 'RepeatableRead'; + ''') + await self.con.query(''' + CONFIGURE SESSION + SET default_transaction_access_mode := 'ReadWrite'; + ''') + + with self.assertRaisesRegex( + edgedb.TransactionError, + 'cannot execute.*RepeatableRead', + ): + await self.con.query(''' + INSERT Tmp { + tmp := 'aaa' + }; + ''') + finally: + await self.con.query(''' + CONFIGURE SESSION + RESET default_transaction_access_mode; + ''') + await self.con.query(''' + CONFIGURE SESSION + RESET default_transaction_isolation; + ''') + + self.assertEqual( + await self.con.query('SELECT 42'), + [42]) + + async def test_server_proto_tx_26(self): + # Test that default_transaction_access_mode is respected + + await self.con.query(''' + CONFIGURE SESSION + SET default_transaction_access_mode := 'ReadOnly'; + ''') + + try: + self.assertEqual( + await self.con.query( + 'select sys::get_transaction_isolation();', + ), + ["Serializable"], + ) + + with self.assertRaisesRegex( + edgedb.TransactionError, + 'cannot execute.*ReadOnly', + ): + await self.con.query(''' + INSERT Tmp { + tmp := 'aaa' + }; + ''') + finally: + await self.con.query(''' + CONFIGURE SESSION + RESET default_transaction_access_mode; + ''') + + self.assertEqual( + await self.con.query('SELECT 42'), + [42]) + + async def test_server_proto_tx_27(self): + # Test that START TRANSACTION respects the default isolation + + try: + await self.con.query(''' + CONFIGURE SESSION + SET default_transaction_isolation := 'RepeatableRead'; + ''') + await self.con.query(''' + START TRANSACTION; + ''') + + self.assertEqual( + await self.con.query( + 'select sys::get_transaction_isolation();', + ), + ["RepeatableRead"], + ) + + finally: + await self.con.query(f''' + ROLLBACK; + ''') + await self.con.query(''' + CONFIGURE SESSION + RESET default_transaction_isolation; + ''') + + self.assertEqual( + await self.con.query('SELECT 42'), + [42]) + + async def test_server_proto_tx_28(self): + # Test that non-serializable START TRANSACTION enforces read-only + + try: + await self.con.query(''' + CONFIGURE SESSION + SET default_transaction_isolation := 'RepeatableRead'; + ''') + await self.con.query(''' + START TRANSACTION; + ''') + + with self.assertRaisesRegex( + edgedb.TransactionError, + 'read-only transaction'): + + await self.con.query(''' + INSERT Tmp { + tmp := 'aaa' + }; + ''') + finally: + await self.con.query(f''' + ROLLBACK; + ''') + await self.con.query(''' + CONFIGURE SESSION + RESET default_transaction_isolation; + ''') + + self.assertEqual( + await self.con.query('SELECT 42'), + [42]) + + async def test_server_proto_tx_29(self): + # Test that START TRANSACTION respects default read-only + + try: + await self.con.query(''' + CONFIGURE SESSION + SET default_transaction_access_mode:= 'ReadOnly'; + ''') + await self.con.query(''' + START TRANSACTION; + ''') + + self.assertEqual( + await self.con.query( + 'select sys::get_transaction_isolation();', + ), + ["Serializable"], + ) + + with self.assertRaisesRegex( + edgedb.TransactionError, + 'read-only transaction'): + + await self.con.query(''' + INSERT Tmp { + tmp := 'aaa' + }; + ''') + finally: + await self.con.query(f''' + ROLLBACK; + ''') + await self.con.query(''' + CONFIGURE SESSION + RESET default_transaction_access_mode; + ''') + + self.assertEqual( + await self.con.query('SELECT 42'), + [42]) + + async def test_server_proto_tx_30(self): + # Test that non-serializable START TRANSACTION conflicts read-write + + try: + await self.con.query(''' + CONFIGURE SESSION + SET default_transaction_isolation := 'RepeatableRead'; + ''') + with self.assertRaisesRegex( + edgedb.TransactionError, + 'only supported in read-only transactions', + ): + await self.con.query(''' + START TRANSACTION READ WRITE; + ''') + + finally: + await self.con.query(f''' + ROLLBACK; + ''') + await self.con.query(''' + CONFIGURE SESSION + RESET default_transaction_isolation; + ''') + + self.assertEqual( + await self.con.query('SELECT 42'), + [42]) + + async def test_server_proto_tx_31(self): + # Test that non-serializable START TRANSACTION works fine with the + # default read-only + + try: + await self.con.query(''' + CONFIGURE SESSION + SET default_transaction_access_mode:= 'ReadOnly'; + ''') + await self.con.query(''' + START TRANSACTION ISOLATION REPEATABLE READ; + ''') + + self.assertEqual( + await self.con.query( + 'select sys::get_transaction_isolation();', + ), + ["RepeatableRead"], + ) + + with self.assertRaisesRegex( + edgedb.TransactionError, + 'read-only transaction'): + + await self.con.query(''' + INSERT Tmp { + tmp := 'aaa' + }; + ''') + finally: + await self.con.query(f''' + ROLLBACK; + ''') + await self.con.query(''' + CONFIGURE SESSION + RESET default_transaction_access_mode; + ''') + + self.assertEqual( + await self.con.query('SELECT 42'), + [42]) + class TestServerProtoMigration(tb.QueryTestCase):