Skip to content

Commit 59ef0d5

Browse files
committed
feat(tests): add OpenCypher DDL tests for index and constraint commands
1 parent 9f1a72a commit 59ef0d5

3 files changed

Lines changed: 216 additions & 0 deletions

File tree

bindings/python/docs/api/database.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,13 @@ db.command("sql", "CREATE DOCUMENT TYPE Person")
213213
db.command("sql", "CREATE PROPERTY Person.name STRING")
214214
db.command("sql", "CREATE PROPERTY Person.age INTEGER")
215215

216+
# OpenCypher DDL also passes straight through to the engine
217+
db.command("opencypher", "CREATE INDEX FOR (p:Person) ON (p.email)")
218+
db.command(
219+
"opencypher",
220+
"CREATE CONSTRAINT FOR (p:Person) REQUIRE p.email IS TYPED STRING",
221+
)
222+
216223
# Data operations must be in a transaction
217224
with db.transaction():
218225
db.command("sql", "INSERT INTO Person SET name = ?, age = ?", "Alice", 30)

bindings/python/tests/test_core.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,66 @@ def test_truncate_bucket(temp_db_path):
340340
assert db.count_type("BucketDoc") == 0
341341

342342

343+
def test_update_batch_clause(temp_db_path):
344+
"""UPDATE ... BATCH executes successfully through the Python SQL pass-through."""
345+
with arcadedb.create_database(temp_db_path) as db:
346+
db.command("sql", "CREATE DOCUMENT TYPE BatchUpdateDoc")
347+
348+
with db.transaction():
349+
for idx in range(25):
350+
db.command(
351+
"sql",
352+
"INSERT INTO BatchUpdateDoc SET idx = ?, processed = false",
353+
idx,
354+
)
355+
356+
with db.transaction():
357+
db.command(
358+
"sql",
359+
"UPDATE BatchUpdateDoc SET processed = true WHERE idx >= 10 BATCH 5",
360+
)
361+
362+
processed = db.query(
363+
"sql",
364+
"SELECT count(*) AS c FROM BatchUpdateDoc WHERE processed = true",
365+
).one()
366+
untouched = db.query(
367+
"sql",
368+
"SELECT count(*) AS c FROM BatchUpdateDoc WHERE processed = false",
369+
).one()
370+
371+
assert processed.get("c") == 15
372+
assert untouched.get("c") == 10
373+
374+
375+
def test_delete_batch_clause(temp_db_path):
376+
"""DELETE ... BATCH executes successfully through the Python SQL pass-through."""
377+
with arcadedb.create_database(temp_db_path) as db:
378+
db.command("sql", "CREATE DOCUMENT TYPE BatchDeleteDoc")
379+
380+
with db.transaction():
381+
for idx in range(30):
382+
db.command("sql", "INSERT INTO BatchDeleteDoc SET idx = ?", idx)
383+
384+
with db.transaction():
385+
db.command(
386+
"sql",
387+
"DELETE FROM BatchDeleteDoc WHERE idx % 2 = 0 BATCH 4",
388+
)
389+
390+
remaining = db.query(
391+
"sql",
392+
"SELECT count(*) AS c FROM BatchDeleteDoc",
393+
).one()
394+
odd_count = db.query(
395+
"sql",
396+
"SELECT count(*) AS c FROM BatchDeleteDoc WHERE idx % 2 = 1",
397+
).one()
398+
399+
assert remaining.get("c") == 15
400+
assert odd_count.get("c") == 15
401+
402+
343403
def test_transactions(temp_db_path):
344404
"""Test transaction support."""
345405
with arcadedb.create_database(temp_db_path) as db:

bindings/python/tests/test_cypher.py

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,155 @@ def test_opencypher_count_non_existing_label_returns_zero(temp_db_path):
389389
assert row.get("c") == 0
390390

391391

392+
def test_opencypher_create_index_command(temp_db_path):
393+
"""Test CREATE INDEX DDL passes through to the OpenCypher engine."""
394+
with arcadedb.create_database(temp_db_path) as db:
395+
_ensure_opencypher(db)
396+
397+
db.command("opencypher", "CREATE INDEX FOR (n:Event) ON (n.code)")
398+
399+
with db.transaction():
400+
db.command("opencypher", "CREATE (:Event {code: 'evt-1', name: 'Launch'})")
401+
402+
row = db.query(
403+
"opencypher",
404+
"MATCH (n:Event {code: 'evt-1'}) RETURN n.name AS name",
405+
).one()
406+
407+
assert row.get("name") == "Launch"
408+
409+
410+
def test_opencypher_typed_constraint_command(temp_db_path):
411+
"""Test typed Cypher constraints are accepted through the Python bindings."""
412+
with arcadedb.create_database(temp_db_path) as db:
413+
_ensure_opencypher(db)
414+
415+
db.command(
416+
"opencypher",
417+
"CREATE CONSTRAINT FOR (p:Person) REQUIRE p.email IS TYPED STRING",
418+
)
419+
420+
with db.transaction():
421+
db.command(
422+
"opencypher",
423+
"CREATE (:Person {email: 'alice@example.com', name: 'Alice'})",
424+
)
425+
426+
row = db.query(
427+
"opencypher",
428+
"MATCH (p:Person {email: 'alice@example.com'}) RETURN p.name AS name",
429+
).one()
430+
431+
assert row.get("name") == "Alice"
432+
433+
434+
def test_opencypher_create_index_if_not_exists_command(temp_db_path):
435+
"""Test CREATE INDEX IF NOT EXISTS is idempotent through the Python bindings."""
436+
with arcadedb.create_database(temp_db_path) as db:
437+
_ensure_opencypher(db)
438+
439+
db.command(
440+
"opencypher",
441+
"CREATE INDEX IF NOT EXISTS FOR (n:Metric) ON (n.code)",
442+
)
443+
db.command(
444+
"opencypher",
445+
"CREATE INDEX IF NOT EXISTS FOR (n:Metric) ON (n.code)",
446+
)
447+
448+
with db.transaction():
449+
db.command("opencypher", "CREATE (:Metric {code: 'm-1', value: 42})")
450+
451+
row = db.query(
452+
"opencypher",
453+
"MATCH (n:Metric {code: 'm-1'}) RETURN n.value AS value",
454+
).one()
455+
456+
assert row.get("value") == 42
457+
458+
459+
def test_opencypher_ddl_auto_creates_vertex_and_edge_types(temp_db_path):
460+
"""Test Cypher DDL auto-creates missing vertex and edge types via Python."""
461+
with arcadedb.create_database(temp_db_path) as db:
462+
_ensure_opencypher(db)
463+
464+
assert db.schema.exists_type("AutoEvent") is False
465+
assert db.schema.exists_type("RELATES_TO") is False
466+
467+
db.command("opencypher", "CREATE INDEX FOR (n:AutoEvent) ON (n.code)")
468+
db.command(
469+
"opencypher",
470+
"CREATE CONSTRAINT FOR ()-[r:RELATES_TO]-() REQUIRE r.since IS NOT NULL",
471+
)
472+
473+
assert db.schema.exists_type("AutoEvent") is True
474+
assert db.schema.exists_type("RELATES_TO") is True
475+
476+
with db.transaction():
477+
db.command("opencypher", "CREATE (:AutoEvent {code: 'a'})")
478+
db.command("opencypher", "CREATE (:AutoEvent {code: 'b'})")
479+
480+
source = db.query(
481+
"sql",
482+
"SELECT @rid AS rid FROM AutoEvent WHERE code = 'a'",
483+
).one()
484+
target = db.query(
485+
"sql",
486+
"SELECT @rid AS rid FROM AutoEvent WHERE code = 'b'",
487+
).one()
488+
489+
with db.transaction():
490+
db.command(
491+
"sql",
492+
f"CREATE EDGE RELATES_TO FROM {source.get('rid')} TO {target.get('rid')} "
493+
"SET since = '2026-04-07'",
494+
)
495+
496+
row = db.query(
497+
"opencypher",
498+
"MATCH (:AutoEvent {code: 'a'})-[r:RELATES_TO]->(:AutoEvent {code: 'b'}) "
499+
"RETURN r.since AS since",
500+
).one()
501+
502+
assert row.get("since") == "2026-04-07"
503+
504+
505+
def test_opencypher_edge_typed_constraint_command(temp_db_path):
506+
"""Test typed edge constraints are accepted through the Python bindings."""
507+
with arcadedb.create_database(temp_db_path) as db:
508+
_ensure_opencypher(db)
509+
510+
db.command("sql", "CREATE VERTEX TYPE Person")
511+
db.command(
512+
"opencypher",
513+
"CREATE CONSTRAINT FOR ()-[r:KNOWS]-() REQUIRE r.since IS TYPED DATE",
514+
)
515+
516+
with db.transaction():
517+
db.command("sql", "INSERT INTO Person SET name = 'Alice'")
518+
db.command("sql", "INSERT INTO Person SET name = 'Bob'")
519+
520+
alice = db.query(
521+
"sql",
522+
"SELECT @rid AS rid FROM Person WHERE name = 'Alice'",
523+
).one()
524+
bob = db.query(
525+
"sql",
526+
"SELECT @rid AS rid FROM Person WHERE name = 'Bob'",
527+
).one()
528+
529+
with db.transaction():
530+
db.command(
531+
"sql",
532+
f"CREATE EDGE KNOWS FROM {alice.get('rid')} TO {bob.get('rid')} "
533+
"SET since = date('2026-04-07')",
534+
)
535+
536+
row = db.query("sql", "SELECT since FROM KNOWS").one()
537+
538+
assert row.get("since") is not None
539+
540+
392541
def test_opencypher_projection_property_order_is_preserved(temp_db_path):
393542
"""Test projected property order is stable and matches RETURN clause order."""
394543
with arcadedb.create_database(temp_db_path) as db:

0 commit comments

Comments
 (0)