Skip to content

Commit 8dbc294

Browse files
committed
libsql: attach databases from other namespaces as readonly
With this proof-of-concept patch, other namespaces hosted on the same sqld machine can now be attached in readonly mode, so that users can read from other databases when connected to a particular one. TODO: * hide it as an opt-in flag: abuse risk
1 parent 47ec93e commit 8dbc294

File tree

2 files changed

+47
-3
lines changed

2 files changed

+47
-3
lines changed

libsql-server/src/connection/libsql.rs

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ impl<W: Wal> WrapWal<W> for InhibitCheckpointWalWrapper {
180180
_buf: &mut [u8],
181181
) -> libsql_sys::wal::Result<(u32, u32)> {
182182
tracing::warn!(
183-
"chackpoint inhibited: this connection is not allowed to perform checkpoints"
183+
"checkpoint inhibited: this connection is not allowed to perform checkpoints"
184184
);
185185
Err(rusqlite::ffi::Error::new(SQLITE_BUSY))
186186
}
@@ -276,6 +276,7 @@ where
276276
)?;
277277
conn.conn
278278
.pragma_update(None, "max_page_count", max_db_size)?;
279+
279280
Ok(conn)
280281
})
281282
.await
@@ -692,12 +693,32 @@ impl<W: Wal> Connection<W> {
692693
StmtKind::Read | StmtKind::TxnBegin | StmtKind::Other => config.block_reads,
693694
StmtKind::Write => config.block_reads || config.block_writes,
694695
StmtKind::TxnEnd | StmtKind::Release | StmtKind::Savepoint => false,
696+
StmtKind::Attach | StmtKind::Detach => false,
695697
};
696698
if blocked {
697699
return Err(Error::Blocked(config.block_reason.clone()));
698700
}
699701

700-
let mut stmt = self.conn.prepare(&query.stmt.stmt)?;
702+
let mut stmt = if matches!(query.stmt.kind, StmtKind::Attach) {
703+
let attached_db_name: &str = query.stmt.id.as_deref().unwrap_or("");
704+
let path = PathBuf::from(self.conn.path().unwrap_or("."));
705+
let dbs_path = path
706+
.parent()
707+
.unwrap_or_else(|| std::path::Path::new(".."))
708+
.parent()
709+
.unwrap_or_else(|| std::path::Path::new(".."))
710+
.canonicalize()
711+
.unwrap_or_else(|_| std::path::PathBuf::from(".."));
712+
let query = format!(
713+
"ATTACH DATABASE 'file:{}?mode=ro' AS \"{attached_db_name}\"",
714+
dbs_path.join(attached_db_name).join("data").display()
715+
);
716+
tracing::trace!("ATTACH rewritten to: {query}");
717+
self.conn.prepare(&query)?
718+
} else {
719+
self.conn.prepare(&query.stmt.stmt)?
720+
};
721+
701722
if stmt.readonly() {
702723
READ_QUERY_COUNT.increment(1);
703724
} else {

libsql-server/src/query_analysis.rs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use anyhow::Result;
22
use fallible_iterator::FallibleIterator;
3-
use sqlite3_parser::ast::{Cmd, PragmaBody, QualifiedName, Stmt};
3+
use sqlite3_parser::ast::{Cmd, Expr, Id, PragmaBody, QualifiedName, Stmt};
44
use sqlite3_parser::lexer::sql::{Parser, ParserError};
55

66
/// A group of statements to be executed together.
@@ -11,6 +11,8 @@ pub struct Statement {
1111
/// Is the statement an INSERT, UPDATE or DELETE?
1212
pub is_iud: bool,
1313
pub is_insert: bool,
14+
// Optional id associated with the statement (used for attach/detach)
15+
pub id: Option<String>,
1416
}
1517

1618
impl Default for Statement {
@@ -30,6 +32,8 @@ pub enum StmtKind {
3032
Write,
3133
Savepoint,
3234
Release,
35+
Attach,
36+
Detach,
3337
Other,
3438
}
3539

@@ -113,6 +117,12 @@ impl StmtKind {
113117
savepoint_name: Some(_),
114118
..
115119
}) => Some(Self::Release),
120+
Cmd::Stmt(Stmt::Attach {
121+
expr: Expr::Id(Id(expr)),
122+
db_name: Expr::Id(Id(name)),
123+
..
124+
}) if expr == name => Some(Self::Attach),
125+
Cmd::Stmt(Stmt::Detach(_)) => Some(Self::Detach),
116126
_ => None,
117127
}
118128
}
@@ -237,6 +247,7 @@ impl Statement {
237247
kind: StmtKind::Read,
238248
is_iud: false,
239249
is_insert: false,
250+
id: None,
240251
}
241252
}
242253

@@ -258,6 +269,7 @@ impl Statement {
258269
kind,
259270
is_iud: false,
260271
is_insert: false,
272+
id: None,
261273
});
262274
}
263275
}
@@ -268,11 +280,22 @@ impl Statement {
268280
);
269281
let is_insert = matches!(c, Cmd::Stmt(Stmt::Insert { .. }));
270282

283+
let id = match &c {
284+
Cmd::Stmt(Stmt::Attach {
285+
expr: Expr::Id(Id(expr)),
286+
db_name: Expr::Id(Id(name)),
287+
..
288+
}) if expr == name => Some(name.clone()),
289+
Cmd::Stmt(Stmt::Detach(Expr::Id(Id(expr)))) => Some(expr.clone()),
290+
_ => None,
291+
};
292+
271293
Ok(Statement {
272294
stmt: c.to_string(),
273295
kind,
274296
is_iud,
275297
is_insert,
298+
id,
276299
})
277300
}
278301
// The parser needs to be boxed because it's large, and you don't want it on the stack.

0 commit comments

Comments
 (0)