From 7334e728923433ddbfa951a10e78d3286b0b5e86 Mon Sep 17 00:00:00 2001 From: Arun Sharma Date: Tue, 15 Sep 2026 11:36:42 -0700 Subject: [PATCH 1/2] Add Integrations section: Snowflake Native App and PostgreSQL (pg_ladybug) guides --- astro.config.mjs | 9 + src/content/docs/integrations/index.mdx | 20 ++ src/content/docs/integrations/postgres.mdx | 232 +++++++++++++++++++ src/content/docs/integrations/snowflake.mdx | 244 ++++++++++++++++++++ 4 files changed, 505 insertions(+) create mode 100644 src/content/docs/integrations/index.mdx create mode 100644 src/content/docs/integrations/postgres.mdx create mode 100644 src/content/docs/integrations/snowflake.mdx diff --git a/astro.config.mjs b/astro.config.mjs index 41a5640..a6c3260 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -315,6 +315,15 @@ export default defineConfig({ { label: 'Vector search', link: '/extensions/vector'}, ], }, + { + label: 'Integrations', + collapsed: true, + items: [ + { label: 'Overview', link: '/integrations'}, + { label: 'Snowflake', link: '/integrations/snowflake'}, + { label: 'PostgreSQL', link: '/integrations/postgres'}, + ], + }, ], plugins: process.env.CHECK_LINKS ? [ diff --git a/src/content/docs/integrations/index.mdx b/src/content/docs/integrations/index.mdx new file mode 100644 index 0000000..c774220 --- /dev/null +++ b/src/content/docs/integrations/index.mdx @@ -0,0 +1,20 @@ +--- +title: Integrations +description: Run Ladybug inside other platforms — Snowflake Native App and PostgreSQL extension. +--- + +Ladybug can run inside the platforms you already use, so you can query your existing +data as a graph with Cypher without moving it elsewhere. + +## Available integrations + +| Integration | Description | +|-------------|-------------| +| [Snowflake](/integrations/snowflake) | Run Ladybug as a Snowflake Native App and query Snowflake tables with Cypher. | +| [PostgreSQL](/integrations/postgres) | Run Cypher queries inside PostgreSQL with the `pg_ladybug` extension. | + +:::note[Note] +These integrations are different from [attaching](/extensions/attach/rdbms) external +databases to a standalone Ladybug instance. Here, Ladybug runs *inside* the host +platform: queries execute against your Snowflake or PostgreSQL tables in place. +::: diff --git a/src/content/docs/integrations/postgres.mdx b/src/content/docs/integrations/postgres.mdx new file mode 100644 index 0000000..f14559b --- /dev/null +++ b/src/content/docs/integrations/postgres.mdx @@ -0,0 +1,232 @@ +--- +title: PostgreSQL Extension (pg_ladybug) +description: Run Cypher queries inside PostgreSQL with the pg_ladybug extension. +--- + +**Cypher query support as a PostgreSQL extension.** + +`pg_ladybug` embeds the [Ladybug](https://github.com/LadybugDB/ladybug) graph engine +via its C API (`liblbug`) and lets you run **Cypher** queries that read your **Postgres** +tables — with native Postgres execution under the hood. + +:::note[Note] +This page covers `pg_ladybug`, which runs Cypher *inside* PostgreSQL. To query a +PostgreSQL database from a standalone Ladybug instance instead, see the +[`postgres` extension](/extensions/attach/postgres). +::: + +Source: [LadybugDB/pg_ladybug](https://github.com/LadybugDB/pg_ladybug). + +## How it works + +1. You declare which Postgres tables represent Cypher node/edge labels + (`ladybug.register_node`, `ladybug.register_edge`), or use the `node_*`/`rel_*` + naming convention. +2. You write a `MATCH` query in Cypher. +3. `pg_ladybug` hands it to the embedded Ladybug planner. Ladybug's planner + detects that all tables are backed by the *same* Postgres database and + rewrites the entire pattern into a **single SQL JOIN query**. +4. `pg_ladybug` extracts that SQL from the EXPLAIN plan and runs it **natively via + SPI** against your local Postgres tables. +5. Results stream back through a standard `SETOF record` SRF. + +There is also a second mode: run Cypher-only queries that don't access Postgres tables +at all. You can create Ladybug tables just like in a standalone Ladybug deployment. +Data lives in `$PGDATA/storage.lbdb`. Mixed-mode execution is not supported in this +release. + +## Requirements + +- **PostgreSQL 17+** (tested on 17 and 18) +- **pg_config** (postgresql-server-dev package) +- **`liblbug.so` v0.19.0+** (Ladybug engine shared library; see Installation) +- **`libpg_client.lbug_extension`** (pg_client extension for ATTACH; see Installation) +- **OpenSSL** (liblbug requires `libssl`/`libcrypto`) + +## Installation + +### 1. Download liblbug and pg_client + +You need **liblbug v0.19.0 or later** and the **pg_client extension**. + +```bash +# Download liblbug v0.19.0 (shared library) +# Set LBUG_VERSION or use a nightly build RUN_ID +export LBUG_PRECOMPILED_RUN_ID= +export LBUG_TARGET_DIR=./lib +export LBUG_LIB_KIND=shared +bash scripts/download-liblbug.sh + +# Download pg_client extension (nightly build from LadybugDB/extensions) +# Place libpg_client.lbug_extension next to liblbug.so +cp /path/to/libpg_client.lbug_extension lib/ +``` + +The `lib/` directory should contain: + +``` +lib/ +├── liblbug.so -> liblbug.so.0 +├── liblbug.so.0 -> liblbug.so.0.19.0.* +├── liblbug.so.0.19.0.* +└── libpg_client.lbug_extension +``` + +### 2. Build and install the extension + +```bash +make +sudo make install +``` + +### 3. Start a test database + +```bash +bash scripts/setup-test-db.sh +``` + +Or manually: + +```sql +CREATE EXTENSION pg_ladybug; +``` + +## Quick start + +```bash +# Run the full test suite (uses pgembed — no pre-installed PG needed) +bash scripts/test.sh + +# Or use an existing PG cluster +bash scripts/setup-test-db.sh +bash scripts/test.sh +``` + +## Manual test walkthrough + +```sql +-- Connect to the test database +\c ladybug_test + +-- Set connection string for the Ladybug ATTACH (via pg_client) +SET ladybug.pg_connstr = 'host=/var/run/postgresql port=5433 dbname=ladybug_test user=postgres'; + +-- --- Simple node MATCH --- +SELECT * FROM ladybug.cypher( + 'MATCH (n:node_person) RETURN n.name, n.age ORDER BY n.age' +) AS t(name text, age int); + +-- Output: +-- name | age +-- -------+----- +-- Bob | 25 +-- Dave | 28 +-- Alice | 30 +-- Carol | 35 + +-- --- See the pushed SQL --- +SELECT ladybug.pushed_sql( + 'MATCH (n:node_person) RETURN n.name, n.age ORDER BY n.age' +); +-- Returns: SELECT * FROM node_person ORDER BY age + +-- --- Relationship pattern (requires rel_* tables with FK constraints) --- +SELECT * FROM ladybug.cypher( + 'MATCH (a:node_person)-[r:rel_knows]->(b:node_person) RETURN a.name, b.name, r.since' +) AS t(a_name text, b_name text, since int); +``` + +**Note on labels vs table names:** the pg_client extension registers tables +using their raw names. Use `node_person` (the table name) as the label +in Cypher queries. Label-to-table mapping via `ladybug.register_node()` is +used by the SQL extraction layer. + +## Naming convention + +Tables with the following prefixes are automatically recognized by the +pg_client extension: + +| Prefix | Cypher role | Example | +|--------|--------------|---------------| +| `node_`| Node label | `node_person` | +| `rel_` | Relationship | `rel_knows` | + +For `rel_*` tables, the source and destination node tables are determined by +foreign key constraints on `src_id` and `dst_id` columns. + +You can also register tables manually using `ladybug.register_node()` and +`ladybug.register_edge()`. + +## Test scripts + +```bash +# Run the test suite (uses pgembed — no pre-installed PG needed) +bash scripts/test.sh + +# With verbose output +bash scripts/test.sh -v +``` + +The test suite uses [pgembed](https://pypi.org/project/pgembed/) to start a +temporary PostgreSQL instance, build the extension against it, install +pg_ladybug + pg_client, and run the full test suite. + +## SQL reference + +| Function | Description | +|---|---| +| `ladybug.cypher(text)` → `SETOF record` | Translate Cypher → SQL via Ladybug planner, execute natively, return rows | +| `ladybug.sql_query(text)` → `SETOF record` | Run arbitrary SQL via SPI (pure Postgres, no liblbug needed) | +| `ladybug.explain(text)` → `text` | Return the Ladybug EXPLAIN plan as text | +| `ladybug.pushed_sql(text)` → `text` | Return the pushed-down SQL that `cypher()` would execute | +| `ladybug.register_node(label, table_name, id_column, props_json, graph)` → `int` | Map a Postgres table as a Cypher node label | +| `ladybug.register_edge(label, table_name, from_col, to_col, id_column, graph)` → `int` | Map a Postgres table as a Cypher edge label | +| `ladybug.list_labels(graph)` → `table` | List all registered node/edge labels for a graph | +| `ladybug.reset_graph(graph)` → `int` | Clear all label mappings for a graph | + +## GUCs + +| GUC | Default | Description | +|---|---|---| +| `ladybug.pg_connstr` | `""` | Connection string used to ATTACH this Postgres to Ladybug's catalog via pg_client | +| `ladybug.storage_path` | `/storage.lbdb` | Filesystem path for the persistent Ladybug storage. If initialization at this path fails, the extension falls back to in-memory mode and emits a `WARNING`. Set to an empty string to disable persistent storage. | + +## Execution strategies + +`ladybug.cypher()` tries two strategies when executing a Cypher query: + +1. **Pushdown (default).** The Cypher is handed to the embedded Ladybug + planner, which extracts a pushed-down SQL string. That SQL is then run + natively by PostgreSQL via SPI, and the rows are streamed back through + the SRF. This is the fastest path and is the only one that lets the + engine plan across multiple Cypher patterns (e.g. multi-hop matches). +2. **Direct execution (fallback).** If the planner reports that no + pushdown is possible for the Cypher (for example `RETURN 1`, + `UNWIND [1, 2, 3] AS x RETURN x`, or any Cypher that produces values + the planner cannot translate to plain SQL), `ladybug.cypher()` falls + back to executing the Cypher as-is through the Ladybug engine and + returning the result rows as if a corresponding native SQL query had + been run. + +Use `ladybug.pushed_sql()` and `ladybug.explain()` to inspect which +strategy was used for a given query. + +The bridge uses the `pg_client` Ladybug extension (loaded from +`libpg_client.lbug_extension`) to ATTACH the local PostgreSQL database. +pg_client queries `information_schema.tables` and `information_schema.columns` +via libpq, making it compatible with all PostgreSQL versions. + +## Updating liblbug + +```bash +# Download a specific nightly build +export LBUG_PRECOMPILED_RUN_ID= +bash scripts/download-liblbug.sh + +# Or download the latest release +bash scripts/download-liblbug.sh +``` + +## License + +PostgreSQL License. diff --git a/src/content/docs/integrations/snowflake.mdx b/src/content/docs/integrations/snowflake.mdx new file mode 100644 index 0000000..93ec9ca --- /dev/null +++ b/src/content/docs/integrations/snowflake.mdx @@ -0,0 +1,244 @@ +--- +title: Snowflake Native App +description: Run Ladybug inside Snowflake as a Native App and query Snowflake tables with Cypher. +--- + +LadybugDB is a graph query engine that runs inside Snowflake as a Native App. +It lets you run [Cypher](https://en.wikipedia.org/wiki/Cypher_(query_language)) +queries against an in-memory graph built from your Snowflake tables — including +Iceberg tables — with no external infrastructure. + +## Required Privileges + +After installing the app, Snowflake prompts you to grant these account-level +privileges: + +| Privilege | Why it's needed | +|-----------|-----------------| +| **CREATE COMPUTE POOL** | LadybugDB runs as a container service and needs a compute pool (`CPU_X64_XS`, 1 node) to execute. The pool is created automatically during setup. | +| **BIND SERVICE ENDPOINT** | The container exposes an internal HTTP endpoint that Snowflake service functions call. This privilege allows the app to bind that endpoint. | + +These are requested in the manifest and granted through the Snowsight install +flow. No additional account-level grants are needed for the built-in demo. + +### Iceberg namespace grants (optional) + +To load your own tables into the graph, the app uses restricted caller's rights. +Grant the app read access to the database and schema containing your tables: + +```sql +GRANT CALLER USAGE ON DATABASE my_db TO APPLICATION ; +GRANT CALLER DATA READ ON SCHEMA my_db.my_schema TO APPLICATION ; +``` + +Both grants are required. The database-level grant allows the app to resolve +objects in the schema; the schema-level grant allows it to read table data. + +## Configuration + +No manual configuration is required. On install the app: + +1. Creates a compute pool (`_compute_pool`, `CPU_X64_XS`, 1 node). +2. Starts the LadybugDB container service. +3. Waits for the service to become ready. +4. Registers all service functions and procedures. + +The service auto-suspends after 10 minutes of inactivity to avoid charges, and +resumes automatically on the next function call. You can also manage the +lifecycle manually (see [Service Management](#service-management)). + +## Quick Start + +> In all examples below, replace `` with the name you chose when +> installing the application (e.g. `ladybugdb`). + +```sql +-- Verify the service is running +CALL .core.get_service_status(); + +-- Run all 6 built-in demo queries +SELECT .core.run_demo(); + +-- Run a custom Cypher query against the demo graph +SELECT .core.query_graph( + 'MATCH (u:User) RETURN u.name, u.age ORDER BY u.age' +); +``` + +## Functions and Procedures + +### Demo graph (built-in, stateless) + +| Function | Description | +|----------|-------------| +| `core.query_graph(cypher STRING)` | Run a Cypher query against the built-in demo graph. Returns JSON. | +| `core.run_demo()` | Run 6 sample queries and return all results as JSON. | +| `core.list_tables()` | Show node and relationship table definitions in the demo graph. | + +### Iceberg namespace (persistent, your data) + +| Function / Procedure | Description | +|----------------------|-------------| +| `core.register_namespace(schema_fqn STRING, schema_cypher STRING)` | Load all tables from a consumer schema into the graph. `schema_cypher` declares the graph topology (see below). | +| `core.query_iceberg(cypher STRING)` | Run a Cypher query against the loaded namespace graph. Returns JSON. | +| `core.iceberg_schema()` | Show node and relationship tables in the namespace graph. | +| `core.namespace_info()` | Return push-down metadata: source FQNs, PK/FK mappings, column types, and the original `schema_cypher`. | +| `core.clear_iceberg()` | Reset the namespace graph. | + +### Service management + +| Procedure | Description | +|-----------|-------------| +| `core.get_service_status()` | Check if the container service is running. | +| `core.suspend_service()` | Suspend the service to stop compute charges. | +| `core.resume_service()` | Resume a suspended service. | +| `core.get_service_logs(instance_id, container_name)` | View container logs. Use `'0'` and `'ladybug'` for defaults. | + +## Loading Your Data (Iceberg Namespace) + +### Naming convention + +Place your tables in a single schema. Name them as follows: + +- **Node tables**: any name *not* starting with `rel_`. + The first column is used as the primary key. +- **Relationship tables**: prefix with `rel_` (e.g. `rel_has_department`, + `rel_friends`). The first two columns are the source and destination foreign + keys matching the primary keys of the referenced node tables. + +### Schema declaration + +Pass a `schema_cypher` string that declares the graph topology using +`CREATE REL TABLE` statements: + +``` +CREATE REL TABLE has_department (FROM university TO department, MANY_MANY); +CREATE REL TABLE friends (FROM person TO person); +``` + +This tells LadybugDB which node tables each relationship connects. Backtick +quoting is supported for table names with special characters. + +### Full example + +```sql +-- 1. Grant the app access to your schema +GRANT CALLER USAGE ON DATABASE analytics TO APPLICATION ; +GRANT CALLER DATA READ ON SCHEMA analytics.graph_data TO APPLICATION ; + +-- 2. Register the namespace +CALL .core.register_namespace( + 'analytics.graph_data', + 'CREATE REL TABLE has_department (FROM university TO department, MANY_MANY); + CREATE REL TABLE collaborates (FROM university TO university);' +); + +-- 3. Query with Cypher +SELECT .core.query_iceberg( + 'MATCH (u:university {name: ''Stanford''})-[:has_department]->(d:department) + RETURN d.name, d.budget ORDER BY d.budget DESC' +); + +-- 4. Inspect the graph schema +SELECT .core.iceberg_schema(); + +-- 5. View push-down metadata (source table FQNs, column types) +SELECT .core.namespace_info(); +``` + +## Demo Graph + +The built-in demo graph models a small social network with **5 users** and +**3 cities**, connected by `Follows` and `LivesIn` relationships. + +### Example queries + +```sql +-- All users sorted by age +SELECT .core.query_graph( + 'MATCH (u:User) RETURN u.name, u.age ORDER BY u.age' +); + +-- Who does Alice follow? +SELECT .core.query_graph( + 'MATCH (a:User {name: ''Alice''})-[:Follows]->(b:User) RETURN b.name' +); + +-- Friends of friends (2-hop) from Alice +SELECT .core.query_graph( + 'MATCH (a:User {name: ''Alice''})-[:Follows]->(b:User)-[:Follows]->(c:User) + RETURN a.name AS start, b.name AS via, c.name AS reached' +); + +-- Users who follow someone in the same city +SELECT .core.query_graph( + 'MATCH (a:User)-[:Follows]->(b:User), + (a)-[:LivesIn]->(c:City)<-[:LivesIn]-(b) + RETURN a.name AS user, b.name AS follows, c.name AS shared_city' +); + +-- Count followers per user +SELECT .core.query_graph( + 'MATCH (a:User)-[:Follows]->(b:User) + RETURN b.name, COUNT(a) AS follower_count ORDER BY follower_count DESC' +); +``` + +## Output Format + +All functions return JSON strings. Each key is a column name and each value is +a list of values for that column: + +```json +{ + "u.name": ["Eve", "Bob", "Dave", "Alice", "Carol"], + "u.age": [22, 25, 28, 30, 35] +} +``` + +To parse results into rows in Snowflake: + +```sql +SELECT f.value::STRING AS name +FROM TABLE( + FLATTEN( + PARSE_JSON( + .core.query_graph('MATCH (u:User) RETURN u.name ORDER BY u.age') + ):'u.name' + ) +) f; +``` + +## Service Management + +The service runs on a `CPU_X64_XS` compute pool (1 node). It auto-suspends +after 10 minutes of inactivity and resumes automatically on the next call. + +To manage the lifecycle manually: + +```sql +-- Check status +CALL .core.get_service_status(); + +-- Suspend immediately +CALL .core.suspend_service(); + +-- Resume +CALL .core.resume_service(); + +-- View container logs +CALL .core.get_service_logs('0', 'ladybug'); +``` + +## Cypher Quick Reference + +``` +(n:Label) Match a node with a given label +(n:Label {prop: value}) Match a node with specific property +-[:REL_TYPE]-> Directed relationship +WHERE n.prop > value Filter condition +RETURN n.prop Select output columns +ORDER BY n.prop Sort results +LIMIT n Limit result count +COUNT(n) Aggregate count +``` From 4f361918dc0ed857482b590a34dccee77a40ada9 Mon Sep 17 00:00:00 2001 From: Arun Sharma Date: Tue, 15 Sep 2026 11:42:04 -0700 Subject: [PATCH 2/2] Snowflake page: grants required, drop Cypher quick reference --- src/content/docs/integrations/snowflake.mdx | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/src/content/docs/integrations/snowflake.mdx b/src/content/docs/integrations/snowflake.mdx index 93ec9ca..4b3e3c5 100644 --- a/src/content/docs/integrations/snowflake.mdx +++ b/src/content/docs/integrations/snowflake.mdx @@ -21,9 +21,9 @@ privileges: These are requested in the manifest and granted through the Snowsight install flow. No additional account-level grants are needed for the built-in demo. -### Iceberg namespace grants (optional) +### Iceberg namespace grants -To load your own tables into the graph, the app uses restricted caller's rights. +To query your own Snowflake tables, the app uses restricted caller's rights. Grant the app read access to the database and schema containing your tables: ```sql @@ -229,16 +229,3 @@ CALL .core.resume_service(); -- View container logs CALL .core.get_service_logs('0', 'ladybug'); ``` - -## Cypher Quick Reference - -``` -(n:Label) Match a node with a given label -(n:Label {prop: value}) Match a node with specific property --[:REL_TYPE]-> Directed relationship -WHERE n.prop > value Filter condition -RETURN n.prop Select output columns -ORDER BY n.prop Sort results -LIMIT n Limit result count -COUNT(n) Aggregate count -```