Back to blog
Kuzu Migration Guide: Moving an Archived Kuzu Graph to HelixDB

This blog is written by AI for SEO

Kuzu Migration Guide: Moving an Archived Kuzu Graph to HelixDB

HelixDB9 min read

The Kuzu repository on GitHub was archived in October 2025. Teams that picked Kuzu for embedded graph workloads, agent memory, or local GraphRAG pipelines are now running on frozen code with no security patches and no maintenance releases.

An unmaintained database inside a live product pipeline creates compliance risk and blocks runtime upgrades. If your workload involves transactional state updates, real-time agent context, or hybrid graph-vector retrieval, you need a destination that is actively maintained.

This guide walks through a full migration from Kuzu to HelixDB. You will inspect your existing Kuzu DDL, export tables to Parquet files, map node schemas and indexes to HelixDB, load data with the TypeScript SDK, and rewrite your Cypher queries as application-level JSON queries. Before you start, make sure you have Node.js 18 or higher, access to your existing Kuzu storage directory, and the Helix CLI installed.

Step 1: Confirm Kuzu's status and decide if you should move

The upstream Kuzu project was archived on GitHub in October 2025. LadybugDB is the successor project in the same C++ lineage, but staying on the legacy Kuzu binary leaves your application exposed to unpatched runtime bugs. Where you migrate depends on what your database workload actually does.

Kuzu was built from the ground up as an embedded graph OLAP engine. Its storage and vectorized execution pipeline are good at analytical scans, heavy aggregations, and bulk reads across structured graph tables. If your main use case is offline graph analytics, business intelligence over complex networks, or batch graph algorithms, LadybugDB is the direct successor for pure OLAP workloads. You can read a direct comparison in our guide on HelixDB vs LadybugDB.

Agent memory and interactive GraphRAG are not OLAP workloads. They are operational, low-latency OLTP systems. An autonomous agent constantly writes new observation nodes, links conversational entities, updates semantic vectors, and runs immediate point lookups alongside localized traversals. An analytical columnar database under agent memory creates write-amplification bottlenecks and transaction friction. HelixDB is an open-source, Rust-based OLTP graph-vector database built for exactly this pattern. It combines multi-hop graph traversal, native vector search, and BM25 full-text search in a single storage engine, so you don't have to duct-tape separate transactional stores together.

Step 2: Inventory your Kuzu schema, vector index and FTS index

Before you extract anything, document every table definition and index in your Kuzu database. Kuzu uses strict, typed schemas declared with Cypher DDL. Open the Kuzu CLI or check your project setup script to find the table statements.

A typical agent memory schema in Kuzu looks like this:

CREATE NODE TABLE Entity(
    id STRING,
    name STRING,
    entity_type STRING,
    created_at INT64,
    PRIMARY KEY (id)
);

CREATE NODE TABLE Document(
    id STRING,
    content STRING,
    embedding FLOAT[1536],
    PRIMARY KEY (id)
);

CREATE REL TABLE MENTIONS(
    FROM Document TO Entity,
    confidence DOUBLE
);

Next, catalog your indexes. Kuzu keeps vector indexing and full-text search behind extension functions instead of built-in property constraints. Look in your startup scripts for extension calls like CALL CREATE_VECTOR_INDEX and CALL CREATE_FTS_INDEX:

CALL CREATE_VECTOR_INDEX('Document', 'embedding', 'cosine');
CALL CREATE_FTS_INDEX('Document', 'content', stemmer := 'english');

Run CALL SHOW_TABLES() RETURN *; inside the Kuzu CLI to confirm no untracked tables exist. Write down the primary key field for each node table and the exact direction of your relationship tables. HelixDB treats both nodes and edges as property holders, but creating an edge requires that the source and target node identifiers already exist.

Step 3: Export the graph with EXPORT DATABASE (Parquet default) or COPY ... TO

Kuzu has an internal migration command that dumps schema definitions and table data in one pass. The fastest way to export your full dataset is EXPORT DATABASE, run inside the Kuzu CLI or through an embedded language binding:

EXPORT DATABASE '/tmp/kuzu_export' (FORMAT = 'PARQUET');

Kuzu defaults to Parquet for database dumps. The command writes a schema.cypher file with your DDL statements, plus one Parquet file per node and relationship table. Relationship tables produce separate Parquet partitions holding source keys, destination keys, and relationship properties.

If you only need a subset of the graph, or your database holds test fixtures you want to drop, use COPY ... TO instead. It lets you project and filter straight into a standalone Parquet target:

COPY (
    MATCH (d:Document)-[r:MENTIONS]->(e:Entity)
    RETURN d.id AS doc_id, e.id AS entity_id, r.confidence AS confidence
) TO '/tmp/kuzu_export/mentions_filtered.parquet';

Check the exported directory. The Parquet row counts should match what Kuzu reports. If your vectors were stored as fixed-size arrays (FLOAT[1536]), Parquet keeps them as list structures, and those translate cleanly at ingest.

Step 4: Map tables, relationships, embeddings and FTS to HelixDB

Moving from Kuzu to HelixDB takes your data from rigid tabular definitions to a schema-flexible graph-vector model. You don't need to pre-create any relational DDL skeletons in HelixDB before writing data.

Translate your components using these rules:

  1. Node Tables to HelixDB Labels: In Kuzu, Entity and Document were fixed relational tables. In HelixDB they become node labels (Entity, Document). Properties like name, entity_type, and content map directly to top-level node properties.

  2. Relationship Tables to Directed Edges: Kuzu's CREATE REL TABLE MENTIONS(FROM Document TO Entity) becomes directed edges labeled MENTIONS, connecting source nodes to target nodes. Properties on the relationship, such as confidence, stay attached to the edge.

  3. Vector Embeddings: In Kuzu, vectors are fixed-dimension arrays on node tables. In HelixDB, vector properties must be top-level properties on nodes or edges. HelixDB normalizes vector values to float32 internally and natively supports cosine, euclidean, and manhattan distance metrics. You don't need a bolt-on sidecar vector index.

  4. Full-Text Search: HelixDB's native BM25 text indexing replaces Kuzu's separate FTS extension. BM25 indexes run over string properties next to vector indexes, so hybrid queries can cover semantic and lexical dimensions in one step.

  5. Type Mapping: Map Kuzu INT64 to integer, DOUBLE to float, and STRING to string. Store dates and timestamps as numeric epoch integers to get the benefit of HelixDB's efficient time-range indexing.

Step 5: Stand up HelixDB locally with helix init and helix start dev, then load nodes before edges with idempotent upserts instead of MERGE

Install the Helix CLI if you haven't already, initialize your project root, and start the development engine:

helix init
helix start dev

In Kuzu, engineers often relied on Cypher MERGE statements to avoid inserting duplicate records. HelixDB skips Cypher merge syntax and uses programmatic, idempotent writes through its native SDKs. To keep the graph intact, always load node datasets completely before inserting the edges that connect them.

Install the official TypeScript SDK:

npm install @helix-db/helix-db parquetjs

Create a migration script (migrate.ts) that reads the exported Parquet records and writes them in transactional batches with writeBatch:

import { writeBatch } from "@helix-db/helix-db";
import parquet from "parquetjs";

async function importNodes() {
  const reader = await parquet.ParquetReader.openFile("/tmp/kuzu_export/Document.parquet");
  const cursor = reader.getCursor();
  let record = null;
  const batch = [];

  while ((record = await cursor.next())) {
    batch.push({
      id: record.id,
      label: "Document",
      properties: {
        content: record.content,
        embedding: Array.from(record.embedding),
      },
    });

    if (batch.length >= 500) {
      await flushBatch(batch);
      batch.length = 0;
    }
  }
  if (batch.length > 0) {
    await flushBatch(batch);
  }
  await reader.close();
}

async function flushBatch(records: any[]) {
  await writeBatch(async (tx) => {
    for (const item of records) {
      tx.upsertNode({
        id: item.id,
        label: item.label,
        properties: item.properties,
      });
    }
  });
}

Repeat the process for relationships: read the relationship Parquet file and call tx.upsertEdge({ sourceId, targetId, label: 'MENTIONS', properties: { confidence } }). Loading nodes first means edge upserts never point at entity IDs that don't exist.

Step 6: Rewrite your Cypher MATCH, QUERY_VECTOR_INDEX and QUERY_FTS_INDEX calls as TypeScript SDK queries sent to POST /v2/query

In Kuzu, a hybrid graph-vector lookup meant stitching Cypher together with several procedure calls. You ran CALL QUERY_VECTOR_INDEX to find candidate nodes, matched relationships with MATCH, and sometimes joined full-text searches with CALL QUERY_FTS_INDEX. In production, that logic ended up scattered.

HelixDB drops the separate query language. The TypeScript SDK builds structured JSON queries that post directly to POST /v2/query. The engine parses the JSON payload and turns it into the code that executes the query.

Here is a Kuzu semantic-traversal query rewritten as a single HelixDB TypeScript call:

import { g, VectorDistanceMetric } from "@helix-db/helix-db";

// Query: Find documents semantically similar to an input vector,
// then traverse outward to related Entity nodes.
const query = g()
  .nodes("Document")
  .vectorSearch({
    property: "embedding",
    vector: targetVector,
    metric: VectorDistanceMetric.Cosine,
    limit: 10,
  })
  .outEdges("MENTIONS")
  .inNodes("Entity")
  .select(["id", "name", "entity_type"]);

const results = await query.execute();

HelixDB also supports vector pre-filtering, so similarity searches run only over an already-filtered candidate set instead of the whole vector space. You will want this when structuring retrieval for how to build a GraphRAG pipeline.

Keep HelixDB's documented engine limits in mind as you translate queries. Unrestricted vector searches are subject to an upper limit on effective results. If your query uses traversal-scoped candidate sets, that ceiling is applied after candidate intersection. Candidate streams that exceed supported limits return an explicit query error instead of silently truncating.

Step 7: Verify counts and sample queries, then pick a deployment (embedded, Docker server, HelixDB Cloud) and troubleshoot common failures

Once your migration script finishes, check data parity between Kuzu and HelixDB. Count nodes and edges in HelixDB and compare them against your exported Parquet row counts:

import { g } from "@helix-db/helix-db";

const docCount = await g().nodes("Document").count().execute();
const entityCount = await g().nodes("Entity").count().execute();
const edgeCount = await g().edges("MENTIONS").count().execute();

console.log(`Verified counts: ${docCount} Docs, ${entityCount} Entities, ${edgeCount} Edges`);

Next, pick a deployment target. HelixDB offers three run modes on the same underlying SlateDB storage engine:

  1. Embedded: Run directly inside your application process, in memory or on local disk.

  2. Self-Hosted Docker Server: Deploy a standalone container set up for in-memory execution, local disk persistence, or S3-compatible object storage.

  3. HelixDB Cloud: A managed service on object storage, with auto-scaling reader nodes, single-writer serializable ACID transactions, and tenant isolation.

If the migration fails, check these common causes:

  • Orphaned Edge Insertion: If an edge write fails, verify that both the source and target node identifiers exist. Make sure your script finishes node ingest completely before inserting edges.

  • Nested Vector Properties: HelixDB requires vector embeddings to be top-level properties on nodes or edges. If you nested vectors under a sub-document property during export, flatten them to the root property map.

  • Candidate Ceiling Errors: If a hybrid traversal throws an error during a large scan, you have exceeded the 1,000,000 entity candidate stream limit. Apply property or time-range filters earlier in the query pipeline to shrink the candidate set.

Conclusion

Moving off an archived database protects your stack from unmaintained dependencies. Kuzu pioneered embedded graph analytics, but modern AI architectures need transactional write performance, native vector indices, and graph traversals in one engine.

HelixDB is a purpose-built OLTP foundation for production AI memory and GraphRAG systems. You can read more about designing persistent context in our guide to AI agent memory architecture.

Star HelixDB on GitHub at github.com/HelixDB/helix-db, install the CLI, and migrate your graph out of cold storage today.

Build with HelixDB

Give your coding agent the setup prompt, or sign up and deploy a database.

Sign up