{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Propagation Properties — Full Neo4j Demo\n", "\n", "This notebook demonstrates the complete workflow for working with\n", "propagation properties (`_propagate`, `is_weak`, `parent_relation`,\n", "`_weak_init_done`) on a real Neo4j database.\n", "\n", "**Steps:**\n", "1. Connect to Neo4j DEV and load the Game of Thrones dataset\n", "2. Generate YAML schema + Python entity classes\n", "3. Initialize propagation properties on existing data\n", "4. Demonstrate `create_group()` — atomic group creation\n", "5. Run sample queries to verify everything works\n", "\n", "## Prerequisites\n", "\n", "- A running Neo4j instance (DEV target)\n", "- Environment variables: `NEO4J_DEV_URL`, `NEO4J_DEV_USER`, `NEO4J_DEV_PASSWORD`\n", "- Default connection: `bolt://localhost:7687` / `neo4j` / `secret`\n", "\n", "## Connection configuration\n", "\n", "The notebook reads connection parameters from environment variables\n", "with sensible defaults. Set these before running:\n", "\n", "```bash\n", "export NEO4J_DEV_URL=bolt://your-host:7687\n", "export NEO4J_DEV_USER=neo4j\n", "export NEO4J_DEV_PASSWORD=your-password\n", "```" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "✅ cvcdocdb already installed. Skipping.\n" ] } ], "source": [ "import importlib.util\n", "\n", "# Check if cvcdocdb is installed\n", "package_to_check = 'cvcdocdb'\n", "spec = importlib.util.find_spec(package_to_check)\n", "\n", "if spec is None:\n", " print(f'⚠️ {package_to_check} not installed. Installing...')\n", " %pip install -q --upgrade cvcdocdb-tools\n", " print(\"✅ Installation complete.\")\n", "else:\n", " print(f'✅ {package_to_check} already installed. Skipping.')" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "✅ Loaded .env from /Users/oriol/Desenvolupament/cvcdocdb-tools/.env\n", " Found 5 variables\n", "✅ Added /Users/oriol/Desenvolupament/cvcdocdb-tools/cvcdocdb to sys.path\n", "✅ All imports successful.\n" ] } ], "source": [ "import os\n", "import sys\n", "from pathlib import Path\n", "\n", "# Read .env file manually (no python-dotenv dependency)\n", "def read_env(env_path):\n", " \"\"\"Read a .env file and return a dict of key=value pairs.\"\"\"\n", " env_vars = {}\n", " with open(env_path, 'r') as f:\n", " for line in f:\n", " line = line.strip()\n", " if not line or line.startswith('#'):\n", " continue\n", " if '=' in line:\n", " key, value = line.split('=', 1)\n", " env_vars[key.strip()] = value.strip()\n", " return env_vars\n", "\n", "# Find .env in repo root\n", "_env_path = Path.cwd() / \".env\"\n", "if not _env_path.exists():\n", " for parent in Path.cwd().parents:\n", " candidate = parent / \".env\"\n", " if candidate.exists():\n", " _env_path = candidate\n", " break\n", "\n", "if _env_path.exists():\n", " _env_vars = read_env(_env_path)\n", " for key, value in _env_vars.items():\n", " os.environ.setdefault(key, value)\n", " print(f\"✅ Loaded .env from {_env_path}\")\n", " print(f\" Found {len(_env_vars)} variables\")\n", "else:\n", " print(\"⚠️ .env file not found. Set NEO4J_DEV_PASSWORD environment variable manually.\")\n", "\n", "# Add cvcdocdb package to path for development\n", "_repo_root = Path.cwd()\n", "if not (_repo_root / \"cvcdocdb\").exists():\n", " for parent in Path.cwd().parents:\n", " if (parent / \"cvcdocdb\").exists():\n", " _repo_root = parent\n", " break\n", "_cvcdocdb_path = str(_repo_root / \"cvcdocdb\")\n", "if _cvcdocdb_path not in sys.path:\n", " sys.path.insert(0, _cvcdocdb_path)\n", " print(f\"✅ Added {_cvcdocdb_path} to sys.path\")\n", "\n", "import json\n", "\n", "from cvcdocdb import Neo4jGraph, NetworkXGraph\n", "from cvcdocdb.exemples import load_got_characters\n", "from cvcdocdb.schema_gen import generate_classes\n", "\n", "\n", "def neo4j_config(default_target=\"DEV\"):\n", " \"\"\"Build Neo4j connection config from environment variables.\"\"\"\n", " target = os.environ.get(\"NEO4J_TARGET\", default_target).upper()\n", " prefix = f\"NEO4J_{target}_\"\n", " return {\n", " \"target\": target,\n", " \"url\": os.environ.get(f\"{prefix}URL\", os.environ.get(\"NEO4J_URL\", \"bolt://localhost:7687\")),\n", " \"user\": os.environ.get(f\"{prefix}USER\", os.environ.get(\"NEO4J_USER\", \"neo4j\")),\n", " \"password\": os.environ.get(f\"{prefix}PASSWORD\", os.environ.get(\"NEO4J_PASSWORD\", \"secret\")),\n", " \"database\": os.environ.get(f\"{prefix}DATABASE\", os.environ.get(\"NEO4J_DATABASE\", \"neo4j\")),\n", " }\n", "\n", "\n", "def section(title):\n", " \"\"\"Print a section header.\"\"\"\n", " print(f\"\\n{'=' * 60}\")\n", " print(f\" {title}\")\n", " print(f\"{'=' * 60}\")\n", "\n", "\n", "def sub_section(title):\n", " \"\"\"Print a subsection header.\"\"\"\n", " print(f\"\\n--- {title} ---\")\n", "\n", "\n", "# Resolve paths relative to repo root\n", "def repo_path(rel):\n", " \"\"\"Return a Path relative to the repository root.\"\"\"\n", " return _repo_root / rel\n", "\n", "print(\"✅ All imports successful.\")\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Step 1: Connect to Neo4j\n", "\n", "Connect to the DEV Neo4j instance and verify the connection." ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Target: DEV\n", "URL: bolt://localhost:7687\n", "User: neo4j\n", "Database: neo4j\n", "\n", "✅ Connection verified.\n", "Nodes in database: 57\n" ] } ], "source": [ "cfg = neo4j_config()\n", "print(f\"Target: {cfg['target']}\")\n", "print(f\"URL: {cfg['url']}\")\n", "print(f\"User: {cfg['user']}\")\n", "print(f\"Database: {cfg['database']}\")\n", "\n", "graph = Neo4jGraph(\n", " url=cfg[\"url\"],\n", " user=cfg[\"user\"],\n", " password=cfg[\"password\"],\n", " database=cfg[\"database\"],\n", ")\n", "\n", "# Verify connection\n", "result = graph.query(\"RETURN 1 AS test\")\n", "if result and result[0].get(\"test\") == 1:\n", " print(\"\\n✅ Connection verified.\")\n", "else:\n", " print(\"\\n⚠️ Connection returned unexpected result.\")\n", "\n", "# Check current state\n", "count_result = graph.query(\"MATCH (n) RETURN count(n) AS total\")\n", "total = count_result[0][\"total\"] if count_result else 0\n", "print(f\"Nodes in database: {total}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Step 2: Load Game of Thrones dataset\n", "\n", "Load character and house data from [thronesapi.com](https://thronesapi.com).\n", "Falls back to bundled data if the API is unavailable." ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Loaded: {\n", " \"characters\": 25,\n", " \"houses\": 15,\n", " \"member_of\": 25\n", "}\n", "\n", "--- Nodes by label ---\n", " Character: 25\n", " House: 15\n", " Valor: 5\n", " IndividuPadro: 3\n", " LlocPadro: 2\n", " NodeA: 1\n", " NodeB: 1\n", " TestNode: 1\n", " TransientNode: 1\n", " TEST: 1\n", " Document: 1\n", " Page: 1\n", "\n", "--- Relations by type ---\n", " MEMBER_OF: 25\n", " COGNOM1: 3\n", " NOM: 3\n", " CONNECTS_TO: 1\n", " HAS_PAGE: 1\n" ] } ], "source": [ "stats = load_got_characters(graph, limit=25)\n", "print(\"Loaded:\", json.dumps(stats, indent=2))\n", "\n", "# Show nodes by label\n", "sub_section(\"Nodes by label\")\n", "result = graph.query(\n", " \"MATCH (n) RETURN labels(n)[0] AS label, count(n) AS cnt \"\n", " \"ORDER BY cnt DESC\"\n", ")\n", "for row in result:\n", " print(f\" {row['label']}: {row['cnt']}\")\n", "\n", "# Show relations by type\n", "sub_section(\"Relations by type\")\n", "result = graph.query(\n", " \"MATCH ()-[r]->() RETURN type(r) AS rel_type, count(r) AS cnt \"\n", " \"ORDER BY cnt DESC\"\n", ")\n", "for row in result:\n", " print(f\" {row['rel_type']}: {row['cnt']}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Step 3: Generate YAML schema\n", "\n", "Introspect the Neo4j database and generate a YAML schema description.\n", "This captures labels, properties, relationships, and counts." ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Written: /Users/oriol/Desenvolupament/cvcdocdb-tools/cvcdocdb/exemples/generated/got_schema.yaml\n", "Size: 3845 bytes\n", "\n", "Schema preview:\n", " # Schema generated from: got\n", " # Generated at: 2026-07-12\n", " \n", " labels:\n", " Character:\n", " class_name: Character\n", " base_class: Node\n", " properties:\n", " character_id: integer\n", " name: string\n", " source: string\n", " title: string\n", " primary_key: ['name', 'character_id']\n", " count: 25\n", " Document:\n", " class_name: Document\n", " base_class: Node\n", " properties:\n", " _weak_init_done: boolean\n", " doc_id: string\n", " ...\n" ] } ], "source": [ "output_dir = repo_path(\"cvcdocdb/exemples/generated\")\n", "output_dir.mkdir(exist_ok=True)\n", "\n", "yaml_content = graph.schema_yaml(db_name=\"got\")\n", "yaml_path = output_dir / \"got_schema.yaml\"\n", "yaml_path.write_text(yaml_content)\n", "\n", "print(f\"Written: {yaml_path}\")\n", "print(f\"Size: {len(yaml_content)} bytes\")\n", "\n", "print(\"\\nSchema preview:\")\n", "for line in yaml_content.split(\"\\n\")[:20]:\n", " print(f\" {line}\")\n", "print(\" ...\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Step 4: Generate Python entity classes\n", "\n", "Generate Python classes from the YAML schema. This creates `Node`,\n", "`WeakNode`, `Relation`, and `WeakRelation` subclasses for each\n", "label and relationship type in the database." ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Written: /Users/oriol/Desenvolupament/cvcdocdb-tools/cvcdocdb/exemples/generated/got_entities.py\n", "Size: 11259 bytes\n", "\n", "Generated code preview:\n", " \"\"\"Auto-generated entity classes from schema.\"\"\"\n", " \n", " from __future__ import annotations\n", " \n", " from typing import Any, Dict, List, Optional\n", " \n", " from cvcdocdb.base import Node, WeakNode, Relation, WeakRelation\n", " \n", " \n", " class Character(Node):\n", " \"\"\"Auto-generated entity class..\"\"\"\n", " \n", " def __init__(self, pk: Dict[str, Any], **kwargs: Any) -> None:\n", " \"\"\"Initialize a Character node.\n", " \n", " Args:\n", " pk: Primary key dict with fields: name, character_id.\n", " **kwargs: Additional properties and attributes.\n", " \"\"\"\n", " super().__init__(pk=pk, main_label=\"Character\", **kwargs)\n", " self.character_id = kwargs.get(\"character_id\", 'integer')\n", " self.name = kwargs.get(\"name\", 'string')\n", " self.source = kwargs.get(\"source\", 'string')\n", " self.title = kwargs.get(\"title\", 'string')\n", " class Document(Node):\n", " ...\n" ] } ], "source": [ "# Read the YAML file content (generate_classes expects YAML string, not file path)\n", "yaml_content = yaml_path.read_text()\n", "code = generate_classes(yaml_content)\n", "\n", "py_path = output_dir / \"got_entities.py\"\n", "py_path.write_text(code)\n", "\n", "print(f\"Written: {py_path}\")\n", "print(f\"Size: {len(code)} bytes\")\n", "\n", "print(\"\\nGenerated code preview:\")\n", "for line in code.split(\"\\n\")[:25]:\n", " print(f\" {line}\")\n", "print(\" ...\")\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Step 5: Initialize propagation properties\n", "\n", "Scan the entire Neo4j graph and initialize propagation properties:\n", "- `is_weak`: marks nodes that are children of `_propagate` edges\n", "- `_propagate`: marks nodes that have cascade-delete enabled\n", "- `parent_relation`: stores the edge type linking parent → child\n", "- `_weak_init_done`: marks strong nodes whose weak children are initialized\n", "\n", "This uses `init_propagation()` which:\n", "1. Finds all edges with `_propagate = TRUE`\n", "2. Marks the child nodes as `is_weak = TRUE`\n", "3. Skips nodes already marked via `_weak_init_done`" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "--- Before init_propagation() ---\n", " Total nodes: 57\n", " Nodes with is_weak: 1\n", " Nodes with _weak_init_done: 17\n", "\n", "Running init_propagation()...\n", "Result: True\n", "\n", "--- After init_propagation() ---\n", " Total nodes: 57\n", " Nodes with is_weak: 1\n", " Nodes with _weak_init_done: 57\n", " Edges with _propagate: 1\n" ] } ], "source": [ "# Check state before init\n", "sub_section(\"Before init_propagation()\")\n", "result = graph.query(\n", " \"MATCH (n) RETURN count(n) AS total, \"\n", " \"count(n.is_weak) AS weak_count, \"\n", " \"count(n._weak_init_done) AS done_count\"\n", ")\n", "row = result[0] if result else {}\n", "print(f\" Total nodes: {row.get('total', 0)}\")\n", "print(f\" Nodes with is_weak: {row.get('weak_count', 0)}\")\n", "print(f\" Nodes with _weak_init_done: {row.get('done_count', 0)}\")\n", "\n", "# Run initialization\n", "print(\"\\nRunning init_propagation()...\")\n", "init_result = graph.init_propagation()\n", "print(f\"Result: {init_result}\")\n", "\n", "# Check state after init\n", "sub_section(\"After init_propagation()\")\n", "result = graph.query(\n", " \"MATCH (n) RETURN count(n) AS total, \"\n", " \"count(n.is_weak) AS weak_count, \"\n", " \"count(n._weak_init_done) AS done_count\"\n", ")\n", "row = result[0] if result else {}\n", "print(f\" Total nodes: {row.get('total', 0)}\")\n", "print(f\" Nodes with is_weak: {row.get('weak_count', 0)}\")\n", "print(f\" Nodes with _weak_init_done: {row.get('done_count', 0)}\")\n", "\n", "# Show propagation edges\n", "result = graph.query(\n", " \"MATCH ()-[r]->() WHERE r._propagate = TRUE RETURN count(r) AS c\"\n", ")\n", "prop_count = result[0][\"c\"] if result else 0\n", "print(f\" Edges with _propagate: {prop_count}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Step 6: Transactional group creation\n", "\n", "Use `create_group()` to atomically create a strong node with its\n", "WeakNodes and WeakRelations. The entire group is created in a single\n", "Neo4j transaction — if any step fails, everything is rolled back.\n", "\n", "We create a Document → Section → Page hierarchy." ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Creating group:\n", " Strong node: Document(GOT-DEMO-001)\n", " Weak nodes: Section(intro), Section(characters)\n", " Weak nodes: Page(1) → intro, Page(1) → characters\n" ] }, { "ename": "AttributeError", "evalue": "'Session' object has no attribute 'write_transaction'", "output_type": "error", "traceback": [ "\u001b[31m---------------------------------------------------------------------------\u001b[39m", "\u001b[31mAttributeError\u001b[39m Traceback (most recent call last)", "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[8]\u001b[39m\u001b[32m, line 54\u001b[39m\n\u001b[32m 50\u001b[39m print(\u001b[33mf\" Weak nodes: Section(intro), Section(characters)\"\u001b[39m)\n\u001b[32m 51\u001b[39m print(\u001b[33mf\" Weak nodes: Page(1) → intro, Page(1) → characters\"\u001b[39m)\n\u001b[32m 52\u001b[39m \n\u001b[32m 53\u001b[39m \u001b[38;5;66;03m# Create atomically\u001b[39;00m\n\u001b[32m---> \u001b[39m\u001b[32m54\u001b[39m doc_id = graph.create_group(\n\u001b[32m 55\u001b[39m strong_node=doc,\n\u001b[32m 56\u001b[39m weak_nodes=[section1, section2, page1, page2],\n\u001b[32m 57\u001b[39m )\n", "\u001b[36mFile \u001b[39m\u001b[32m~/opt/anaconda3/envs/cvcdocdb-env/lib/python3.11/site-packages/cvcdocdb/neo4j_graph.py:700\u001b[39m, in \u001b[36mNeo4jGraph.create_group\u001b[39m\u001b[34m(self, strong_node, weak_nodes, weak_relations, **kwargs)\u001b[39m\n\u001b[32m 697\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m result\n\u001b[32m 699\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[32m--> \u001b[39m\u001b[32m700\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[30;43msession\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43mwrite_transaction\u001b[39;49m(_create_group_tx)\n\u001b[32m 701\u001b[39m \u001b[38;5;28;01mexcept\u001b[39;00m ConstraintError \u001b[38;5;28;01mas\u001b[39;00m err:\n\u001b[32m 702\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mRuntimeError\u001b[39;00m(\u001b[33m\"\u001b[39m\u001b[33mDuplicate key: \u001b[39m\u001b[33m\"\u001b[39m + err.message) \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01merr\u001b[39;00m\n", "\u001b[31mAttributeError\u001b[39m: 'Session' object has no attribute 'write_transaction'" ] } ], "source": [ "from cvcdocdb.base import Node, WeakNode\n", "\n", "# Create the strong node (Document)\n", "doc = Node(\n", " pk={\"doc_id\": \"GOT-DEMO-001\"},\n", " main_label=\"Document\",\n", " title=\"Game of Thrones — Episode Guide\",\n", " author=\"Demo Script\",\n", ")\n", "\n", "# Create weak nodes (Sections and Pages)\n", "section1 = WeakNode(\n", " parent=doc,\n", " parent_relation=\"HAS_SECTION\",\n", " pk={\"section\": \"intro\"},\n", " main_label=\"Section\",\n", " title=\"Introduction\",\n", " order=1,\n", ")\n", "\n", "section2 = WeakNode(\n", " parent=doc,\n", " parent_relation=\"HAS_SECTION\",\n", " pk={\"section\": \"characters\"},\n", " main_label=\"Section\",\n", " title=\"Main Characters\",\n", " order=2,\n", ")\n", "\n", "page1 = WeakNode(\n", " parent=section1,\n", " parent_relation=\"HAS_PAGE\",\n", " pk={\"page\": 1},\n", " main_label=\"Page\",\n", " content=\"Welcome to the Game of Thrones episode guide.\",\n", " order=1,\n", ")\n", "\n", "page2 = WeakNode(\n", " parent=section2,\n", " parent_relation=\"HAS_PAGE\",\n", " pk={\"page\": 1},\n", " main_label=\"Page\",\n", " content=\"Jon Snow, Daenerys Targaryen, Tyrion Lannister...\",\n", " order=1,\n", ")\n", "\n", "print(\"Creating group:\")\n", "print(f\" Strong node: Document(GOT-DEMO-001)\")\n", "print(f\" Weak nodes: Section(intro), Section(characters)\")\n", "print(f\" Weak nodes: Page(1) → intro, Page(1) → characters\")\n", "\n", "# Create atomically\n", "doc_id = graph.create_group(\n", " strong_node=doc,\n", " weak_nodes=[section1, section2, page1, page2],\n", ")\n", "\n", "print(f\"\\n✅ Group created (strong node id: {doc_id})\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Step 7: Verify the group\n", "\n", "Query the database to verify the group was created correctly and\n", "check the propagation properties." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Verify Document node\n", "sub_section(\"Document node\")\n", "result = graph.query(\n", " \"MATCH (n) WHERE n.doc_id = 'GOT-DEMO-001' \"\n", " \"RETURN labels(n) AS labels, n.title, n._weak_init_done\"\n", ")\n", "for row in result:\n", " print(f\" Labels: {row['labels']}\")\n", " print(f\" Title: {row['title']}\")\n", " print(f\" _weak_init_done: {row.get('_weak_init_done')}\")\n", "\n", "# Verify Section nodes\n", "sub_section(\"Section nodes\")\n", "result = graph.query(\n", " \"MATCH (n:Section) WHERE n._weak_init_done IS NULL \"\n", " \"RETURN labels(n) AS labels, n.title, n.is_weak, n._propagate\"\n", ")\n", "for row in result:\n", " print(f\" {row['title']}: is_weak={row.get('is_weak')}, _propagate={row.get('_propagate')}\")\n", "\n", "# Verify Page nodes\n", "sub_section(\"Page nodes\")\n", "result = graph.query(\n", " \"MATCH (n:Page) WHERE n._weak_init_done IS NULL \"\n", " \"RETURN labels(n) AS labels, n.content, n.is_weak, n._propagate\"\n", ")\n", "for row in result:\n", " content = (row['content'] or '')[:50]\n", " print(f\" Page: '{content}...' is_weak={row.get('is_weak')}, _propagate={row.get('_propagate')}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Step 8: Sample queries\n", "\n", "Run several queries to demonstrate the initialized graph and\n", "propagation properties." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Query 1: Characters by House\n", "sub_section(\"Characters by House\")\n", "result = graph.query(\n", " \"MATCH (c:Character)-[:MEMBER_OF]->(h:House) \"\n", " \"RETURN h.name AS house, collect(c.name) AS characters \"\n", " \"ORDER BY house\"\n", ")\n", "for row in result:\n", " chars = row['characters'][:5]\n", " print(f\" {row['house']}: {', '.join(chars)}\")\n", " if len(row['characters']) > 5:\n", " print(f\" ... and {len(row['characters']) - 5} more\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Query 2: Nodes with propagation properties\n", "sub_section(\"Nodes with propagation properties\")\n", "result = graph.query(\n", " \"MATCH (n) WHERE n.is_weak = TRUE \"\n", " \"RETURN labels(n) AS label, count(n) AS count \"\n", " \"ORDER BY count DESC\"\n", ")\n", "for row in result:\n", " print(f\" {row['label']}: {row['count']} nodes\")\n", "\n", "# Query 3: Edges with _propagate\n", "sub_section(\"Edges with _propagate\")\n", "result = graph.query(\n", " \"MATCH (a)-[r]->(b) WHERE r._propagate = TRUE \"\n", " \"RETURN type(a) AS from_label, type(r) AS rel_type, \"\n", " \"type(b) AS to_label, count(*) AS count \"\n", " \"ORDER BY count DESC\"\n", ")\n", "for row in result:\n", " print(f\" {row['from_label']} -[{row['rel_type']}]-> {row['to_label']}: {row['count']}\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Query 4: Document group hierarchy\n", "sub_section(\"Document group hierarchy\")\n", "result = graph.query(\n", " \"MATCH path = (d:Document {doc_id: 'GOT-DEMO-001'})-[*1..3]->(n) \"\n", " \"RETURN path\"\n", ")\n", "if result:\n", " for row in result:\n", " path = row.get(\"path\")\n", " if path:\n", " nodes = path.nodes\n", " rels = path.relationships\n", " parts = []\n", " for i, node in enumerate(nodes):\n", " label = list(node.labels)[0] if hasattr(node, 'labels') else 'Node'\n", " props = dict(node)\n", " name = props.get('title', props.get('name', str(node.element_id)))\n", " parts.append(f\"{label}({name})\")\n", " for rel in rels:\n", " parts.append(f\"-[{rel.type}]->\")\n", " print(f\" {' → '.join(parts)}\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Query 5: Summary statistics\n", "sub_section(\"Summary statistics\")\n", "\n", "result = graph.query(\n", " \"MATCH (n) \"\n", " \"RETURN count(n) AS total_nodes, \"\n", " \"count(n.is_weak) AS weak_nodes, \"\n", " \"count(n._weak_init_done) AS initialized_nodes\"\n", ")\n", "row = result[0] if result else {}\n", "print(f\" Total nodes: {row.get('total_nodes', 0)}\")\n", "print(f\" Weak nodes: {row.get('weak_nodes', 0)}\")\n", "print(f\" Initialized nodes: {row.get('initialized_nodes', 0)}\")\n", "\n", "result = graph.query(\n", " \"MATCH ()-[r]->() \"\n", " \"RETURN count(r) AS total_edges, \"\n", " \"count(r._propagate) AS propagate_edges\"\n", ")\n", "row = result[0] if result else {}\n", "print(f\" Total edges: {row.get('total_edges', 0)}\")\n", "print(f\" Propagate edges: {row.get('propagate_edges', 0)}\")\n", "\n", "print(\"\\n\" + \"=\" * 60)\n", "print(\" ✅ Demo completed successfully!\")\n", "print(\"=\" * 60)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Cleanup\n", "\n", "Close the Neo4j connection when done." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "graph.close()\n", "print(\"✅ Neo4j connection closed.\")" ] } ], "metadata": { "kernelspec": { "display_name": "Python (cvcdocdb)", "language": "python", "name": "cvcdocdb-env" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.11.14" } }, "nbformat": 4, "nbformat_minor": 4 }