Rename peo4c to peo

Renames the Python package, environment variables (PEO4C_* to PEO_*), the
SQL helper schema, image paths and the entrypoint script.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Bart BroereandClaude Opus 5.5 committed 2026-10-14 15:21:42 +02:00
1 parent 7bd044ddb9
commit fd0dda6470
17 files changed
+124 -124

No files matched your search

File renamed without changes.
@@ -1,6 +1,6 @@
# SPDX-FileCopyrightText: 2026 Bart Broere
# SPDX-License-Identifier: GPL-3.0-or-later
"""peo4c bridge: serves neo4j-browser and translates Bolt/Cypher to PostgreSQL + AGE."""
"""peo bridge: serves neo4j-browser and translates Bolt/Cypher to PostgreSQL + AGE."""
import asyncio
import functools
@@ -20,19 +20,19 @@ def env(name, default):
async def main():
logging.basicConfig(level=env("PEO4C_LOG_LEVEL", "INFO").upper(),
logging.basicConfig(level=env("PEO_LOG_LEVEL", "INFO").upper(),
format="%(asctime)s %(levelname)s %(name)s: %(message)s")
log = logging.getLogger("peo4c")
log = logging.getLogger("peo")
bolt_port = int(env("PEO4C_BOLT_PORT", "7687"))
http_port = int(env("PEO4C_HTTP_PORT", "7474"))
advertised_port = env("PEO4C_ADVERTISED_BOLT_PORT", str(bolt_port))
bolt_port = int(env("PEO_BOLT_PORT", "7687"))
http_port = int(env("PEO_HTTP_PORT", "7474"))
advertised_port = env("PEO_ADVERTISED_BOLT_PORT", str(bolt_port))
config = Config(
pg_host=env("PEO4C_PG_HOST", "127.0.0.1"),
pg_port=int(env("PEO4C_PG_PORT", "5432")),
pg_database=env("PEO4C_PG_DATABASE", env("POSTGRES_DB", env("POSTGRES_USER", "postgres"))),
default_graph=env("PEO4C_DEFAULT_GRAPH", "neo4j"),
advertised_bolt=f"{env('PEO4C_ADVERTISED_HOST', 'localhost')}:{advertised_port}",
pg_host=env("PEO_PG_HOST", "127.0.0.1"),
pg_port=int(env("PEO_PG_PORT", "5432")),
pg_database=env("PEO_PG_DATABASE", env("POSTGRES_DB", env("POSTGRES_USER", "postgres"))),
default_graph=env("PEO_DEFAULT_GRAPH", "neo4j"),
advertised_bolt=f"{env('PEO_ADVERTISED_HOST', 'localhost')}:{advertised_port}",
)
bolt = await asyncio.start_server(functools.partial(handle_client, config=config), "0.0.0.0", bolt_port)
@@ -40,23 +40,23 @@ async def main():
log.info("Bolt listening on :%d (PostgreSQL %s:%d/%s, default graph %r)", bolt_port,
config.pg_host, config.pg_port, config.pg_database, config.default_graph)
browser_dir = env("PEO4C_BROWSER_DIR", "")
browser_dir = env("PEO_BROWSER_DIR", "")
if browser_dir:
http = HttpServer(browser_dir, bolt_port)
servers.append(await asyncio.start_server(http.handle, "0.0.0.0", http_port))
log.info("neo4j-browser on http://localhost:%d/browser/", http_port)
tasks = [s.serve_forever() for s in servers]
if env("PEO4C_PROPERTY_COLUMNS", "false") == "true":
if env("PEO_PROPERTY_COLUMNS", "false") == "true":
conninfo = make_conninfo(
host=env("PEO4C_PROPERTY_COLUMNS_PG_HOST", "/var/run/postgresql"),
host=env("PEO_PROPERTY_COLUMNS_PG_HOST", "/var/run/postgresql"),
port=config.pg_port,
dbname=config.pg_database,
user=env("PEO4C_PROPERTY_COLUMNS_PG_USER", env("POSTGRES_USER", "postgres")),
application_name="peo4c property columns",
user=env("PEO_PROPERTY_COLUMNS_PG_USER", env("POSTGRES_USER", "postgres")),
application_name="peo property columns",
)
tasks.append(property_columns.sync_forever(
conninfo, float(env("PEO4C_PROPERTY_COLUMNS_INTERVAL", "5"))))
conninfo, float(env("PEO_PROPERTY_COLUMNS_INTERVAL", "5"))))
await asyncio.gather(*tasks)
File renamed without changes.
@@ -21,14 +21,14 @@ from .cypher import (CypherError, expand_id_in_lists, first_keyword, inline_para
return_columns, rewrite_for_age, strip_statement)
from .packstream import Node, Relationship
log = logging.getLogger("peo4c.backend")
log = logging.getLogger("peo.backend")
NEO4J_VERSION = "5.26.0"
SYSTEM_DB = "system"
SYSTEM_LABELS = ("_ag_label_vertex", "_ag_label_edge")
GRAPH_NAME = re.compile(r"^[A-Za-z_][A-Za-z0-9_.\-]{2,62}$")
# Procedures emulated by peo4c, reported through SHOW PROCEDURES.
# Procedures emulated by peo, reported through SHOW PROCEDURES.
PROCEDURES = [
("db.labels", "List all node labels in the graph.", "db.labels() :: (label :: STRING)"),
("db.relationshipTypes", "List all relationship types in the graph.",
@@ -123,7 +123,7 @@ class Session:
self.conn = await psycopg.AsyncConnection.connect(
host=self.config.pg_host, port=self.config.pg_port, dbname=self.config.pg_database,
user=self.user, password=password, autocommit=True,
application_name="peo4c")
application_name="peo")
except psycopg.OperationalError as e:
msg = str(e)
if "password authentication failed" in msg or "role" in msg and "does not exist" in msg:
@@ -201,7 +201,7 @@ class Session:
if db == SYSTEM_DB:
raise BoltFailure("Neo.ClientError.Statement.NotSystemDatabaseCommand",
"This is an administration command and it should be executed "
"against the system database: peo4c only supports a few of them.")
"against the system database: peo only supports a few of them.")
result = await self._run_cypher(query, params or {}, db)
except CypherError as e:
raise BoltFailure(e.code, e.message) from e
@@ -217,9 +217,9 @@ class Session:
f"{keyword} is not supported by Apache AGE.")
text = expand_id_in_lists(inline_parameters(rewrite_for_age(text), params))
columns = return_columns(text)
tag = "peo4c"
tag = "peo"
while f"${tag}$" in text:
tag = "peo4c_" + secrets.token_hex(4)
tag = "peo_" + secrets.token_hex(4)
col_defs = ", ".join(f"c{i} ag_catalog.agtype" for i in range(max(len(columns), 1)))
stmt = (f"SELECT * FROM ag_catalog.cypher({sql.Literal(graph).as_string(self.conn)}, "
f"${tag}$ {text} ${tag}$) AS ({col_defs})")
@@ -309,7 +309,7 @@ class Session:
if m:
raise BoltFailure("Neo.ClientError.Procedure.ProcedureNotFound",
f"There is no procedure with the name `{m.group(0)[5:]}` registered "
"for this database instance (peo4c).")
"for this database instance (peo).")
return None
async def _show_databases(self, special, name):
@@ -337,9 +337,9 @@ class Session:
raise BoltFailure("Neo.ClientError.Statement.ArgumentError",
f"Invalid graph name '{name}': AGE graph names must be 3-63 characters "
"of letters, digits, '_', '.' or '-', starting with a letter or '_'.")
if name == "peo4c":
if name == "peo":
raise BoltFailure("Neo.ClientError.Statement.ArgumentError",
"The name 'peo4c' is reserved: it is the schema of peo4c's own SQL helpers.")
"The name 'peo' is reserved: it is the schema of peo's own SQL helpers.")
if name in await self.graphs():
if if_not_exists:
return Result([], query_type="s")
@@ -369,7 +369,7 @@ class Session:
async def _cypher_values(self, graph, query):
rows = await self._sql(
f"SELECT * FROM ag_catalog.cypher({sql.Literal(graph).as_string(self.conn)}, "
f"$peo4c$ {query} $peo4c$) AS (c0 ag_catalog.agtype)")
f"$peo$ {query} $peo$) AS (c0 ag_catalog.agtype)")
return [agtype.parse(r[0]) for r in rows]
async def _property_keys(self, graph):
+5 -5
View File
@@ -18,7 +18,7 @@ from .backend import BoltFailure, Config, Session
from .packstream import Structure
from .web import discovery_document
log = logging.getLogger("peo4c.bolt")
log = logging.getLogger("peo.bolt")
MAGIC = b"\x60\x60\xB0\x17"
SUPPORTED = [(5, 4), (5, 3), (5, 2), (5, 1), (5, 0), (4, 4)]
@@ -248,7 +248,7 @@ class BoltConnection:
except Exception as e: # noqa: BLE001 - report anything to the client
log.exception("%s: internal error", self.id)
self.results.clear()
await self.failure("Neo.DatabaseError.General.UnknownError", f"peo4c internal error: {e}")
await self.failure("Neo.DatabaseError.General.UnknownError", f"peo internal error: {e}")
return True
async def dispatch(self, tag, f):
@@ -281,7 +281,7 @@ class BoltConnection:
await self.success()
elif tag == COMMIT:
await self.require_session().commit()
await self.success({"bookmark": "peo4c:bookmark"})
await self.success({"bookmark": "peo:bookmark"})
elif tag == ROLLBACK:
await self.require_session().rollback()
await self.success()
@@ -296,7 +296,7 @@ class BoltConnection:
async def logon(self, auth):
if auth.get("scheme") != "basic":
raise BoltFailure("Neo.ClientError.Security.Unauthorized",
"peo4c requires basic authentication with a PostgreSQL user name and password.")
"peo requires basic authentication with a PostgreSQL user name and password.")
session = Session(self.config, auth.get("principal", ""))
await session.connect(auth.get("credentials", ""))
self.session = session
@@ -345,7 +345,7 @@ class BoltConnection:
del self.results[qid]
metadata = {"type": result.query_type, "t_last": 0, "db": result.db}
if not self.session.in_transaction:
metadata["bookmark"] = "peo4c:bookmark"
metadata["bookmark"] = "peo:bookmark"
await self.success(metadata)
@@ -112,7 +112,7 @@ def return_columns(text: str) -> list:
expr = text[start:end].strip()
if expr == "*":
raise CypherError("Neo.ClientError.Statement.FeatureNotSupported",
"RETURN * is not supported by peo4c; list the returned variables explicitly.")
"RETURN * is not supported by peo; list the returned variables explicitly.")
alias = None
for a_start, a_end in _top_level(s, r"(?<![\w.$])AS\s+(`[^`]*`|\w+)\s*$", start, end):
alias = re.match(r"AS\s+(.*)", text[a_start:a_end].strip(), re.IGNORECASE | re.DOTALL).group(1)
@@ -195,7 +195,7 @@ def literal(v) -> str:
return "{" + ", ".join(
"`" + str(k).replace("`", "``") + "`: " + literal(x) for k, x in v.items()) + "}"
raise CypherError("Neo.ClientError.Statement.TypeError",
f"Parameters of type {type(v).__name__} are not supported by peo4c")
f"Parameters of type {type(v).__name__} are not supported by peo")
# neo4j-browser's "expand node" query uses a pattern comprehension, which AGE
@@ -207,8 +207,8 @@ _NEIGHBOUR_COUNT = re.compile(
def rewrite_for_age(text: str) -> str:
return _NEIGHBOUR_COUNT.sub(
lambda m: f"OPTIONAL MATCH ({m.group(1)})-[peo4c_r]-() "
f"WITH {m.group(1)}, count(peo4c_r) AS {m.group(2)}",
lambda m: f"OPTIONAL MATCH ({m.group(1)})-[peo_r]-() "
f"WITH {m.group(1)}, count(peo_r) AS {m.group(2)}",
text)
File renamed without changes.
@@ -3,7 +3,7 @@
"""Keeps generated property columns on AGE label tables up to date.
Installs the procedures in ``property_columns.sql`` and calls
``peo4c.sync_property_columns()`` every ``interval`` seconds. This needs a
``peo.sync_property_columns()`` every ``interval`` seconds. This needs a
connection that may alter every graph's tables, so it uses its own
(superuser) connection rather than the credentials of Bolt users.
"""
@@ -14,7 +14,7 @@ from pathlib import Path
import psycopg
log = logging.getLogger("peo4c.property_columns")
log = logging.getLogger("peo.property_columns")
INSTALL_SQL = (Path(__file__).parent / "property_columns.sql").read_text()
@@ -34,7 +34,7 @@ async def sync_forever(conninfo, interval):
log.info("syncing property columns every %gs", interval)
error = None
while True:
await conn.execute("CALL peo4c.sync_property_columns()")
await conn.execute("CALL peo.sync_property_columns()")
await asyncio.sleep(interval)
except psycopg.Error as e:
# PostgreSQL is still starting or restarted, or AGE is not
@@ -13,7 +13,7 @@
--
-- AGE's Cypher writes bypass the executor and fire no triggers, row or
-- statement level, so new keys cannot be caught by a trigger. Instead
-- CALL peo4c.sync_property_columns() looks for them; the peo4c bridge calls
-- CALL peo.sync_property_columns() looks for them; the peo bridge calls
-- it every few seconds. It only scans label tables that saw inserts or updates
-- since its previous run, according to the statistics collector, and commits
-- after each table so a table rewrite never holds locks on other tables.
@@ -22,9 +22,9 @@
-- Columns are text, like ->> on json, because a property can hold values of
-- different types in different rows and a failing cast would make writes fail.
CREATE SCHEMA IF NOT EXISTS peo4c;
CREATE SCHEMA IF NOT EXISTS peo;
CREATE TABLE IF NOT EXISTS peo4c.property_column_state (
CREATE TABLE IF NOT EXISTS peo.property_column_state (
relid oid PRIMARY KEY,
changes bigint NOT NULL -- inserts + updates seen when the table was last scanned
);
@@ -33,7 +33,7 @@ CREATE TABLE IF NOT EXISTS peo4c.property_column_state (
-- that name yet, and returns the keys it added. Keys that are empty or longer
-- than PostgreSQL's 63-byte identifier limit are skipped, as are keys that
-- clash with an existing column such as id, start_id or end_id.
CREATE OR REPLACE FUNCTION peo4c.add_property_columns(tbl regclass)
CREATE OR REPLACE FUNCTION peo.add_property_columns(tbl regclass)
RETURNS SETOF text LANGUAGE plpgsql AS $$
DECLARE
new_keys text[];
@@ -63,13 +63,13 @@ BEGIN
END
$$;
CREATE OR REPLACE PROCEDURE peo4c.sync_property_columns()
CREATE OR REPLACE PROCEDURE peo.sync_property_columns()
LANGUAGE plpgsql AS $$
DECLARE
t record;
added text[];
BEGIN
DELETE FROM peo4c.property_column_state s
DELETE FROM peo.property_column_state s
WHERE NOT EXISTS (SELECT FROM pg_catalog.pg_class WHERE oid = s.relid);
FOR t IN
@@ -77,7 +77,7 @@ BEGIN
FROM ag_catalog.ag_label AS l
CROSS JOIN LATERAL (SELECT pg_stat_get_tuples_inserted(l.relation)
+ pg_stat_get_tuples_updated(l.relation) AS changes) AS c
LEFT JOIN peo4c.property_column_state AS s ON s.relid = l.relation
LEFT JOIN peo.property_column_state AS s ON s.relid = l.relation
WHERE l.name NOT IN ('_ag_label_vertex', '_ag_label_edge')
AND s.changes IS DISTINCT FROM c.changes
LOOP
@@ -85,10 +85,10 @@ BEGIN
-- Adding a column rewrites the table under an exclusive lock;
-- rather than queue behind a long transaction, retry next run.
PERFORM set_config('lock_timeout', '2s', true);
SELECT array_agg(key) INTO added FROM peo4c.add_property_columns(t.tbl) AS key;
SELECT array_agg(key) INTO added FROM peo.add_property_columns(t.tbl) AS key;
-- The rewrite counts as inserts in this transaction; include them
-- so the table is not scanned again for its own rewrite.
INSERT INTO peo4c.property_column_state AS s
INSERT INTO peo.property_column_state AS s
VALUES (t.tbl, t.changes + pg_stat_get_xact_tuples_inserted(t.tbl)
+ pg_stat_get_xact_tuples_updated(t.tbl))
ON CONFLICT (relid) DO UPDATE SET changes = excluded.changes;
+2 -2
View File
@@ -12,7 +12,7 @@ from urllib.parse import unquote, urlsplit
from .backend import NEO4J_VERSION
log = logging.getLogger("peo4c.http")
log = logging.getLogger("peo.http")
class HttpServer:
@@ -77,7 +77,7 @@ def discovery_document(headers, bolt_port) -> bytes:
host = host[:host.index("]") + 1]
else:
host = host.split(":", 1)[0]
port = os.environ.get("PEO4C_ADVERTISED_BOLT_PORT", str(bolt_port))
port = os.environ.get("PEO_ADVERTISED_BOLT_PORT", str(bolt_port))
return json.dumps({
"bolt_routing": f"neo4j://{host}:{port}",
"bolt_direct": f"bolt://{host}:{port}",