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

Expose default_transaction_... Postgres settings #8276

Merged
merged 10 commits into from
Feb 5, 2025
2 changes: 1 addition & 1 deletion edb/buildmeta.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = 2024_02_04_00_00
EDGEDB_MAJOR_VERSION = 7


Expand Down
75 changes: 75 additions & 0 deletions edb/ir/statypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
Optional,
Self,
TypeVar,
TYPE_CHECKING,
)

import dataclasses
Expand All @@ -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()


Expand Down Expand Up @@ -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}")
41 changes: 41 additions & 0 deletions edb/lib/cfg.edgeql
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,47 @@ ALTER TYPE cfg::AbstractConfig {
SET default := <std::duration>'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"';
Expand Down
8 changes: 8 additions & 0 deletions edb/lib/sys.edgeql
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ CREATE SCALAR TYPE sys::TransactionIsolation
EXTENDING enum<RepeatableRead, Serializable>;


CREATE SCALAR TYPE sys::TransactionAccessMode
EXTENDING enum<ReadOnly, ReadWrite>;


CREATE SCALAR TYPE sys::TransactionDeferrability
EXTENDING enum<Deferrable, NotDeferrable>;


CREATE SCALAR TYPE sys::VersionStage
EXTENDING enum<dev, alpha, beta, rc, final>;

Expand Down
2 changes: 1 addition & 1 deletion edb/pgsql/compiler/clauses.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think we should try to invalidate the persistent cache because of this change, or should we just instruct affected users to clear the cache manually?

(This is a more general question about miscompilation bugs and the persistent cache that I'm not sure we've ever properly figured out)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh true! One way that might work is to always "upgrade" the persistent cache automatically after server upgrade (by recompiling the cached queries) even if there are no compiler changes - if it's already in downtime maintenance, why not make the most of it and make sure the query cache is up to date. Let me draft a PR for that.

should we just instruct affected users to clear the cache manually?

I'd use that as the last resort when we failed to make the server fix itself (which is likely the case now, but let me fix that).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is extending the downtime (possibly by a lot) better than a cache flush? It's not totally obvious to me.

Maybe it would be best to come up with an empty cache and then repopulate in the background?

Copy link
Member

@msullivan msullivan Feb 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't consider cache recompilation to be a release blocker, by the way; it's been an outstanding issue through all of 5.0, so we can live with it a little longer.

(Fixing it is still good, though; but lower priority than getting this PR finalized and landed)

where_clause=pgast.Expr(
name="=",
lexpr=pgast.ColumnRef(name=["id"]),
Expand Down
30 changes: 0 additions & 30 deletions edb/pgsql/metaschema.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'))
Expand Down Expand Up @@ -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()),
Expand Down
8 changes: 5 additions & 3 deletions edb/server/bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -2563,9 +2563,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(
Expand Down
48 changes: 33 additions & 15 deletions edb/server/compiler/compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 += ';'
Expand Down Expand Up @@ -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"]),
Expand Down
4 changes: 4 additions & 0 deletions edb/server/compiler/explain/to_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
import enum
import uuid

from edb.ir import statypes


class ToJson:
def to_json(self) -> Any:
Expand All @@ -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}")
1 change: 1 addition & 0 deletions edb/server/dbview/dbview.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
33 changes: 26 additions & 7 deletions edb/server/dbview/dbview.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
Loading