tx2-query 0.1.2

SQL analytics layer for TX-2 ECS - one-way synchronization from ECS to SQL databases
Documentation
# tx2-query

SQL Analytics Layer for TX-2 ECS - enabling advanced analytics, complex queries, and external tool integration while keeping the ECS world as the source of truth.

## Overview

tx2-query provides a SQL side-car database for TX-2 applications. It's designed as an **analytics layer, NOT an ORM**:

- **ECS World → SQL Database**: One-way synchronization from ECS to SQL
- **Component → Table Mapping**: Automatic schema generation from component types
- **Incremental Sync**: Track and sync only changed entities/components
- **Multiple Backends**: PostgreSQL, SQLite, and extensible to more

## Philosophy

- ECS world is always the source of truth
- SQL database is a read-only projection for analytics
- No bi-directional sync - changes flow ECS → SQL only
- Optimized for analytical queries, not transactional workloads

## Features

- **Schema Generation**: Automatic DDL creation from component definitions
- **Query Builder**: Ergonomic SQL query construction
- **Sync API**: Incremental synchronization with batching
- **Backend Abstraction**: Support for multiple SQL databases
- **Type-Safe**: Leverages Rust's type system for safety
- **DuckDB Support**: OLAP workloads with columnar storage and window functions

## Installation

Add to your `Cargo.toml`:

```toml
[dependencies]
tx2-query = { version = "0.1", features = ["postgres", "sqlite", "duckdb"] }
tokio = { version = "1.0", features = ["full"] }
```

## Quick Start

```rust
use tx2_query::prelude::*;
use serde_json::json;

#[derive(Debug)]
struct Player;

#[tokio::main]
async fn main() -> Result<()> {
    // 1. Create schema generator
    let mut schema_gen = SchemaGenerator::new();
    schema_gen.register::<Player>(
        "Player",
        vec![
            ("name", SqlType::Text, false),
            ("email", SqlType::Text, false),
            ("score", SqlType::Integer, false),
        ],
    )?;

    // 2. Connect to database
    let backend = SqliteBackend::memory().await?;
    let sync = QuerySync::new(backend, schema_gen);

    // 3. Initialize schema
    sync.initialize_schema().await?;

    // 4. Track changes from your ECS world
    let mut player_data = HashMap::new();
    player_data.insert("name".to_string(), json!("Alice"));
    player_data.insert("email".to_string(), json!("alice@example.com"));
    player_data.insert("score".to_string(), json!(1500));

    sync.track_component_change(
        1,  // entity_id
        TypeId::of::<Player>(),
        "Player".to_string(),
        player_data,
    ).await?;

    // 5. Flush to database
    sync.flush().await?;

    // 6. Query with builder
    let query = SelectBuilder::new("Player")
        .select(vec!["name", "score"])
        .where_gt("score", json!(1000))
        .order_desc("score")
        .limit(10)
        .build()?;

    let results = sync.query(&query).await?;
    for row in results {
        println!("{}: {}",
            row.get_string("name").unwrap(),
            row.get_i64("score").unwrap()
        );
    }

    Ok(())
}
```

## Database Backends

### SQLite

```rust
use tx2_query::sqlite::SqliteBackend;

// In-memory database
let backend = SqliteBackend::memory().await?;

// File-based database
let backend = SqliteBackend::file("mydb.sqlite").await?;
```

### PostgreSQL

```rust
use tx2_query::postgres::PostgresBackend;

let backend = PostgresBackend::connect(
    "postgresql://user:password@localhost/database"
).await?;
```

### DuckDB

```rust
use tx2_query::duckdb::DuckDBBackend;

// In-memory database (perfect for analytics)
let backend = DuckDBBackend::memory().await?;

// File-based database
let backend = DuckDBBackend::file("analytics.duckdb").await?;

// Export to Parquet for external analysis
backend.export_parquet("Player", "players.parquet").await?;

// Import from Parquet
backend.import_parquet("Player", "players.parquet").await?;
```

## Query Builder

The query builder provides an ergonomic, type-safe way to construct SQL queries:

```rust
// SELECT with WHERE
let query = SelectBuilder::new("Player")
    .select(vec!["name", "score"])
    .where_eq("active", json!(true))
    .where_gt("score", json!(100))
    .build()?;

// Aggregations
let query = SelectBuilder::new("Player")
    .aggregate(AggregateFunc::Count, "*", Some("total"))
    .aggregate(AggregateFunc::Avg, "score", Some("avg_score"))
    .build()?;

// Ordering and pagination
let query = SelectBuilder::new("Player")
    .select_all()
    .order_desc("created_at")
    .limit(20)
    .offset(40)
    .build()?;

// Complex conditions
let query = SelectBuilder::new("Player")
    .select_all()
    .where_in("level", vec![json!(10), json!(20), json!(30)])
    .where_like("name", "%Alice%")
    .build()?;
```

## Schema Generation

Components automatically map to SQL tables:

```rust
#[derive(Debug)]
struct Player;

schema_gen.register::<Player>(
    "Player",  // Table name
    vec![
        ("name", SqlType::Text, false),      // NOT NULL
        ("score", SqlType::Integer, false),
        ("notes", SqlType::Text, true),       // NULL able
    ],
)?;
```

Generated DDL:

```sql
CREATE TABLE IF NOT EXISTS Player (
    entity_id BIGINT NOT NULL,
    name TEXT NOT NULL,
    score INTEGER NOT NULL,
    notes TEXT,
    _tx2_updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (entity_id)
);
```

## Synchronization

### Incremental Sync

Track changes as they happen in your ECS world:

```rust
// Entity created
sync.track_entity_created(entity_id).await?;

// Component added/modified
sync.track_component_change(
    entity_id,
    component_type_id,
    component_name,
    data,
).await?;

// Entity deleted
sync.track_entity_deleted(entity_id).await?;

// Flush changes to database
sync.flush().await?;
```

### Full Sync

Perform a full synchronization of all entities:

```rust
let entities = vec![
    (entity_id, type_id, "Player".to_string(), player_data),
    // ... more entities
];

sync.full_sync(entities).await?;
```

### Configuration

```rust
let config = SyncConfig {
    batch_size: 1000,              // Batch size for bulk operations
    background_sync: false,         // Sync in background
    use_transactions: false,        // Use transactions (see note below)
    max_retries: 3,                // Max retries on failure
};

let sync = QuerySync::with_config(backend, schema_gen, config);
```

**Note on Transactions**: Transaction support is limited when using connection pooling. For production use with proper transaction isolation, consider implementing connection affinity or using a dedicated transaction connection.

## SQL Types

Supported SQL types:

- `SqlType::BigInt` - 64-bit integer
- `SqlType::Integer` - 32-bit integer
- `SqlType::SmallInt` - 16-bit integer
- `SqlType::Real` - Single precision float
- `SqlType::DoublePrecision` - Double precision float
- `SqlType::Text` - Variable length text
- `SqlType::Boolean` - Boolean
- `SqlType::Timestamp` - Timestamp
- `SqlType::Json` - JSON/JSONB data
- `SqlType::Bytea` - Binary data

## Why DuckDB for Analytics?

DuckDB is optimized for OLAP workloads and provides several advantages:

- **Columnar Storage**: Efficient storage and query performance for analytics
- **Window Functions**: Advanced analytics with `ROW_NUMBER()`, `RANK()`, `LAG()`, etc.
- **Parquet Integration**: Export/import data in industry-standard format
- **No Server Required**: Embedded database, no separate process
- **Fast Aggregations**: Optimized for `GROUP BY`, `SUM()`, `AVG()`, etc.
- **Join Performance**: Excellent performance on complex joins

Example analytical query:

```rust
use tx2_query::duckdb::DuckDBBackend;
use tx2_query::prelude::*;

let mut backend = DuckDBBackend::memory().await?;
let sync = QuerySync::new(backend, schema_gen);

// Complex analytical query with window functions
let results = sync.query("
    SELECT
        player_name,
        score,
        AVG(score) OVER (PARTITION BY level) as avg_level_score,
        RANK() OVER (ORDER BY score DESC) as global_rank
    FROM Player
    WHERE active = true
").await?;
```

## Use Cases

### Analytics Dashboard

```rust
// Get player statistics
let query = SelectBuilder::new("Player")
    .aggregate(AggregateFunc::Count, "*", Some("total_players"))
    .aggregate(AggregateFunc::Avg, "score", Some("avg_score"))
    .aggregate(AggregateFunc::Max, "score", Some("high_score"))
    .build()?;
```

### Leaderboards

```rust
// Top 10 players
let query = SelectBuilder::new("Player")
    .select(vec!["name", "score", "level"])
    .where_eq("active", json!(true))
    .order_desc("score")
    .limit(10)
    .build()?;
```

### External Tools

Connect BI tools, data visualization platforms, or SQL clients directly to the SQL database for analysis without affecting your ECS world.

## Performance Tips

1. **Batch Operations**: Use appropriate `batch_size` to balance memory and performance
2. **Indexes**: Add indexes for frequently queried columns
3. **Selective Sync**: Only sync components needed for analytics
4. **Background Sync**: Enable background sync for non-blocking updates
5. **Clear Unused Data**: Periodically use `clear_all()` if full re-sync is needed

## Testing

```bash
# Run all tests
cargo test

# Run with specific backend
cargo test --features sqlite
cargo test --features postgres

# Run integration tests (requires database)
cargo test --test integration_test
```

## License

MIT

## Contributing

Contributions are welcome! Please open an issue or PR on GitHub.

## Roadmap

- [x] DuckDB backend for OLAP workloads
- [ ] ClickHouse backend for time-series analytics
- [ ] Historical queries via tx2-pack integration
- [ ] Materialized views support
- [ ] Query result caching
- [ ] Automatic index recommendations

## Related Crates

- **tx2-core**: Core ECS framework
- **tx2-link**: Serialization and messaging
- **tx2-pack**: Delta compression and time travel