@@ -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+
392541def 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