Docs
View as markdown

relate push

The relate push command, which copies a scoped subgraph of a store into Neo4j or Aura, with its flags, scope, merge keys, update policies, report and errors.

cli

relate push copies a subgraph of a local store A relate database: a directory, a single .rl file, or :memory:, opened with --database. Read more in the glossary into a Neo4j or Aura database over the HTTP Query API v2, then exits. It is the mirror of relate pull: the same credentials file, the same URI rules, the same transport. Push merges each node and relationship on a key taken from the store's constraints, sends batched, parameterized statements, never deletes anything on the remote, and prints a report of what it created, updated, and skipped.

--dry-run prints the plan without contacting the remote. No Neo4j server was available when this page was verified: the dry runs and refusals shown are recorded runs of the relate binary, and what a push does against a live remote is described from relate's CLI specification and its agent skill, not from a run.

Synopsis

relate push --to CREDS --database PATH [--query Q | --labels L1,L2 | --types T1,T2] [--batch-size N] [--nodes-update POLICY] [--rels-update POLICY] [--updated-at NAME[,NAME]] [--rel-mode MODE] [--merge-key LABEL:PROP[,PROP]] [--rel-key TYPE:PROP[,PROP]] [--unkeyed-labels ACTION] [--dry-run] [--json] [--allow-plaintext]
PartRequiredMeaning
--to CREDSyesThe remote: a credentials file, or a Query API URI
--database PATHyesThe source store: a directory or a single-file .rl store
--query, --labels or --typesnoWhat to push; at most one of the three, and without any, the whole store

push relate push: the command that copies a scoped subgraph of a store into Neo4j or Aura over the HTTP Query API; the mirror of relate pull. Read more in the glossary is a command, so it takes only its own flags. --namespace is refused in both positions, because push always reads the neo4j namespace.

Options

FlagArgumentDefaultScopeSinceDescription
--toCREDSrequiredrelate push—Env file (NEO4J_URI, NEO4J_USERNAME, NEO4J_PASSWORD, optional NEO4J_DATABASE), or a Query API URI with the username and password from the process environment.
--database, -dPATHrequiredrelate push—Source store directory or .rl file; not :memory:.
--queryQthe whole storerelate push—Push only what the statements return (nodes, relationships, paths); a ;-list, file path, or - for stdin. One of --query, --labels, --types.
--labelsL1,L2unsetrelate push—Only nodes with these labels, and the relationships between them.
--typesT1,T2unsetrelate push—Only relationships of these types, with both their endpoints.
--batch-sizeN5000relate push—Rows per batched statement.
--nodes-updatePOLICYaddrelate push—add (SET +=, keeps remote-only properties), replace (SET =) or skip (create missing only), per label or global.
--rels-updatePOLICYaddrelate push—The same policies, per relationship type or global.
--updated-atNAME[,NAME]unsetrelate push—Candidate timestamp properties; when both sides carry one, a strictly newer remote row is skipped.
--rel-modeMODEkeyedrelate push—keyed (merge on the type's resolved key), structural (one relationship of a type between two nodes) or create (non-idempotent).
--merge-keyLABEL:PROP[,PROP]the constraint-derived keyrelate push—Merge key for one label (repeatable).
--rel-keyTYPE:PROP[,PROP]the constraint-derived keyrelate push—Merge key for one relationship type (repeatable).
--unkeyed-labelsACTIONskiprelate push—skip (reported and counted) or create (non-idempotent) for a label with no merge key.
--dry-run—offrelate push—Print the plan without touching the remote.
--json—offrelate push—Print the run report as one JSON object instead of the table.
--allow-plaintext—offrelate push—Permit Basic auth to a non-loopback http:// Query API (insecure; prints a warning).

A — under Since: the data does not record the release that added the flag.

A flag that takes one value may appear once: a second --to, --query or --rel-mode is a usage error. --dry-run, --json and --allow-plaintext may also appear once. --merge-key and --rel-key repeat once per label or type, --updated-at repeats but never names a property twice, and --labels and --types add to their lists when repeated.

The remote and its credentials

--to takes a credentials file in the format relate pull --from reads (NEO4J_URI, NEO4J_USERNAME, NEO4J_PASSWORD, optional NEO4J_DATABASE), or a Query API URI. With a URI, the username, password and database come from the process environment; a missing one is an error such as NEO4J_USERNAME missing from push --to.

Everything else about the connection is pull's: the URI scheme mapping, the refusal of a URI that carries a password, the warning for a credentials file other users can read, and the refusal of plain http:// to a non-loopback host unless --allow-plaintext is given. The pull reference has the tables.

The source store

PropertyBehavior
--databaseA directory store or a single-file .rl store; :memory: is a usage error (exit code 2)
OpeningPush opens the store itself, as a writer: a store another process holds for writing fails with storage locked: <path> (exit code 1), and push has no --read-only flag
A path that does not existCreated as a new, empty store; the push then plans nothing
NamespaceAlways neo4j

Scope

SelectorWhat is pushed
NoneThe whole store
--query QThe nodes, relationships and paths the statements RETURN, with the endpoints of each relationship. Q is a statement, a ;-separated list, a file path, or - for stdin, with the same rule as pull: every RETURN item is a graph element
--labels L1,L2Nodes with any of these labels, and the relationships whose two endpoints are both in scope; a relationship with an endpoint out of scope is counted in a (dangling) row and not written
--types T1,T2Relationships of these relationship types, with both their endpoints

Two selectors together are a usage error: push takes one of --query, --labels, or --types (none pushes the whole store).

Merge keys

Push writes a node with MERGE on its label's merge key, so pushing twice does not duplicate it. The key comes from the store's uniqueness, node key and relationship key constraint A rule the store enforces on every write, such as a unique property, a required property, a property type or a node key; a violating write fails. Read more in the glossary, composite keys included. --merge-key LABEL:PROP[,PROP] sets or overrides one label's key, once per label.

CaseBehavior
A label with a keyUNWIND $rows AS row MERGE (n:`Label` {`key`: row.key.`key`}) SET …
A label without one, --unkeyed-labels skip (default)Not written; its row in the report says (unkeyed) and counts the nodes as skipped
A label without one, --unkeyed-labels createUNWIND $rows AS row CREATE (n:`Label`) SET n += row.props, where the SET follows --nodes-update (SET n = row.props under replace); pushing twice creates the nodes twice
A relationship with an unkeyed endpointNot written, counted (unkeyed), under either action

Relationships follow --rel-mode:

--rel-modeStatementPushing twice
keyed (default)MERGE on the type's key: --rel-key TYPE:PROP[,PROP], else the type's relationship uniqueness or key constraint; without either, one relationship of the type between the two nodesIdempotent
structuralMERGE (a)-[r:`TYPE`]->(b), even when a key exists; parallel relationships of one type between the same two nodes become oneIdempotent
createCREATE (a)-[r:`TYPE`]->(b)Creates them again

Update policies

--nodes-update and --rels-update decide what happens to an entity that already exists on the remote. Each takes a global policy (add), a per-entity list (Person=replace,Project=skip), or both, where a bare policy in the list is the default for every entity it does not name.

PolicyStatementOn the remote
add (default)SET n += row.propsPushed properties are set; properties only the remote has stay
replaceSET n = row.propsThe property map is replaced; properties only the remote has are removed
skipCreates missing entities onlyExisting entities are left as they are

--updated-at NAME[,NAME] names timestamp properties to compare, the first one present on each side winning. When both the local entity and the remote row carry one, a strictly newer remote row is skipped and counted as remote-newer, and an equal or newer local row is written. Temporal values and numbers compare; any other pair is incomparable, is written under the policy, and adds to the warn count.

The report

Push prints one row per entity, then a summary line. The skipped column adds up the four reasons: unkeyed, policy, remote-newer and dangling. warn counts the --updated-at pairs that could not be compared. A dry run also prints each entity's statement shape and ends with dry run: <n> entities, <m> rows planned; nothing was sent.

--json prints the same report as one JSON object:

KeyHolds
dryRuntrue for a dry run
entitiesOne object per entity: entity, kind (node or relationship), total, created, updated, skipped (unkeyed, policy, remoteNewer, dangling), incomparableWarnings, mergeKey, policy, statement, batches
statementsSent, rowsSent, bytesSent, elapsedMsThe totals of the run

In a dry run nothing is read from the remote, so created counts every row in scope.

Against a live remote

From the CLI specification, not from a run:

  • Before it writes an entity, push reads the remote's current rows for that entity by the same merge key, so the report can separate created from updated and --updated-at can compare timestamps.
  • Every value is sent as a typed parameter, so DATE, POINT, UUID and vector properties keep their Cypher type on the remote. A byte-array property fails the push: push failed: property "<key>": a byte array property ….
  • Rows go in batches of --batch-size (default 5000) per statement.
  • Nothing is deleted on the remote.
  • Pushing the same scope twice, under add or skip, with keyed or structural relationships, leaves the remote as one push did.
  • A failing batch stops the push, names the entity, the batch and the row range, prints the partial report of what landed, and exits 1.
  • The report does not compare the remote's constraints with the local ones: a local merge key that the remote does not enforce still merges, but the remote may already hold duplicates of it.

A failure before any write is a recorded run. With a uniqueness constraint on Person only, --labels Person, and NEO4J_URI=http://127.0.0.1:8873 in local.env, where nothing was listening, the pre-read fails, the report prints with nothing written, and the push exits 1. The timing and the text after io: vary by run and by operating system (this recording is from macOS):

relate push --to local.env --database ./team.rl --labels Person
entity                       total  created  updated  skipped  warn  merge key  policy  batches
Person                           3        0        0        0     0  name       add           0
(Person)-[:KNOWS]->(Person)      1        0        0        0     0             add           0
(dangling)                       4        0        0        4     0             skip          0
pushed 0 rows in 0 statements (0 bytes) in 1 ms
Error: push failed: pre-read of Person failed: HTTP request to http://127.0.0.1:8873/db/neo4j/query/v2 failed: io: Connection refused (os error 61)

Errors and exit codes

Exit codeWhen
0The push completed, or a dry run printed its plan
1A runtime failure: credentials missing or unreadable, a refused NEO4J_URI, plaintext refused, the store locked by another process, no answer from the remote, a remote error, a failing batch, or a value that cannot be pushed
2A usage error: --to or --database missing, :memory:, two selectors, an invalid value, a repeated single-value flag, an unknown flag, or --namespace. The message is followed by the usage line and Run 'relate push --help' for more information.

The usage messages, each on stderr after Error: :

MessageCause
push requires --toNo --to
push requires --databaseNo --database
push cannot read :memory: (--database must be an on-disk store)--database :memory:
push takes one of --query, --labels, or --types (none pushes the whole store)Two selectors
invalid --batch-size "<value>" (expected a number >= 1)--batch-size 0 or a value that is not a number
unknown update policy "<value>" (expected add, replace or skip)A bad --nodes-update or --rels-update value
unknown --rel-mode "<value>" (expected keyed, structural or create)A bad --rel-mode
unknown --unkeyed-labels "<value>" (expected skip or create)A bad --unkeyed-labels
--namespace is not valid with push (push always reads the neo4j namespace)--namespace before the push word
push <flag> may be given only onceA single-value flag repeated, such as two --to flags, or --dry-run, --json or --allow-plaintext given twice
--merge-key "<value>" must be NAME:PROP[,PROP] (no ':' found)A --merge-key value without a colon; --rel-key gives the same message with its own name
--merge-key "<value>" has an empty nameNothing before the colon; also from --rel-key
--merge-key "<value>" has an empty property nameNothing after the colon, or an empty item in its property list; also from --rel-key
--merge-key names "<label>" twiceTwo --merge-key flags for one label
--rel-key names "<type>" twiceTwo --rel-key flags for one type
--labels has an empty name in "<value>"An empty item in a --labels list, such as A,; --types gives the same message with its own name
--updated-at names "<name>" twiceOne property in two --updated-at flags
--updated-at "<value>" names "<name>" twiceOne property twice in one --updated-at list
--updated-at "<value>" has an empty property nameAn empty --updated-at value, or an empty item in its list
empty item in policy listAn empty --nodes-update or --rels-update value, or an empty item in its list
policy list has two global policies ("<policy>" twice)Two bare policies in one list, such as add,replace
empty entity name in policy item "<item>"A list item with nothing before the =
policy list names entity "<entity>" twiceOne entity named twice in one list
unexpected argument to push: <argument>An unknown flag, including --read-only and --namespace after the word

Examples

The setup writes the team graph (five nodes, five relationships) into ./team.rl and a credentials file that names a documentation host, neo4j.example.com; a dry run never contacts it.

printf 'NEO4J_URI=https://neo4j.example.com:7473\nNEO4J_USERNAME=neo4j\nNEO4J_PASSWORD=<password>\n' > neo4j.env
chmod 600 neo4j.env
relate --database ./team.rl "CREATE (ada:Person {name: 'Ada', role: 'engineer'}),
       (grace:Person {name: 'Grace', role: 'engineer'}),
       (noor:Person {name: 'Noor', role: 'maintainer'}),
       (relate:Project {name: 'relate', language: 'Rust'}),
       (tck:Project {name: 'opencypher-tck', language: 'Gherkin'}),
       (ada)-[:WORKS_ON {since: 2024}]->(relate),
       (grace)-[:WORKS_ON {since: 2023}]->(relate),
       (grace)-[:WORKS_ON {since: 2022}]->(tck),
       (noor)-[:MAINTAINS]->(relate),
       (ada)-[:KNOWS]->(grace)" > /dev/null

The store has no constraints yet, so no label has a merge key and the plan skips everything:

relate push --to neo4j.env --database ./team.rl --dry-run
entity                                      total  created  updated  skipped  warn  merge key  policy  batches
Person (unkeyed)                                3        0        0        3     0             add           0
Project (unkeyed)                               2        0        0        2     0             add           0
(Person)-[:KNOWS]->(Person) (unkeyed)           1        0        0        1     0             add           0
(Person)-[:MAINTAINS]->(Project) (unkeyed)      1        0        0        1     0             add           0
(Person)-[:WORKS_ON]->(Project) (unkeyed)       3        0        0        3     0             add           0
dry run: 5 entities, 10 rows planned; nothing was sent

The next two statements write a uniqueness constraint on each label into ./team.rl, which gives both labels a key. The second dry run plans every row and prints the statements:

relate --database ./team.rl "CREATE CONSTRAINT person_name IF NOT EXISTS FOR (p:Person) REQUIRE p.name IS UNIQUE" > /dev/null
relate --database ./team.rl "CREATE CONSTRAINT project_name IF NOT EXISTS FOR (p:Project) REQUIRE p.name IS UNIQUE" > /dev/null
relate push --to neo4j.env --database ./team.rl --dry-run
entity                            total  created  updated  skipped  warn  merge key  policy  batches
Person                                3        3        0        0     0  name       add           1
Project                               2        2        0        0     0  name       add           1
(Person)-[:KNOWS]->(Person)           1        1        0        0     0             add           1
(Person)-[:MAINTAINS]->(Project)      1        1        0        0     0             add           1
(Person)-[:WORKS_ON]->(Project)       3        3        0        0     0             add           1
Person: UNWIND $rows AS row MERGE (n:`Person` {`name`: row.key.`name`}) SET n += row.props
Project: UNWIND $rows AS row MERGE (n:`Project` {`name`: row.key.`name`}) SET n += row.props
(Person)-[:KNOWS]->(Person): UNWIND $rows AS row MATCH (a:`Person` {`name`: row.start.`name`}) MATCH (b:`Person` {`name`: row.end.`name`}) MERGE (a)-[r:`KNOWS`]->(b) SET r += row.props
(Person)-[:MAINTAINS]->(Project): UNWIND $rows AS row MATCH (a:`Person` {`name`: row.start.`name`}) MATCH (b:`Project` {`name`: row.end.`name`}) MERGE (a)-[r:`MAINTAINS`]->(b) SET r += row.props
(Person)-[:WORKS_ON]->(Project): UNWIND $rows AS row MATCH (a:`Person` {`name`: row.start.`name`}) MATCH (b:`Project` {`name`: row.end.`name`}) MERGE (a)-[r:`WORKS_ON`]->(b) SET r += row.props
dry run: 5 entities, 10 rows planned; nothing was sent

--labels Person keeps the people and the one relationship between two people; the four relationships that reach a project are counted as dangling:

relate push --to neo4j.env --database ./team.rl --dry-run --labels Person
entity                       total  created  updated  skipped  warn  merge key  policy  batches
Person                           3        3        0        0     0  name       add           1
(Person)-[:KNOWS]->(Person)      1        1        0        0     0             add           1
(dangling)                       4        0        0        4     0             skip          0
Person: UNWIND $rows AS row MERGE (n:`Person` {`name`: row.key.`name`}) SET n += row.props
(Person)-[:KNOWS]->(Person): UNWIND $rows AS row MATCH (a:`Person` {`name`: row.start.`name`}) MATCH (b:`Person` {`name`: row.end.`name`}) MERGE (a)-[r:`KNOWS`]->(b) SET r += row.props
dry run: 3 entities, 8 rows planned; nothing was sent

Two selectors are a usage error:

relate push --to neo4j.env --database ./team.rl --labels Person --types KNOWS
echo "exit=$?"
Error: push takes one of --query, --labels, or --types (none pushes the whole store)
Usage: relate push --to CREDS --database PATH [--query Q | --labels L1,L2 | --types T1,T2] [--batch-size N] [--nodes-update POLICY] [--rels-update POLICY] [--updated-at NAME[,NAME]] [--rel-mode MODE] [--merge-key LABEL:PROP[,PROP]] [--rel-key TYPE:PROP[,PROP]] [--unkeyed-labels ACTION] [--dry-run] [--json] [--allow-plaintext]
Run 'relate push --help' for more information.
exit=2

See also