Repository: geldata/gel
Pull request: pg-ext: pretend to be Postgres 13
Patched `server_version` and `server_version_num` to 13.9

* Also fixed conn params `session_authorization` and `application_name`
* SQL `version()` now shows EdgeDB version info

Fixes #5089

Problem statement:
Advertise EdgeDB server as Postgres 13 compatible through SQL
The current SQL support (through Postgres binary protocol) in EdgeDB is actually only compatible with Postgres 13, regardless of the actual version of the backend Postgres in use. We should also advertise `13` instead of `14` or higher through the binary protocol.

Hints from issue discussion:

Commit message: pg-ext: pretend to be Postgres 13

* Also fixed conn params session_authorization and application_name
* SQL `version()` now shows EdgeDB version info

Patch to review:
diff --git a/edb/buildmeta.py b/edb/buildmeta.py
index b548bc227eb..f3b6e169da7 100644
--- a/edb/buildmeta.py
+++ b/edb/buildmeta.py
@@ -588,3 +588,24 @@ def get_cache_src_dirs():
 def get_default_tenant_id() -> str:
     catver = EDGEDB_CATALOG_VERSION
     return f'V{catver:x}'
+
+
+def get_version_line() -> str:
+    ver_meta = get_version_metadata()
+
+    extras = []
+    source = ""
+    if build_date := ver_meta["build_date"]:
+        nice_date = build_date.strftime("%Y-%m-%dT%H:%MZ")
+        source += f" on {nice_date}"
+    if ver_meta["scm_revision"]:
+        source += f" from revision {ver_meta['scm_revision']}"
+        if source_date := ver_meta["source_date"]:
+            nice_date = source_date.strftime("%Y-%m-%dT%H:%MZ")
+            source += f" ({nice_date})"
+    if source:
+        extras.append(f", built{source}")
+    if ver_meta["target"]:
+        extras.append(f"for {ver_meta['target']}")
+
+    return get_version_string() + " ".join(extras)
diff --git a/edb/pgsql/resolver/static.py b/edb/pgsql/resolver/static.py
index faede32ef83..9355aec4e03 100644
--- a/edb/pgsql/resolver/static.py
+++ b/edb/pgsql/resolver/static.py
@@ -111,6 +111,10 @@ def eval_FuncCall(
     if fn_name == 'current_query':
         return pgast.StringConstant(val=ctx.options.current_query)
 
+    if fn_name == 'version':
+        from edb import buildmeta
+        return pgast.StringConstant(val="EdgeDB " + buildmeta.get_version_line())
+
     return None
 
 
diff --git a/edb/server/compiler/compiler.py b/edb/server/compiler/compiler.py
index 6d54027d8c5..f62fa6af4c8 100644
--- a/edb/server/compiler/compiler.py
+++ b/edb/server/compiler/compiler.py
@@ -526,17 +526,28 @@ def parse_search_path(search_path_str: str) -> list[str]:
                 if isinstance(arg, pgast.StringConstant)
             ]
 
-        frontend_only = {'search_path'}
+        # frontend-only settings and their mutability
+        frontend_only = {
+            'search_path': True,
+            'server_version': False,
+            'server_version_num': False,
+        }
         stmts = pg_parser.parse(query_str)
         sql_units = []
         for stmt in stmts:
+            fe_only = stmt.name in frontend_only
             if isinstance(stmt, pgast.VariableSetStmt):
                 args = {
                     "query": pg_codegen.generate_source(stmt),
-                    "frontend_only": stmt.name in frontend_only,
+                    "frontend_only": fe_only,
                     "is_local": stmt.scope == pgast.OptionsScope.TRANSACTION,
                 }
-                if stmt.name in frontend_only:
+                if fe_only:
+                    if not frontend_only[stmt.name]:
+                        raise errors.QueryError(
+                            f'parameter "{stmt.name}" cannot be changed',
+                            pgext_code='55P02',  # cant_change_runtime_param
+                        )
                     value = pg_codegen.generate_source(stmt.args, pretty=False)
                     args["set_vars"] = {stmt.name: value}
                 elif stmt.scope == pgast.OptionsScope.SESSION:
@@ -550,15 +561,17 @@ def parse_search_path(search_path_str: str) -> list[str]:
                     args["set_vars"] = {stmt.name: value}
                 unit = dbstate.SQLQueryUnit(**args)
             elif isinstance(stmt, pgast.VariableResetStmt):
+                if fe_only and not frontend_only[stmt.name]:
+                    raise errors.QueryError(
+                        f'parameter "{stmt.name}" cannot be changed',
+                        pgext_code='55P02',  # cant_change_runtime_param
+                    )
                 args = {
                     "query": pg_codegen.generate_source(stmt),
-                    "frontend_only": stmt.name in frontend_only,
+                    "frontend_only": fe_only,
                     "is_local": stmt.scope == pgast.OptionsScope.TRANSACTION,
                 }
-                if (
-                    stmt.name in frontend_only
-                    or stmt.scope == pgast.OptionsScope.SESSION
-                ):
+                if fe_only or stmt.scope == pgast.OptionsScope.SESSION:
                     args["set_vars"] = {stmt.name: None}
                 unit = dbstate.SQLQueryUnit(**args)
             elif isinstance(stmt, pgast.VariableShowStmt):
@@ -566,7 +579,7 @@ def parse_search_path(search_path_str: str) -> list[str]:
                 unit = dbstate.SQLQueryUnit(
                     query=source,
                     get_var=stmt.name,
-                    frontend_only=stmt.name in frontend_only,
+                    frontend_only=fe_only,
                 )
             elif isinstance(stmt, pgast.SetTransactionStmt):
                 args = {"query": pg_codegen.generate_source(stmt)}
diff --git a/edb/server/defines.py b/edb/server/defines.py
index 51bba63c249..8edd018dcd9 100644
--- a/edb/server/defines.py
+++ b/edb/server/defines.py
@@ -84,3 +84,6 @@
 
 _TLS_CERT_RELOAD_MAX_RETRIES = 5
 _TLS_CERT_RELOAD_EXP_INTERVAL = 0.1
+
+PGEXT_POSTGRES_VERSION = "13.9"
+PGEXT_POSTGRES_VERSION_NUM = 130009
diff --git a/edb/server/main.py b/edb/server/main.py
index 67dd49529d9..a3d768efd2a 100644
--- a/edb/server/main.py
+++ b/edb/server/main.py
@@ -429,24 +429,7 @@ async def run_server(
     global server
     server = server_mod
 
-    ver_meta = buildmeta.get_version_metadata()
-    extras = []
-    source = ""
-    if build_date := ver_meta["build_date"]:
-        nice_date = build_date.strftime("%Y-%m-%dT%H:%MZ")
-        source += f" on {nice_date}"
-    if ver_meta["scm_revision"]:
-        source += f" from revision {ver_meta['scm_revision']}"
-        if source_date := ver_meta["source_date"]:
-            nice_date = source_date.strftime("%Y-%m-%dT%H:%MZ")
-            source += f" ({nice_date})"
-    if source:
-        extras.append(f", built{source}")
-    if ver_meta["target"]:
-        extras.append(f"for {ver_meta['target']}")
-
-    ver_line = buildmeta.get_version_string() + " ".join(extras)
-    logger.info(f"starting EdgeDB server {ver_line}")
+    logger.info(f"starting EdgeDB server {buildmeta.get_version_line()}")
     logger.info(f'instance name: {args.instance_name!r}')
     if devmode.is_in_dev_mode():
         logger.info(f'development mode active')
diff --git a/edb/server/protocol/pg_ext.pyx b/edb/server/protocol/pg_ext.pyx
index 9aef73052bc..09ac053a6be 100644
--- a/edb/server/protocol/pg_ext.pyx
+++ b/edb/server/protocol/pg_ext.pyx
@@ -37,6 +37,7 @@ from edb import errors
 from edb.common import debug
 from edb.pgsql.parser import exceptions as parser_errors
 from edb.server import args as srvargs
+from edb.server import defines
 from edb.server.compiler import dbstate
 from edb.server.pgcon import errors as pgerror
 from edb.server.pgcon.pgcon cimport PGAction, PGMessage
@@ -44,7 +45,11 @@ from edb.server.protocol cimport frontend
 
 cdef object logger = logging.getLogger('edb.server')
 cdef object DEFAULT_SETTINGS = immutables.Map()
-cdef object DEFAULT_FE_SETTINGS = immutables.Map({"search_path": "public"})
+cdef object DEFAULT_FE_SETTINGS = immutables.Map({
+    "search_path": "public",
+    "server_version": defines.PGEXT_POSTGRES_VERSION,
+    "server_version_num": str(defines.PGEXT_POSTGRES_VERSION_NUM),
+})
 cdef object DEFAULT_STATE = json.dumps(dict(DEFAULT_SETTINGS)).encode('utf-8')
 
 encodings.aliases.aliases["sql_ascii"] = "ascii"
@@ -654,9 +659,14 @@ cdef class PgConnection(frontend.FrontendConnection):
                 msg_buf = WriteBuffer.new_message(b'S')
                 msg_buf.write_str(name, "utf-8")
                 if name == "client_encoding":
-                    msg_buf.write_str(client_encoding, "utf-8")
-                else:
-                    msg_buf.write_str(value, "utf-8")
+                    value = client_encoding
+                elif name == "server_version":
+                    value = defines.PGEXT_POSTGRES_VERSION
+                elif name == "session_authorization":
+                    value = user
+                elif name == "application_name":
+                    value = self.server.get_instance_name()
+                msg_buf.write_str(value, "utf-8")
                 msg_buf.end_message()
                 buf.write_buffer(msg_buf)
                 if self.debug:
diff --git a/tests/test_sql_query.py b/tests/test_sql_query.py
index d9b0bce817f..b0c40a9aa9c 100644
--- a/tests/test_sql_query.py
+++ b/tests/test_sql_query.py
@@ -23,6 +23,7 @@
 
 try:
     import asyncpg
+    from asyncpg import serverversion
 except ImportError:
     pass
 
@@ -759,3 +760,25 @@ async def test_sql_query_client_encoding_2(self):
         await self.squery_values("set client_encoding to 'latin1'")
         with self.assertRaises(asyncpg.UntranslatableCharacterError):
             await self.squery_values('select * from "Genre"')
+
+    async def test_sql_query_server_version(self):
+        version = await self.scon.fetchval("show server_version")
+        self.assertEqual(
+            self.scon.get_server_version(),
+            serverversion.split_server_version_string(version),
+        )
+        with self.assertRaises(asyncpg.CantChangeRuntimeParamError):
+            await self.squery_values("set server_version to blah")
+        with self.assertRaises(asyncpg.CantChangeRuntimeParamError):
+            await self.squery_values("reset server_version")
+
+    async def test_sql_query_server_version_num(self):
+        await self.squery_values("show server_version_num")
+        with self.assertRaises(asyncpg.CantChangeRuntimeParamError):
+            await self.squery_values("set server_version_num to blah")
+        with self.assertRaises(asyncpg.CantChangeRuntimeParamError):
+            await self.squery_values("reset server_version_num")
+
+    async def test_sql_query_version(self):
+        version = await self.squery_values("select version()")
+        self.assertIn("EdgeDB", version[0][0])


Code review comments:
diff_hunk: @@ -526,17 +526,28 @@ def parse_search_path(search_path_str: str) -> list[str]:
                 if isinstance(arg, pgast.StringConstant)
             ]
 
-        frontend_only = {'search_path'}
+        # frontend-only settings and their mutability
+        frontend_only = {
+            'search_path': True,
+            'server_version': False,
+            'server_version_num': False,
+        }
         stmts = pg_parser.parse(query_str)
         sql_units = []
         for stmt in stmts:
+            fe_only = stmt.name in frontend_only
path: edb/server/compiler/compiler.py
text: @user1:
Did you mean to write `fe_only = frontend_only[stmt.name]`?

@author:
Ah no, that's a bad naming - let me update, thanks!