| Crates.io | tx2-query |
| lib.rs | tx2-query |
| version | 0.1.2 |
| created_at | 2025-11-27 20:24:09.812424+00 |
| updated_at | 2025-11-28 22:41:01.829583+00 |
| description | SQL analytics layer for TX-2 ECS - one-way synchronization from ECS to SQL databases |
| homepage | https://github.com/IreGaddr/tx2-query |
| repository | https://github.com/IreGaddr/tx2-query |
| max_upload_size | |
| id | 1954380 |
| size | 197,854 |
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.
tx2-query provides a SQL side-car database for TX-2 applications. It's designed as an analytics layer, NOT an ORM:
Add to your Cargo.toml:
[dependencies]
tx2-query = { version = "0.1", features = ["postgres", "sqlite", "duckdb"] }
tokio = { version = "1.0", features = ["full"] }
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(())
}
use tx2_query::sqlite::SqliteBackend;
// In-memory database
let backend = SqliteBackend::memory().await?;
// File-based database
let backend = SqliteBackend::file("mydb.sqlite").await?;
use tx2_query::postgres::PostgresBackend;
let backend = PostgresBackend::connect(
"postgresql://user:password@localhost/database"
).await?;
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?;
The query builder provides an ergonomic, type-safe way to construct SQL queries:
// 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()?;
Components automatically map to SQL tables:
#[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:
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)
);
Track changes as they happen in your ECS world:
// 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?;
Perform a full synchronization of all entities:
let entities = vec![
(entity_id, type_id, "Player".to_string(), player_data),
// ... more entities
];
sync.full_sync(entities).await?;
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.
Supported SQL types:
SqlType::BigInt - 64-bit integerSqlType::Integer - 32-bit integerSqlType::SmallInt - 16-bit integerSqlType::Real - Single precision floatSqlType::DoublePrecision - Double precision floatSqlType::Text - Variable length textSqlType::Boolean - BooleanSqlType::Timestamp - TimestampSqlType::Json - JSON/JSONB dataSqlType::Bytea - Binary dataDuckDB is optimized for OLAP workloads and provides several advantages:
ROW_NUMBER(), RANK(), LAG(), etc.GROUP BY, SUM(), AVG(), etc.Example analytical query:
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?;
// 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()?;
// Top 10 players
let query = SelectBuilder::new("Player")
.select(vec!["name", "score", "level"])
.where_eq("active", json!(true))
.order_desc("score")
.limit(10)
.build()?;
Connect BI tools, data visualization platforms, or SQL clients directly to the SQL database for analysis without affecting your ECS world.
batch_size to balance memory and performanceclear_all() if full re-sync is needed# 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
MIT
Contributions are welcome! Please open an issue or PR on GitHub.