Raw capture β raw/icebug-format.md
url: https://github.com/Ladybug-Memory/icebug-format title: icebug-format author: Ladybug Memory date_fetched: 2026-08-01 date_published: 2025
A standardized graph format for efficient graph data interchange. Converts property graphs stored as relational tables into CSR (Compressed Sparse Row) adjacency format, backed by Parquet files (disk) or Apache Arrow tables (memory).
icebug-format/
icebug-format.py # CLI entry point (7 lines)
icebug_format/
__init__.py # Exports main() and IcebugMemGraph
cli.py # CLI: DuckDBβCSR conversion (866 lines)
memory.py # In-memory IcebugMemGraph class (234 lines)
graphar.py # GraphArβCSR converter (607 lines)
test_csr_duckdb.py # Verify CSR disk output (155 lines)
tests/
test_memory.py # Tests for IcebugMemGraph (285 lines)
test_cli.py # Tests for CLI (447 lines)
doc/
spec.md # CLI specification (507 lines)
examples/
karate/ # Zachary's karate club example graph
pyproject.toml # Project config (v1.0.0, Python 3.9+)
Total: ~3,328 lines across source and tests.
The CLI icebug-format converts a DuckDB source database containing nodes_* / edges_* tables into:
- nodes_<name>.parquet β original node data
- indices_<name>.parquet β target node for each edge, sorted by source
- indptr_<name>.parquet β row-pointer array (size N+1)
- schema.cypher β Cypher schema for mounting in LadybugDB
All Parquet files include icebug_disk_version: v1 in their KV metadata.
The Python API IcebugMemGraph.from_arrow_tables() converts Arrow tables directly into an in-memory CSR graph:
- src / dest β node tables passed through unchanged
- indices β target column + edge properties, ordered by (source, target)
- indptr β ptr column of length len(src) + 1
The project is a format converter with a pipeline architecture: read relational tables β create dense node mappings β join edges to mappings β build CSR indptr/indices β export to Parquet β generate Cypher schema.
It does not store graphs itself; it produces a representation that graph engines can mount directly from object storage without ingesting into a proprietary store.
Each node type gets its own zero-based dense mapping (csr_index), ordered deterministically by the node table's first column. This means node IDs don't need to be globally dense or globally unique β only per-type. The mapping tables are stored as {prefix}_mapping_{node_type}.
All adjacency is stored as CSR β the standard format for sparse matrices. indptr[i] is the start offset in indices for source node i, and indptr[i+1] - indptr[i] gives the out-degree. This enables O(1) lookup of a node's neighbors. The csr_source column is not stored in the final indices table β it's encoded by the offsets.
The implementation uses DuckDB for all heavy lifting: table discovery, sorting, JOIN operations for endpoint mapping, GROUP BY for degree computation, and Parquet export. DuckDB's unnest(range(...)) is used to generate node ranges for the indptr computation, and row_number() OVER (...) for dense ID assignment. The spec is explicitly backend-agnostic, documenting requirements a non-DuckDB backend must meet.
In directed mode, self-loops are preserved as-is. With --add-reverse-edges, self-loops appear only once (forward only) β the WHERE source != target clause on the reverse UNION ALL prevents duplication.
--add-reverse-edges emits the reverse direction for each non-self-loop edge, producing symmetric adjacency. Edge properties are copied onto both directions. This is all-or-nothing per conversion β cannot be applied selectively per edge type. Requires the same node table on both sides of every edge.
Source and target columns are resolved by priority-order alias matching with a positional fallback:
- Source: source β src β from β 0th column
- Target: target β destination β dest β to β 1st column
Any remaining columns become edge properties.
nodes_* and edges_* tables via information_schema.tablesschema.cypher for edge endpoint type inforow_number() OVER (ORDER BY pk) - 1)
- Join edge source/target to mapping tables (inner join β dangling edges dropped)
- Optional reverse-edge expansion via UNION ALL
- Build indptr: cumulative degree per source node, prepended with 0
- Build indices: csr_target + edge properties, ordered by (source, target)
- Drop temporary relations tableicebug_disk_version metadataschema.cypher with WITH (storage = '', format = 'icebug-disk') extensionsReads GraphAr's YAML-based graph info, parses Parquet vertex and edge files from the GraphAr directory layout, reassembles vertices and edges, then feeds through the same CSR conversion pipeline. GraphAr's edge direction and property structure map naturally onto icebug's CSR representation.
duckdb>=1.3.2 β SQL execution, sorting, Parquet exportpyarrow>=21.0.0 β Arrow table handling, Parquet metadatagraphar (optional) β GraphAr format inputOptimized for: Scan efficiency from object storage. CSR + Parquet means a graph engine can read just the files it needs without ingesting the whole graph.
Sacrificed: Write-time convenience. The format is a conversion target, not something you author directly. You need your data in DuckDB (or GraphAr) first.
Missing: Incremental updates. The CLI drops all existing output tables before conversion β full replacement only.
Test mode limitation: --test with --limit N applies floor(N / num_edge_tables) per table before ordering, but ordering before the LIMIT is backend-dependent. Test-mode samples are not deterministic across backends.