Create your first graph
Ladybug implements a structured property graph model and requires a pre-defined schema.
- Schema definition involves node and relationship tables and their associated properties.
- Each property key is strongly typed and these types must be explicitly declared.
- For node tables, a primary key must be defined.
- For relationship tables, no primary key is required.
Persistence
Ladybug supports both on-disk and in-memory modes of operation. The mode is determined at the time of creating the database, as explained below.
On-disk database
If you specify a database path when initializing a database, such as example.lbug, Ladybug
will operate in the on-disk mode. In this mode, Ladybug persists all data to disk at the given
path. All transactions are logged to a Write-Ahead Log (WAL) and updates are periodically merged into the
database files during checkpoints.
In-memory database
If you omit the database path, by specifying it as "" or :memory:, Ladybug will operate in the in-memory mode.
In this mode, there are no writes to the WAL, and no data is persisted to disk. All data is lost when the process finishes.
Quick start
Ensure that you have installed Ladybug using the CLI or your preferred client API. Also download the example CSV files from our GitHub repo.
mkdir ./data/curl -L -o ./data/city.csv https://raw.githubusercontent.com/lbugdb/lbug/refs/heads/master/dataset/demo-db/csv/city.csvcurl -L -o ./data/user.csv https://raw.githubusercontent.com/lbugdb/lbug/refs/heads/master/dataset/demo-db/csv/user.csvcurl -L -o ./data/follows.csv https://raw.githubusercontent.com/lbugdb/lbug/refs/heads/master/dataset/demo-db/csv/follows.csvcurl -L -o ./data/lives-in.csv https://raw.githubusercontent.com/lbugdb/lbug/refs/heads/master/dataset/demo-db/csv/lives-in.csvIn this example, we will create a graph with two node types, User and City, and two relationship types, Follows and LivesIn.
Because Ladybug is an embedded database, there are no servers to set up. You can simply import the
lbug module in your code and run queries on the database. The examples for different client APIs
below demonstrate how to create a graph schema and import data into an on-disk Ladybug database.
import lbug
def main(): # Create an empty on-disk database and connect to it db = lbug.Database("example.lbug") conn = lbug.Connection(db)
# Create schema conn.execute("CREATE NODE TABLE User(name STRING PRIMARY KEY, age INT64)") conn.execute("CREATE NODE TABLE City(name STRING PRIMARY KEY, population INT64)") conn.execute("CREATE REL TABLE Follows(FROM User TO User, since INT64)") conn.execute("CREATE REL TABLE LivesIn(FROM User TO City)")
# Insert data conn.execute('COPY User FROM "./data/user.csv"') conn.execute('COPY City FROM "./data/city.csv"') conn.execute('COPY Follows FROM "./data/follows.csv"') conn.execute('COPY LivesIn FROM "./data/lives-in.csv"')
# Execute Cypher query response = conn.execute( """ MATCH (a:User)-[f:Follows]->(b:User) RETURN a.name, b.name, f.since; """ ) for row in response: print(row)
if __name__ == "__main__": main()['Adam', 'Karissa', 2020]['Adam', 'Zhang', 2020]['Karissa', 'Zhang', 2021]['Zhang', 'Noura', 2022]The approach shown above returned a list of lists containing query results. See below for more output options for Python.
Output as a dictionary
You can also get the results of a Cypher query as a dictionary.
response = conn.execute( """ MATCH (a:User)-[f:Follows]->(b:User) RETURN a.name, b.name, f.since; """)for row in response.rows_as_dict(): print(row){'a.name': 'Adam', 'b.name': 'Karissa', 'f.since': 2020}{'a.name': 'Adam', 'b.name': 'Zhang', 'f.since': 2020}{'a.name': 'Karissa', 'b.name': 'Zhang', 'f.since': 2021}{'a.name': 'Zhang', 'b.name': 'Noura', 'f.since': 2022}Pandas
You can also pass the results of a Cypher query to a Pandas DataFrame
for downstream tasks. This assumes that pandas is installed in your environment.
# pip install pandasresponse = conn.execute( """ MATCH (a:User)-[f:Follows]->(b:User) RETURN a.name, b.name, f.since; """)print(response.get_as_df()) a.name b.name f.since0 Adam Karissa 20201 Adam Zhang 20202 Karissa Zhang 20213 Zhang Noura 2022Polars
Polars is another popular DataFrames library for Python, and you
can process the results of a Cypher query in much the same way you did with Pandas. This assumes
that polars is installed in your environment.
# pip install polarsresponse = conn.execute( """ MATCH (a:User)-[f:Follows]->(b:User) RETURN a.name, b.name, f.since; """)print(response.get_as_pl())shape: (4, 3)┌─────────┬─────────┬─────────┐│ a.name ┆ b.name ┆ f.since ││ --- ┆ --- ┆ --- ││ str ┆ str ┆ i64 │╞═════════╪═════════╪═════════╡│ Adam ┆ Karissa ┆ 2020 ││ Adam ┆ Zhang ┆ 2020 ││ Karissa ┆ Zhang ┆ 2021 ││ Zhang ┆ Noura ┆ 2022 │└─────────┴─────────┴─────────┘Arrow Table
You can also use the PyArrow library to work with
Arrow Tables in Python. This assumes that pyarrow is installed in your environment. This
approach is useful when you need to interoperate with other systems that use Arrow as a backend.
In fact, the get_as_pl() method shown above for Polars materializes a pyarrow.Table under the hood.
# pip install pyarrowresponse = conn.execute( """ MATCH (a:User)-[f:Follows]->(b:User) RETURN a.name, b.name, f.since; """)print(response.get_as_arrow())pyarrow.Tablea.name: stringb.name: stringf.since: int64----a.name: [["Adam","Adam","Karissa","Zhang"]]b.name: [["Karissa","Zhang","Zhang","Noura"]]f.since: [[2020,2020,2021,2022]]const lbug = require("lbug");
(async () => { // Create an empty on-disk database and connect to it const db = new lbug.Database("example.lbug"); const conn = new lbug.Connection(db);
// Create the tables await conn.query("CREATE NODE TABLE User(name STRING PRIMARY KEY, age INT64)"); await conn.query("CREATE NODE TABLE City(name STRING PRIMARY KEY, population INT64)"); await conn.query("CREATE REL TABLE Follows(FROM User TO User, since INT64)"); await conn.query("CREATE REL TABLE LivesIn(FROM User TO City)");
// Load the data await conn.query('COPY User FROM "./data/user.csv"'); await conn.query('COPY City FROM "./data/city.csv"'); await conn.query('COPY Follows FROM "./data/follows.csv"'); await conn.query('COPY LivesIn FROM "./data/lives-in.csv"');
const queryResult = await conn.query("MATCH (a:User)-[f:Follows]->(b:User) RETURN a.name, f.since, b.name;");
// Get all rows from the query result const rows = await queryResult.getAll();
// Print the rows for (const row of rows) { console.log(row); }})();{ "a.name": "Adam", "f.since": 2020, "b.name": "Karissa" }{ "a.name": "Adam", "f.since": 2020, "b.name": "Zhang" }{ "a.name": "Karissa", "f.since": 2021, "b.name": "Zhang" }{ "a.name": "Zhang", "f.since": 2022, "b.name": "Noura" }Ladybug’s Java client library is available on Maven Central. You can add the following snippet to your pom.xml to get it installed:
<dependency> <groupId>com.lbugdb</groupId> <artifactId>lbug</artifactId> <version>0.11.0</version></dependency>Alternatively, if you are using Gradle, you can add the following snippet to your build.gradle file to include Ladybug’s Java client library:
For Groovy DSL:
implementation 'com.lbugdb:lbug:0.11.0'For Kotlin DSL:
implementation("com.lbugdb:lbug:0.11.0")Below is an example Gradle project structure for a simple Java application that creates a graph schema and inserts data into the database for the given example.
├── build.gradle├── src/main│ ├── java│ │ └── Main.java│ └── resources│ │ └── user.csv│ │ └── city.csv│ │ └── follows.csv│ │ └── lives-in.csvThe minimal build.gradle contains the following configurations:
plugins { id 'java' id 'application'}application { mainClassName = "Main"}repositories { mavenCentral()}dependencies { implementation 'com.lbugdb:lbug:0.11.0'}The Main.java contains the following code:
import com.lbugdb.*;
public class Main { public static void main(String[] args) throws ObjectRefDestroyedException { // Create an empty on-disk database and connect to it Database db = new Database("example.lbug"); Connection conn = new Connection(db); // Create tables. conn.query("CREATE NODE TABLE User(name STRING PRIMARY KEY, age INT64)"); conn.query("CREATE NODE TABLE City(name STRING PRIMARY KEY, population INT64)"); conn.query("CREATE REL TABLE Follows(FROM User TO User, since INT64)"); conn.query("CREATE REL TABLE LivesIn(FROM User TO City)"); // Load data. conn.query("COPY User FROM 'src/main/resources/user.csv'"); conn.query("COPY City FROM 'src/main/resources/city.csv'"); conn.query("COPY Follows FROM 'src/main/resources/follows.csv'"); conn.query("COPY LivesIn FROM 'src/main/resources/lives-in.csv'");
// Execute a simple query. QueryResult result = conn.query("MATCH (a:User)-[f:Follows]->(b:User) RETURN a.name, f.since, b.name;"); while (result.hasNext()) { FlatTuple row = result.getNext(); System.out.print(row); } }}To execute the example, navigate to the project root directory and run the following command:
gradle runAdam|2020|KarissaAdam|2020|ZhangKarissa|2021|ZhangZhang|2022|NouraWhen installing the lbug crate via Cargo, it will by default build and statically link Ladybug’s C++
library from source. You can also link against the dynamic release libraries (see the Rust
crate docs for details).
The main.rs file contains the following code:
use lbug::{Connection, Database, Error, SystemConfig};
fn main() -> Result<(), Error> { // Create an empty on-disk database and connect to it let db = Database::new("example.lbug", SystemConfig::default())?; let conn = Connection::new(&db)?;
// Create the tables conn.query("CREATE NODE TABLE User(name STRING PRIMARY KEY, age INT64)")?; conn.query("CREATE NODE TABLE City(name STRING PRIMARY KEY, population INT64)")?; conn.query("CREATE REL TABLE Follows(FROM User TO User, since INT64)")?; conn.query("CREATE REL TABLE LivesIn(FROM User TO City)")?;
// Load the data conn.query("COPY User FROM './data/user.csv'")?; conn.query("COPY City FROM './data/city.csv'")?; conn.query("COPY Follows FROM './data/follows.csv'")?; conn.query("COPY LivesIn FROM './data/lives-in.csv'")?;
let query_result = conn.query("MATCH (a:User)-[f:Follows]->(b:User) RETURN a.name, f.since, b.name;")?;
// Print the rows for row in query_result { println!("{}, {}, {}", row[0], row[1], row[2]); } Ok(())}Adam, 2020, KarissaAdam, 2020, ZhangKarissa, 2021, ZhangZhang, 2022, Nourapackage main
import ( "fmt"
"github.com/LadybugDB/go-lbug")
func main() { // Create an empty on-disk database and connect to it systemConfig := lbug.DefaultSystemConfig() systemConfig.BufferPoolSize = 1024 * 1024 * 1024 db, err := lbug.OpenDatabase("example.lbug", systemConfig) if err != nil { panic(err) } defer db.Close()
conn, err := lbug.OpenConnection(db) if err != nil { panic(err) } defer conn.Close()
// Create schema queries := []string{ "CREATE NODE TABLE User(name STRING PRIMARY KEY, age INT64)", "CREATE NODE TABLE City(name STRING PRIMARY KEY, population INT64)", "CREATE REL TABLE Follows(FROM User TO User, since INT64)", "CREATE REL TABLE LivesIn(FROM User TO City)", "COPY User FROM \"./data/user.csv\"", "COPY City FROM \"./data/city.csv\"", "COPY Follows FROM \"./data/follows.csv\"", "COPY LivesIn FROM \"./data/lives-in.csv\"", } for _, query := range queries { queryResult, err := conn.Query(query) if err != nil { panic(err) } defer queryResult.Close() }
// Execute Cypher query query := "MATCH (a:User)-[e:Follows]->(b:User) RETURN a.name, e.since, b.name" result, err := conn.Query(query) if err != nil { panic(err) } defer result.Close() for result.HasNext() { tuple, err := result.Next() if err != nil { panic(err) } defer tuple.Close() // The result is a tuple, which can be converted to a slice. slice, err := tuple.GetAsSlice() if err != nil { panic(err) } fmt.Println(slice) }[Adam 2020 Karissa][Adam 2020 Zhang][Karissa 2021 Zhang][Zhang 2022 Noura]Initialize a new project using the Swift package manager:
mkdir lbug-swift-examplecd lbug-swift-exampleswift package init --type=executableUpdate the Package.swift file:
import PackageDescription
let package = Package( name: "lbug-swift-example", platforms: [ .macOS(.v11), .iOS(.v14), ], dependencies: [ .package(url: "https://github.com/LadybugDB/ladybug-swift/", branch: "0.11.0"), ], targets: [ // Targets are the basic building blocks of a package, defining a module or a test suite. // Targets can depend on other targets in this package and products from dependencies. .executableTarget( name: "lbug-swift-example", dependencies: [ .product(name: "Ladybug", package: "lbug-swift"), ] ), ])This configuration sets the minimum deployment target and adds the lbug-swift client as a dependency.
Next, replace the contents of Sources/main.swift:
import Ladybug
// Create an empty on-disk database and connect to itlet db = try! Database("example.lbug")let conn = try! Connection(db)
// Create schema and load datalet queries = [ "CREATE NODE TABLE User(name STRING PRIMARY KEY, age INT64)", "CREATE NODE TABLE City(name STRING PRIMARY KEY, population INT64)", "CREATE REL TABLE Follows(FROM User TO User, since INT64)", "CREATE REL TABLE LivesIn(FROM User TO City)", "COPY User FROM 'data/user.csv'", "COPY City FROM 'data/city.csv'", "COPY Follows FROM 'data/follows.csv'", "COPY LivesIn FROM 'data/lives-in.csv'",]
for query in queries { _ = try! conn.query(query)}
// Execute Cypher querylet res = try! conn.query("MATCH (a:User)-[e:Follows]->(b:User) RETURN a.name, e.since, b.name")for tuple in res { let dict = try! tuple.getAsDictionary() print(dict)}Build the project:
swift build -c releaseCopy the output executable so that it can access the data directory:
cp ./.build/arm64-apple-macosx/release/lbug-swift-example .If you are using an Intel Mac or Linux, replace arm64-apple-macosx with your specific architecture.
Finally, run the executable.
./lbug-swift-exampleYou should see the following output:
["b.name": Optional("Karissa"), "e.since": Optional(2020), "a.name": Optional("Adam")]["e.since": Optional(2020), "a.name": Optional("Adam"), "b.name": Optional("Zhang")]["b.name": Optional("Zhang"), "e.since": Optional(2021), "a.name": Optional("Karissa")]["e.since": Optional(2022), "a.name": Optional("Zhang"), "b.name": Optional("Noura")]The Ladybug C++ client is distributed as so/dylib/dll+lib library files along with a header file (lbug.hpp).
Once you’ve downloaded and extracted the C++ files into a directory, they are ready to use without
any additional installation. You just need to specify the library and include file search paths.
For example, organize the files and directories as follows:
├── include│ ├── lbug.hpp│ └── ......├── lib│ ├── liblbug.so / liblbug.dylib / lbug_shared.dll + lbug_shared.lib│ └── ......├── data│ ├── city.csv│ ├── follows.csv│ ├── lives-in.csv│ └── user.csv├── main.cpp├── CMakeLists.txt#include <iostream>
#include "include/lbug.hpp"
using namespace lbug::main;using namespace std;
unique_ptr<QueryResult> runQuery(const string_view &query, unique_ptr<Connection> &connection) { auto results = connection->query(query); if (!results->isSuccess()) { throw std::runtime_error(results->getErrorMessage()); } return results;}
int main() { // Remove example.lbug remove("example.lbug"); remove("example.lbug.wal");
// Create an empty on-disk database and connect to it SystemConfig systemConfig; auto database = make_unique<Database>("example.lbug", systemConfig); auto connection = make_unique<Connection>(database.get());
// Create the schema. runQuery("CREATE NODE TABLE User(name STRING PRIMARY KEY, age INT64)", connection); runQuery("CREATE NODE TABLE City(name STRING PRIMARY KEY, population INT64)", connection); runQuery("CREATE REL TABLE Follows(FROM User TO User, since INT64)", connection); runQuery("CREATE REL TABLE LivesIn(FROM User TO City)", connection);
// Load data. runQuery("COPY User FROM 'data/user.csv'", connection); runQuery("COPY City FROM 'data/city.csv'", connection); runQuery("COPY Follows FROM 'data/follows.csv'", connection); runQuery("COPY LivesIn FROM 'data/lives-in.csv'", connection);
// Execute a simple query. auto result = runQuery("MATCH (a:User)-[f:Follows]->(b:User) RETURN a.name, f.since, b.name;", connection);
// Output query result. while (result->hasNext()) { auto row = result->getNext(); std::cout << row->getValue(0)->getValue<string>() << " | " << row->getValue(1)->getValue<int64_t>() << " | " << row->getValue(2)->getValue<string>() << std::endl; } return 0;}cmake_minimum_required(VERSION 3.15)project(lbug-cpp)
set(CMAKE_CXX_STANDARD 20)
if(MSVC) # Required for M_PI on Windows add_compile_definitions(_USE_MATH_DEFINES) add_compile_definitions(NOMINMAX) add_compile_definitions(SERD_STATIC) # Non-english windows system may use other encodings other than utf-8 (e.g. Chinese use GBK). add_compile_options("/utf-8") # Enables support for custom hardware exception handling add_compile_options("/EHa") # Reduces the size of the static library by roughly 1/2 add_compile_options("/Zc:inline") # Disable type conversion warnings add_compile_options(/wd4244 /wd4267) # Remove the default to avoid warnings STRING(REPLACE "/EHsc" "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") STRING(REPLACE "/EHs" "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") # Store all libraries and binaries in the same directory so that lbug_shared.dll is found at runtime set(LIBRARY_OUTPUT_PATH "${CMAKE_BINARY_DIR}/src") set(EXECUTABLE_OUTPUT_PATH "${CMAKE_BINARY_DIR}/src") # This is a workaround for regex stackoverflow issue on windows in gtest. set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /STACK:8388608")
string(REGEX REPLACE "/W[3|4]" "/w" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") add_compile_options($<$<CONFIG:Release>:/W0>)else() add_compile_options(-Wall -Wextra) # Disable warnings for unknown pragmas, which is used by several third-party libraries add_compile_options(-Wno-unknown-pragmas)endif()
add_executable(lbug-cpp main.cpp)
# Find and link lbug libraryfind_library(LBUG_LIB lbug PATHS ${CMAKE_SOURCE_DIR}/lib REQUIRED)target_link_libraries(lbug-cpp PRIVATE ${LBUG_LIB})target_include_directories(lbug-cpp PRIVATE ${CMAKE_SOURCE_DIR}/include)Compile and run main.cpp:
cmake -S . -B buildcmake --build build./build/lbug-cppAdam | 2020 | KarissaAdam | 2020 | ZhangKarissa | 2021 | ZhangZhang | 2022 | NouraThe Ladybug C API shares the same so/dylib library files with the C++ API and can be used by
including the C header file (lbug.h).
In this example, we assume that the so/dylib, the header file, the CSV files, and the C code file
are all under the same directory:
├── include│ ├── lbug.h│ └── ......├── liblbug.so / liblbug.dylib├── main.c├── user.csv├── city.csv├── follows.csv└── lives-in.csvThe file main.c contains the following code:
#include <stdio.h>
#include "include/lbug.h"
int main(){ // Create an empty on-disk database and connect to it lbug_database db; lbug_database_init("example.lbug", lbug_default_system_config(), &db);
// Connect to the database. lbug_connection conn; lbug_connection_init(&db, &conn);
// Create the schema. lbug_query_result result; lbug_connection_query(&conn, "CREATE NODE TABLE User(name STRING PRIMARY KEY, age INT64)", &result); lbug_query_result_destroy(&result); lbug_connection_query(&conn, "CREATE NODE TABLE City(name STRING PRIMARY KEY, population INT64)", &result); lbug_query_result_destroy(&result); lbug_connection_query(&conn, "CREATE REL TABLE Follows(FROM User TO User, since INT64)", &result); lbug_query_result_destroy(&result); lbug_connection_query(&conn, "CREATE REL TABLE LivesIn(FROM User TO City)", &result); lbug_query_result_destroy(&result);
// Load data. lbug_connection_query(&conn, "COPY User FROM \"user.csv\"", &result); lbug_query_result_destroy(&result); lbug_connection_query(&conn, "COPY City FROM \"city.csv\"", &result); lbug_query_result_destroy(&result); lbug_connection_query(&conn, "COPY Follows FROM \"follows.csv\"", &result); lbug_query_result_destroy(&result); lbug_connection_query(&conn, "COPY LivesIn FROM \"lives-in.csv\"", &result); lbug_query_result_destroy(&result);
// Execute a simple query. lbug_connection_query(&conn, "MATCH (a:User)-[f:Follows]->(b:User) RETURN a.name, f.since, b.name;", &result);
// Output query result. lbug_flat_tuple tuple; lbug_value value; while (lbug_query_result_has_next(&result)) { lbug_query_result_get_next(&result, &tuple);
lbug_flat_tuple_get_value(&tuple, 0, &value); char *name = NULL; lbug_value_get_string(&value, &name); lbug_value_destroy(&value);
lbug_flat_tuple_get_value(&tuple, 1, &value); int64_t since = 0; lbug_value_get_int64(&value, &since); lbug_value_destroy(&value);
lbug_flat_tuple_get_value(&tuple, 2, &value); char *name2 = NULL; lbug_value_get_string(&value, &name2); lbug_value_destroy(&value);
printf("%s follows %s since %lld \n", name, name2, since); free(name); free(name2); } lbug_value_destroy(&value); lbug_flat_tuple_destroy(&tuple); lbug_query_result_destroy(&result); lbug_connection_destroy(&conn); lbug_database_destroy(&db); return 0;}Compile and run main.c: Since we did not install the liblbug as a system library, we need to
override the linker search path to correctly compile the C code and run the compiled program.
On Linux:
env LIBRARY_PATH=. LD_LIBRARY_PATH=. gcc main.c -llbugenv LIBRARY_PATH=. LD_LIBRARY_PATH=. ./a.outOn macOS:
env DYLD_LIBRARY_PATH=. LIBRARY_PATH=. clang main.c -llbugenv DYLD_LIBRARY_PATH=. LIBRARY_PATH=. ./a.outOn Windows, the library file is passed to the compiler directly and the current directory is used
automatically when searching for lbug_shared.dll at runtime:
cl main.c lbug_shared.lib./main.exeAdam follows Karissa since 2020Adam follows Zhang since 2020Karissa follows Zhang since 2021Zhang follows Noura since 2022When using the Ladybug CLI’s shell, you can create an on-disk database by specifying a path after
the lbug command in the terminal.
lbug example.lbugOpened the database example.lbug in read-write mode.Enter ":help" for usage hints.lbug>Proceed to enter the following Cypher statements separated by semicolons. Note that you must indicate the end of each query statement with a semicolon in the shell, otherwise it will not be parsed correctly and fail to execute.
// Create schemaCREATE NODE TABLE User(name STRING PRIMARY KEY, age INT64);CREATE NODE TABLE City(name STRING PRIMARY KEY, population INT64);CREATE REL TABLE Follows(FROM User TO User, since INT64);CREATE REL TABLE LivesIn(FROM User TO City);
// Insert dataCOPY User FROM "./data/user.csv";COPY City FROM "./data/city.csv";COPY Follows FROM "./data/follows.csv";COPY LivesIn FROM "./data/lives-in.csv";
// Execute Cypher queryMATCH (a:User)-[f:Follows]->(b:User)RETURN a.name, b.name, f.since;The following result is obtained:
┌─────────┬─────────┬─────────┐│ a.name │ b.name │ f.since ││ STRING │ STRING │ INT64 │├─────────┼─────────┼─────────┤│ Adam │ Karissa │ 2020 ││ Adam │ Zhang │ 2020 ││ Karissa │ Zhang │ 2021 ││ Zhang │ Noura │ 2022 │└─────────┴─────────┴─────────┘