Skip to content

Commit 818657f

Browse files
feat(cache): wire query cache into reads and invalidation
1 parent f23cbd1 commit 818657f

25 files changed

Lines changed: 1754 additions & 28 deletions

File tree

Cargo.lock

Lines changed: 50 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,9 @@ starknet-core = "0.16.0"
179179

180180

181181
dashmap = "6.1.0"
182+
redis = { version = "0.25", features = ["tokio-comp", "connection-manager"] }
183+
sha2 = "0.10"
184+
bincode = "1.3"
182185

183186
# [patch.crates-io]
184187
# cainome = { git = "https://github.com/Larkooo/cainome", branch = "patch-1" }

crates/cache/Cargo.toml

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,25 @@ edition.workspace = true
44
repository.workspace = true
55
version.workspace = true
66

7+
[features]
8+
default = []
9+
redis = ["dep:redis"]
10+
711
[dependencies]
812
async-trait.workspace = true
13+
bincode.workspace = true
914
dashmap.workspace = true
1015
dojo-types.workspace = true
1116
dojo-world.workspace = true
17+
redis = { workspace = true, optional = true }
18+
serde.workspace = true
19+
serde_json.workspace = true
20+
sha2.workspace = true
1221
sqlx.workspace = true
1322
starknet.workspace = true
1423
thiserror.workspace = true
1524
tokio.workspace = true
1625
torii-math.workspace = true
26+
torii-proto.workspace = true
1727
torii-sqlite-types.workspace = true
18-
serde_json.workspace = true
1928
torii-storage.workspace = true
20-
torii-proto.workspace = true

crates/cache/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ use torii_storage::ReadOnlyStorage;
1818
use crate::error::Error;
1919

2020
pub mod error;
21+
pub mod query_cache;
2122

2223
pub type CacheError = Error;
2324

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
use serde::{Deserialize, Serialize};
2+
use sqlx::sqlite::SqliteRow;
3+
use sqlx::{Column, Row, TypeInfo};
4+
5+
/// A serializable representation of a SQLite row.
6+
#[derive(Debug, Clone, Serialize, Deserialize)]
7+
pub struct CachedRow {
8+
/// Column metadata.
9+
pub columns: Vec<CachedColumn>,
10+
/// Row values.
11+
pub values: Vec<CachedValue>,
12+
}
13+
14+
/// Column metadata for cached rows.
15+
#[derive(Debug, Clone, Serialize, Deserialize)]
16+
pub struct CachedColumn {
17+
/// Column name.
18+
pub name: String,
19+
/// SQLite type name.
20+
pub type_name: String,
21+
}
22+
23+
/// Cached value types matching SQLite's type affinity.
24+
#[derive(Debug, Clone, Serialize, Deserialize)]
25+
pub enum CachedValue {
26+
/// NULL value.
27+
Null,
28+
/// INTEGER value (i64).
29+
Integer(i64),
30+
/// REAL value (f64).
31+
Real(f64),
32+
/// TEXT value.
33+
Text(String),
34+
/// BLOB value.
35+
Blob(Vec<u8>),
36+
}
37+
38+
impl CachedRow {
39+
/// Create a CachedRow from a SQLite row.
40+
pub fn from_sqlite_row(row: &SqliteRow) -> Self {
41+
let columns: Vec<CachedColumn> = row
42+
.columns()
43+
.iter()
44+
.map(|c| CachedColumn {
45+
name: c.name().to_string(),
46+
type_name: c.type_info().name().to_string(),
47+
})
48+
.collect();
49+
50+
let values: Vec<CachedValue> = (0..columns.len())
51+
.map(|i| {
52+
// Try each type in order based on SQLite type affinity
53+
if let Ok(v) = row.try_get::<Option<i64>, _>(i) {
54+
match v {
55+
Some(n) => CachedValue::Integer(n),
56+
None => CachedValue::Null,
57+
}
58+
} else if let Ok(v) = row.try_get::<Option<f64>, _>(i) {
59+
match v {
60+
Some(n) => CachedValue::Real(n),
61+
None => CachedValue::Null,
62+
}
63+
} else if let Ok(v) = row.try_get::<Option<String>, _>(i) {
64+
match v {
65+
Some(s) => CachedValue::Text(s),
66+
None => CachedValue::Null,
67+
}
68+
} else if let Ok(v) = row.try_get::<Option<Vec<u8>>, _>(i) {
69+
match v {
70+
Some(b) => CachedValue::Blob(b),
71+
None => CachedValue::Null,
72+
}
73+
} else {
74+
CachedValue::Null
75+
}
76+
})
77+
.collect();
78+
79+
Self { columns, values }
80+
}
81+
82+
/// Get a value by column name.
83+
pub fn get(&self, column_name: &str) -> Option<&CachedValue> {
84+
let idx = self.columns.iter().position(|c| c.name == column_name)?;
85+
self.values.get(idx)
86+
}
87+
88+
/// Get an integer value by column name.
89+
pub fn get_i64(&self, column_name: &str) -> Option<i64> {
90+
match self.get(column_name)? {
91+
CachedValue::Integer(v) => Some(*v),
92+
_ => None,
93+
}
94+
}
95+
96+
/// Get a string value by column name.
97+
pub fn get_string(&self, column_name: &str) -> Option<&str> {
98+
match self.get(column_name)? {
99+
CachedValue::Text(v) => Some(v),
100+
_ => None,
101+
}
102+
}
103+
104+
/// Get a blob value by column name.
105+
pub fn get_blob(&self, column_name: &str) -> Option<&[u8]> {
106+
match self.get(column_name)? {
107+
CachedValue::Blob(v) => Some(v),
108+
_ => None,
109+
}
110+
}
111+
}
112+
113+
impl CachedValue {
114+
/// Check if the value is null.
115+
pub fn is_null(&self) -> bool {
116+
matches!(self, CachedValue::Null)
117+
}
118+
119+
/// Convert to i64 if possible.
120+
pub fn as_i64(&self) -> Option<i64> {
121+
match self {
122+
CachedValue::Integer(v) => Some(*v),
123+
_ => None,
124+
}
125+
}
126+
127+
/// Convert to f64 if possible.
128+
pub fn as_f64(&self) -> Option<f64> {
129+
match self {
130+
CachedValue::Real(v) => Some(*v),
131+
CachedValue::Integer(v) => Some(*v as f64),
132+
_ => None,
133+
}
134+
}
135+
136+
/// Convert to string if possible.
137+
pub fn as_str(&self) -> Option<&str> {
138+
match self {
139+
CachedValue::Text(v) => Some(v),
140+
_ => None,
141+
}
142+
}
143+
144+
/// Convert to bytes if possible.
145+
pub fn as_bytes(&self) -> Option<&[u8]> {
146+
match self {
147+
CachedValue::Blob(v) => Some(v),
148+
_ => None,
149+
}
150+
}
151+
}
152+
153+
/// A cached page of query results with optional cursor for pagination.
154+
#[derive(Debug, Clone, Serialize, Deserialize)]
155+
pub struct CachedPage {
156+
/// The rows in this page.
157+
pub rows: Vec<CachedRow>,
158+
/// Optional cursor for the next page.
159+
pub next_cursor: Option<String>,
160+
}
161+
162+
impl CachedPage {
163+
/// Create a new cached page.
164+
pub fn new(rows: Vec<CachedRow>, next_cursor: Option<String>) -> Self {
165+
Self { rows, next_cursor }
166+
}
167+
168+
/// Check if this page is empty.
169+
pub fn is_empty(&self) -> bool {
170+
self.rows.is_empty()
171+
}
172+
173+
/// Get the number of rows in this page.
174+
pub fn len(&self) -> usize {
175+
self.rows.len()
176+
}
177+
}

0 commit comments

Comments
 (0)