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:
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):
|
||||
@@ -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;
|
||||
@@ -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}",
|
||||
Reference in new issue
Block a user