Skip to content

Replication trigger emits malformed Cypher for every relationship row #4

Description

@prrao87

Severity: high. No relationship ever replicates.

Environment

  • PostgreSQL 18.4 (Homebrew), macOS arm64
  • pg_ladybug main @ e5182153f1d4b588e7fd758d69c1406a6649a79f
  • liblbug 0.19.1

Summary

The Cypher generated for an edge INSERT has two major issues:

  1. The id property is emitted twice.
  2. The endpoint columns (from_col / to_col) are copied into the
    relationship's property map, where no such properties exist.

The same endpoint leak affects the SET list produced for UPDATE. Every
relationship statement therefore fails at replay with
Binder exception: Cannot find property src_id for r.

Minimal reproduction

CREATE DATABASE bugrepro;
\c bugrepro

CREATE EXTENSION pg_ladybug;

CREATE TABLE node_city (
    id   bigint PRIMARY KEY,
    name text NOT NULL
);

CREATE TABLE rel_route (
    id       bigint PRIMARY KEY,
    src_id   bigint NOT NULL REFERENCES node_city(id),
    dst_id   bigint NOT NULL REFERENCES node_city(id),
    distance double precision NOT NULL
);

SELECT ladybug.register_node('City',  'node_city', 'id', NULL, 'demo');
SELECT ladybug.register_edge('Route', 'rel_route', 'src_id', 'dst_id', 'id', 'demo');
SELECT ladybug.enable_replication('demo', 'INSERT,UPDATE,DELETE');

INSERT INTO node_city VALUES (1, 'Toronto'), (2, 'Vancouver');
INSERT INTO rel_route VALUES (1, 1, 2, 3358.0);
UPDATE rel_route SET distance = 3400.0 WHERE id = 1;

SELECT operation, cypher FROM ladybug.replication_log('demo') ORDER BY id;

Actual output

 operation |                                            cypher
-----------+---------------------------------------------------------------------------------------------
 INSERT    | CREATE (n:City {id: 1, name: 'Toronto'})
 INSERT    | CREATE (n:City {id: 2, name: 'Vancouver'})
 INSERT    | MATCH (a {id: 1}), (b {id: 2}) CREATE (a)-[r:Route {id: 1, id: 1, src_id: 1, dst_id: 2, distance: 3358}]->(b)
 UPDATE    | MATCH (a {id: 1}), (b {id: 2}) MERGE (a)-[r:Route {id: 1}]->(b) SET r.src_id = 1, r.dst_id = 2, r.distance = 3400

Note {id: 1, id: 1, src_id: 1, dst_id: 2, ...} on the INSERT and
SET r.src_id = ..., r.dst_id = ... on the UPDATE.

Expected

MATCH (a {id: 1}), (b {id: 2}) CREATE (a)-[r:Route {id: 1, distance: 3358}]->(b)
MATCH (a {id: 1}), (b {id: 2}) MERGE (a)-[r:Route {id: 1}]->(b) SET r.distance = 3400

Downstream failure

Against a native schema:

SELECT * FROM ladybug.cypher(
  'CREATE NODE TABLE City(id INT64, name STRING, PRIMARY KEY(id))') AS t(ok text);
SELECT * FROM ladybug.cypher(
  'CREATE REL TABLE Route(FROM City TO City, id INT64, distance DOUBLE)') AS t(ok text);

SELECT ladybug.replay_replication('demo');
NOTICE:  ladybug: replay skipped: MATCH (a {id: 1}), (b {id: 2}) CREATE (a)-[r:Route {id: 1, id: 1, src_id: 1, dst_id: 2, distance: 3358}]->(b)
DETAIL:  ladybug: query failed: Binder exception: Cannot find property src_id for r.

In a larger run, all 830 relationships failed this way while the originating
Postgres writes all succeeded, so Postgres and the ladybug graph silently diverge.

Source

pg_ladybug.c:987-1012. The edge INSERT path writes id explicitly and then
calls append_all_props(), which appends every column including id again and
both endpoint columns. The UPDATE path's append_set_items() skips only the ID
column.

Suggested fix

Give the property appenders an exclusion set. For edges, exclude id_column,
from_col, and to_col, then add id_column exactly once. Cover INSERT,
UPDATE, and DELETE with replay tests against a native relationship schema.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions