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: 20 additions & 9 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

92 changes: 51 additions & 41 deletions src/sql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1007,6 +1007,7 @@ impl<'a> SqlEngine<'a> {
.write()
.unwrap()
.insert(table_name, (result.columns, result.rows));
self.store.try_flush_catalog_to_storage();
return Ok(QueryOutput {
columns: vec!["rows".to_string()],
rows: vec![vec![n.to_string()]],
Expand Down Expand Up @@ -1042,6 +1043,7 @@ impl<'a> SqlEngine<'a> {
.write()
.unwrap()
.insert(table_name, (columns, vec![]));
self.store.try_flush_catalog_to_storage();
Ok(QueryOutput {
columns: vec!["result".to_string()],
rows: vec![vec!["ok".to_string()]],
Expand Down Expand Up @@ -1086,36 +1088,40 @@ impl<'a> SqlEngine<'a> {
Some(indices?)
};

let mut guard = self.store.custom_tables.write().unwrap();
let (table_cols, table_rows) = guard
.get_mut(&table_name)
.ok_or_else(|| MqdbError::SqlExec(format!("unknown table: {table_name}")))?;
let ncols = table_cols.len();

let mut inserted = 0usize;
for src_row in &values_out.rows {
let mut row = vec![String::new(); ncols];
match &col_indices {
None => {
if src_row.len() != ncols {
return Err(MqdbError::SqlExec(format!(
"expected {ncols} columns, got {}",
src_row.len()
)));
let inserted = {
let mut guard = self.store.custom_tables.write().unwrap();
let (table_cols, table_rows) = guard
.get_mut(&table_name)
.ok_or_else(|| MqdbError::SqlExec(format!("unknown table: {table_name}")))?;
let ncols = table_cols.len();

let mut inserted = 0usize;
for src_row in &values_out.rows {
let mut row = vec![String::new(); ncols];
match &col_indices {
None => {
if src_row.len() != ncols {
return Err(MqdbError::SqlExec(format!(
"expected {ncols} columns, got {}",
src_row.len()
)));
}
row = src_row.clone();
}
row = src_row.clone();
}
Some(idx_map) => {
for (dst_idx, &src_idx) in idx_map.iter().enumerate() {
if let Some(v) = src_row.get(dst_idx) {
row[src_idx] = v.clone();
Some(idx_map) => {
for (dst_idx, &src_idx) in idx_map.iter().enumerate() {
if let Some(v) = src_row.get(dst_idx) {
row[src_idx] = v.clone();
}
}
}
}
table_rows.push(row);
inserted += 1;
}
table_rows.push(row);
inserted += 1;
}
inserted
}; // write lock released before flush
self.store.try_flush_catalog_to_storage();
Ok(QueryOutput {
columns: vec!["rows_affected".to_string()],
rows: vec![vec![inserted.to_string()]],
Expand All @@ -1127,23 +1133,27 @@ impl<'a> SqlEngine<'a> {
names: &[ObjectName],
if_exists: bool,
) -> Result<QueryOutput, MqdbError> {
let mut guard = self.store.custom_tables.write().unwrap();
let mut dropped = 0usize;
for name in names {
let table_name = name.0.last().map(ident_value).unwrap_or("").to_lowercase();
if matches!(table_name.as_str(), "blocks" | "documents") {
return Err(MqdbError::SqlExec(format!(
"cannot drop built-in table '{table_name}'"
)));
}
if guard.remove(&table_name).is_some() {
dropped += 1;
} else if !if_exists {
return Err(MqdbError::SqlExec(format!(
"table '{table_name}' does not exist"
)));
let dropped = {
let mut guard = self.store.custom_tables.write().unwrap();
let mut dropped = 0usize;
for name in names {
let table_name = name.0.last().map(ident_value).unwrap_or("").to_lowercase();
if matches!(table_name.as_str(), "blocks" | "documents") {
return Err(MqdbError::SqlExec(format!(
"cannot drop built-in table '{table_name}'"
)));
}
if guard.remove(&table_name).is_some() {
dropped += 1;
} else if !if_exists {
return Err(MqdbError::SqlExec(format!(
"table '{table_name}' does not exist"
)));
}
}
}
dropped
}; // write lock released before flush
self.store.try_flush_catalog_to_storage();
Ok(QueryOutput {
columns: vec!["result".to_string()],
rows: vec![vec![format!("{dropped} table(s) dropped")]],
Expand Down
60 changes: 54 additions & 6 deletions src/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use crate::{
document::Document,
error::MqdbError,
storage::{
catalog::{CatalogEntry, read_catalog, write_catalog},
catalog::{CatalogEntry, CustomTableEntry, read_catalog, write_catalog},
codec::{decode_block, encode_block},
page::{
PAGE_BODY_SIZE, PAGE_HEADER_SIZE, PAGE_TYPE_BLOCK_DATA, PAGE_TYPE_CATALOG,
Expand Down Expand Up @@ -146,12 +146,18 @@ impl Storage {
}

/// Save catalog (call after all write_document calls).
pub fn flush_catalog(&mut self, entries: &[CatalogEntry]) -> Result<(), MqdbError> {
write_catalog(&mut self.page_file, entries)
pub fn flush_catalog(
&mut self,
entries: &[CatalogEntry],
custom_tables: &[CustomTableEntry],
) -> Result<(), MqdbError> {
write_catalog(&mut self.page_file, entries, custom_tables)
}

/// Read the catalog.
pub fn load_catalog(&mut self) -> Result<Vec<CatalogEntry>, MqdbError> {
pub fn load_catalog(
&mut self,
) -> Result<(Vec<CatalogEntry>, Vec<CustomTableEntry>), MqdbError> {
read_catalog(&mut self.page_file)
}

Expand Down Expand Up @@ -392,11 +398,11 @@ mod tests {
zone_map_bytes: encode_zone_map(&document.zone_maps),
index_start_page: 0,
};
storage.flush_catalog(&[catalog_entry]).unwrap();
storage.flush_catalog(&[catalog_entry], &[]).unwrap();
drop(storage);

let mut reopened = Storage::open(&path).unwrap();
let catalog = reopened.load_catalog().unwrap();
let (catalog, _) = reopened.load_catalog().unwrap();
assert_eq!(catalog.len(), 1);
assert_eq!(
decode_zone_map(&catalog[0].zone_map_bytes).unwrap(),
Expand Down Expand Up @@ -511,6 +517,48 @@ mod tests {
assert_eq!(decoded, block);
}

#[test]
fn custom_table_round_trip() {
let path = test_file_path("custom-table-round-trip");
cleanup(&path);

let mut store = DocumentStore::new();
store.add_str("# Hello\n\nWorld\n").unwrap();
store.save(&path).unwrap();

// Open and CREATE TABLE + INSERT
let mut opened = DocumentStore::open(&path).unwrap();
opened.load_all_blocks().unwrap();
opened.load_all_indexes().unwrap();
let engine = crate::SqlEngine::new(&opened).unwrap();
engine
.execute("CREATE TABLE notes (id TEXT, body TEXT)")
.unwrap();
engine
.execute("INSERT INTO notes VALUES ('1', 'hello')")
.unwrap();
engine
.execute("INSERT INTO notes VALUES ('2', 'world')")
.unwrap();
drop(engine);
drop(opened);

// Re-open and verify tables persisted
let mut reopened = DocumentStore::open(&path).unwrap();
reopened.load_all_blocks().unwrap();
reopened.load_all_indexes().unwrap();
let engine2 = crate::SqlEngine::new(&reopened).unwrap();
let out = engine2
.execute("SELECT body FROM notes WHERE id = '1'")
.unwrap();
assert_eq!(out.rows.len(), 1);
assert_eq!(out.rows[0][0], "hello");
let all = engine2.execute("SELECT * FROM notes").unwrap();
assert_eq!(all.rows.len(), 2);

cleanup(&path);
}

#[rstest]
#[case(Some("My Title"))]
#[case(None)]
Expand Down
Loading
Loading