Composite databases¶
A composite database is a single query endpoint over several constituent databases. Applications connect to the composite and address the parts through it:
Constituents are reachable only from a session on the composite
Running USE cineasts.latest while connected to some other database is
refused:
42N04: Failed to access database identified by `cineasts.latest` while
connected to session database `neo4j`.
Connect to `cineasts.latest` directly.
Ignore that last sentence — connecting to the constituent by its
qualified name does not work either (22N51: graph reference not found).
Connect to the composite (-d cineasts, or your driver's database
parameter), then USE the constituent from there.
From a composite session, RETURN graph.names() lists what is reachable.
It stores no data of its own. No topology, no store, no indexes or constraints, no seeding — the constituent databases hold everything, and the composite is the way in.
Runnable manifests for everything below are in
examples/composite-databases/.
Creating one¶
apiVersion: neo4j.neo4j.com/v1beta1
kind: Neo4jCompositeDatabase
metadata:
name: cineasts
namespace: neo4j
spec:
clusterRef: prod-cluster
constituents:
- name: latest
targetDatabase: movies-latest
- name: upcoming
targetDatabase: movies-upcoming
Each constituent becomes an alias cineasts.<name>. The target databases do
not have to exist yet — a constituent whose target is missing is skipped and
retried, so you can apply a composite and its databases together.
$ kubectl get neo4jcompositedatabase cineasts -n neo4j
NAME CLUSTER PHASE READY AGE
cineasts prod-cluster Ready True 30s
status.observedConstituents is read back from the server, so it tells you
what the DBMS actually exposes rather than repeating your spec.
Constituents are managed with the composite¶
They are declared inline rather than as separate CRs, for a reason worth knowing about.
The composite must exist before any constituent
Neo4j accepts CREATE ALIAS cineasts.latest even when no composite
cineasts exists. You get an ordinary alias whose name happens to
contain a dot — SHOW ALIASES lists it, and it is even queryable. But the
composite can then never be created:
42N87: The database or alias name `cineasts` conflicts with the name
`cineasts.latest` of an existing database or alias.
The error never mentions ordering, and the only way out is dropping the
alias. Because this CR owns the composite and its constituents together,
the ordering is guaranteed. If a stray dotted alias is already in the way,
the CR reports Failed with reason CompositeDatabaseNameBlocked and
names the alias to drop.
By default spec.constituents is authoritative: an alias in the composite's
namespace that is not in spec gets removed. Set enforceConstituents: false
to leave out-of-band constituents alone, in which case the operator only ever
adds.
An alias that only looks like a constituent is left alone
Neo4j distinguishes a constituent from an ordinary alias by a composite
column, not by the name — so cineasts.stray, created out of band with no
composite behind it, is not a constituent of cineasts however much it
reads like one. enforceConstituents does not remove it: nothing in this
CR claims it, and deleting a resource on the strength of a name collision
would be worse than leaving it.
It is named in status.message instead, because SHOW ALIASES shows only
the name and the two are otherwise indistinguishable. To see which is
which: SHOW ALIASES FOR DATABASE YIELD name, composite.
Quoting a constituent by hand depends on the server's Cypher language
The operator pins its own composite and alias statements to a language, so
you never see this unless you run DDL yourself. If you do, the backticked
forms mean different things. In Cypher 5, CREATE ALIAS `cineasts`.`latest` ...
makes a constituent and CREATE ALIAS `cineasts.latest` ... makes a plain
alias whose name contains a dot. In Cypher 25 the first is refused (42NAA,
separate name parts should not be quoted individually) and the second is
the constituent. An unquoted cineasts.latest is a constituent in both. A
new CalVer deployment defaults to Cypher 25 (spec.serverDefaultCypherLanguage),
so statements written against Cypher 5 can fail there; prefix a statement
with CYPHER 5 to get the Cypher 5 meaning on any supported version. The
dotted-alias trap below exists in both languages.
Do not use Neo4jDatabaseAlias for a constituent
Neo4jDatabaseAlias manages ordinary aliases, and
creating a dotted one through it is how you reach the 42N87 above — it
cannot know a composite is coming. Constituents belong on the composite
that owns them. If you already have a dotted alias from that route,
kubectl neo4j explain CompositeDatabaseNameBlocked walks through
clearing it.
Privileges go on the constituents, never on the composite¶
This is the mistake to watch for, and Neo4j gives you no warning about it:
Graph privileges attach to the constituent target databases. A user reading through the composite needs:
ACCESSon the composite,ACCESSon each constituent's target database,- the graph privileges (
MATCH,TRAVERSE, …) on those target databases.
apiVersion: neo4j.neo4j.com/v1beta1
kind: Neo4jRole
spec:
clusterRef: prod-cluster
name: analyst
privileges:
- "GRANT ACCESS ON DATABASE cineasts TO analyst" # the composite
- "GRANT ACCESS ON DATABASE `movies-latest` TO analyst" # the constituent
- "GRANT MATCH {*} ON GRAPH `movies-latest` NODES * TO analyst"
A Neo4jRole whose privileges name a composite as a graph reports
PrivilegesResolve=False with reason GraphPrivilegeOnComposite, because that
privilege will silently do nothing.
Stopping a composite¶
STOP DATABASE cineasts takes the endpoint offline. The constituent
databases stay online — only the way in closes, and queries through the
composite fail with "database unavailable". Starting it restores access. So
stopping a composite is an access-layer switch, not a data operation.
Deleting¶
deletionPolicy: Delete (the default) issues DROP COMPOSITE DATABASE
cineasts CASCADE ALIASES. CASCADE ALIASES is required — a plain drop is
refused while constituents exist — and it removes only the constituent
aliases. The databases they target are untouched.
Use deletionPolicy: Retain to keep the composite in Neo4j when the CR goes
away.
Remote constituents¶
A constituent can point at a database in another Neo4j DBMS. There are two ways to authenticate, and they cost very different things to run.
OIDC credential forwarding — prefer this¶
The querying user's own token is forwarded to the remote DBMS. Nothing is stored, so there is no credential to leak, rotate or encrypt — and no keystore is needed.
constituents:
- name: partner
targetDatabase: movies
remote:
url: neo4j+s://partner.example.com:7687
oidcCredentialForwarding: true
Both DBMSs must trust the same identity provider, and it needs Cypher 25, so a CalVer image. On the 5.26 LTS the clause does not parse and the CR is rejected with a message saying so.
Stored native credentials¶
A username and password are stored on the alias. Neo4j encrypts them in the
system database, which is why this mode needs a keystore on the deployment.
This mode works on both the 5.26 LTS and CalVer. Neo4j requires a
secure scheme for any remote alias: neo4j+s://, or neo4j+ssc:// to accept
a self-signed certificate.
# on the deployment
spec:
remoteAliasKeystore:
secretRef: remote-alias-keystore # keys: keystore.p12, password
keyName: my-key
---
# on the composite
constituents:
- name: partner
targetDatabase: movies
remote:
url: neo4j+s://partner.example.com:7687
credentialsSecretRef: partner-creds # keys: username, password
Create the keystore with the JDK's keytool — ideally on the same Java version Neo4j runs, as Neo4j advises:
keytool -genseckey -keyalg aes -keysize 256 -storetype pkcs12 \
-keystore keystore.p12 -alias my-key -storepass "$PASSWORD"
kubectl create secret generic remote-alias-keystore \
--from-file=keystore.p12 --from-literal=password="$PASSWORD"
Every server mounts the same Secret, which is what satisfies Neo4j's requirement that the keystore be identical across a cluster.
Rotating the keystore invalidates every existing remote alias
Neo4j encrypts each alias's credentials with this key. Changing the keystore or the key name makes all existing remote-alias credentials permanently unreadable, and the aliases must be recreated. Treat rotation as delete-and-recreate, not an edit.
Where the password does and does not go
The operator passes it to Neo4j as a Cypher parameter, never
interpolated into the statement, so it does not reach the query log. Neo4j
stores it encrypted, and SHOW ALIASES never returns a password on any
alias — so alias output is safe to include in a support bundle.
Forget the keystore and the CR goes to Failed on its first reconcile
(the operator has no admission webhook, so kubectl apply itself succeeds)
with a message naming the field to set, instead of failing inside the
server with an internal error that mentions neither the CR nor the
constituent.
Limits¶
| Remote constituents need setup | OIDC forwarding needs Cypher 25 (CalVer). Stored credentials need spec.remoteAliasKeystore on the deployment. See Remote constituents. |
| No options | Composites have none. The operator never emits an OPTIONS clause. |
| No topology | There is no store to place. |
defaultCypherLanguage needs CalVer for "25" |
The DEFAULT LANGUAGE CYPHER clause does not parse on the 5.26 LTS, so "25" is rejected there. "5" is accepted with a warning and the clause is left out, because every database on the LTS already runs Cypher 5. It is also the only property of a composite that can be changed after creation. |
| Writes are single-graph | A transaction may read across constituents but write to only one. |
| No nesting | A composite cannot contain another composite. |