Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions database/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Database

Off-chain PostgreSQL layer for indexing Soroban contract events to power the analytics dashboard.

## Structure

```
database/
├── migrations/
│ └── 001_create_events_table.sql # events table + analytics indexes
└── benchmarks/
└── explain_analyze_indexes.sql # EXPLAIN ANALYZE before/after comparison
```

## Applying the migration

```bash
psql -U <user> -d <dbname> -f database/migrations/001_create_events_table.sql
```

## Running benchmarks

Seed the table first (see the seed helper comment in the benchmark file), then:

```bash
psql -U <user> -d <dbname> -f database/benchmarks/explain_analyze_indexes.sql
```

Compare `Planning Time` and `Execution Time` in the output before and after the migration.
70 changes: 70 additions & 0 deletions database/benchmarks/explain_analyze_indexes.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
-- explain_analyze_indexes.sql
-- Measures query execution time before and after adding the analytics indexes.
--
-- Usage:
-- 1. Run the BEFORE blocks BEFORE applying the migration (drop indexes if needed).
-- 2. Apply 001_create_events_table.sql
-- 3. Run the AFTER blocks and compare Planning/Execution times.
--
-- Requires: a populated `events` table with representative data.
-- Tip: seed with at least 100k rows for meaningful results.

-- ─── Seed helper (optional) ───────────────────────────────────────────────────
-- INSERT INTO events (group_id, member_address, event_type, amount, cycle,
-- ledger_sequence, transaction_hash, created_at)
-- SELECT
-- (random() * 999 + 1)::BIGINT,
-- 'G' || substr(md5(random()::text), 1, 55),
-- (ARRAY['ContributionMade','PayoutExecuted','MemberJoined','GroupCreated'])[ceil(random()*4)::int],
-- (random() * 10000)::NUMERIC(20,7),
-- (random() * 11 + 1)::INTEGER,
-- (random() * 1000000)::BIGINT,
-- md5(random()::text),
-- NOW() - (random() * INTERVAL '365 days')
-- FROM generate_series(1, 100000);

-- ─────────────────────────────────────────────────────────────────────────────
-- QUERY 1: Cycle analytics (group_id, event_type, created_at)
-- ─────────────────────────────────────────────────────────────────────────────

-- BEFORE (sequential scan expected without index)
EXPLAIN ANALYZE
SELECT group_id, event_type, created_at, amount, cycle
FROM events
WHERE group_id = 42
AND event_type = 'ContributionMade'
AND created_at >= NOW() - INTERVAL '30 days'
ORDER BY created_at DESC;

-- AFTER (index scan expected on idx_events_cycle_analytics)
-- Run the same query again after migration — output should show:
-- "Index Scan using idx_events_cycle_analytics on events"
EXPLAIN ANALYZE
SELECT group_id, event_type, created_at, amount, cycle
FROM events
WHERE group_id = 42
AND event_type = 'ContributionMade'
AND created_at >= NOW() - INTERVAL '30 days'
ORDER BY created_at DESC;

-- ─────────────────────────────────────────────────────────────────────────────
-- QUERY 2: Member history (member_address, event_type)
-- ─────────────────────────────────────────────────────────────────────────────

-- BEFORE (sequential scan expected without index)
EXPLAIN ANALYZE
SELECT member_address, event_type, created_at, amount, group_id
FROM events
WHERE member_address = 'GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5'
AND event_type = 'PayoutExecuted'
ORDER BY created_at DESC;

-- AFTER (index scan expected on idx_events_member_history)
-- Run the same query again after migration — output should show:
-- "Index Scan using idx_events_member_history on events"
EXPLAIN ANALYZE
SELECT member_address, event_type, created_at, amount, group_id
FROM events
WHERE member_address = 'GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5'
AND event_type = 'PayoutExecuted'
ORDER BY created_at DESC;
36 changes: 36 additions & 0 deletions database/migrations/001_create_events_table.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
-- Migration: 001_create_events_table.sql
-- Creates the events table for off-chain indexing of Soroban contract events.
-- This table powers the analytics dashboard and leaderboard queries.

CREATE TABLE IF NOT EXISTS events (
id BIGSERIAL PRIMARY KEY,
group_id BIGINT NOT NULL,
member_address VARCHAR(56) NOT NULL, -- Stellar address (G... format, max 56 chars)
event_type VARCHAR(64) NOT NULL, -- e.g. 'ContributionMade', 'PayoutExecuted'
amount NUMERIC(20,7), -- XLM amount in stroops / 10^7
cycle INTEGER,
ledger_sequence BIGINT NOT NULL,
transaction_hash VARCHAR(64) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
raw_payload JSONB -- full event payload for flexibility
);

-- ─── Indexes ──────────────────────────────────────────────────────────────────

-- Index 1: Cycle analytics queries
-- Supports dashboard queries that filter by group, event type, and time range.
-- e.g. "show all ContributionMade events for group 42 in the last 30 days"
CREATE INDEX IF NOT EXISTS idx_events_cycle_analytics
ON events (group_id, event_type, created_at DESC);

-- Index 2: Member history queries
-- Supports per-member history lookups filtered by event type.
-- e.g. "show all PayoutExecuted events for member G...XYZ"
CREATE INDEX IF NOT EXISTS idx_events_member_history
ON events (member_address, event_type);

-- ─── Comments ─────────────────────────────────────────────────────────────────

COMMENT ON TABLE events IS 'Off-chain index of Soroban contract events for analytics.';
COMMENT ON INDEX idx_events_cycle_analytics IS 'Speeds up cycle analytics dashboard queries (group_id, event_type, created_at).';
COMMENT ON INDEX idx_events_member_history IS 'Speeds up member history queries (member_address, event_type).';
Loading