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
7 changes: 7 additions & 0 deletions packages/python/e2e-attestations/bot-1534-fm-native-1016.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"command": "uv run python -m pytest tests/e2e/frontmatter_parse_speed_test.py tests/e2e/jsonl_parse_speed_test.py tests/e2e/on_file_query_test.py tests/e2e/fanout_test.py tests/e2e/declared_table_name_test.py tests/e2e/query_contract_test.py tests/e2e/use_case_notebook_test.py tests/e2e/use_case_transcripts_test.py tests/e2e/sigint_interrupt_test.py -q -s",
"ran_at": 1792158472,
"exit_code": 0,
"commit": "4eaa7f0c26fa324fdb9b161e90fdb3fe22b8774f",
"branch": "bot/1534-fm-native-1016"
}
3 changes: 3 additions & 0 deletions packages/rust/changelog.d/2026-10-16-shape-beside-insert.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
**Changed**

- **A per-table `on-file` hook's rows are shaped on their own thread while the previous chunk is inserted.** Startup over a large tree, such as a frontmatter table over 131k notes, finishes about 4% sooner. Query results are unchanged.
25 changes: 25 additions & 0 deletions packages/rust/migrations.d/2026-10-16-shape-beside-insert.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
### Shape beside insert

#### Summary

Startup shapes a per-table hook's rows on a separate thread from the one inserting them. No public API changes.

#### Required changes

_None._

#### Deprecations removed

_None._

#### Behavior changes without code changes

_None._

#### Verification

```bash
dirsql query "SELECT status, count(*) FROM notes GROUP BY status" -c .dirsql.toml
```

Prints the same rows as before, sooner.
122 changes: 71 additions & 51 deletions packages/rust/src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,63 @@ impl ShapedRows {
}
}

/// [`Db::shape_rows`] against `columns`, the table's columns in DDL order,
/// so rows can be shaped off the connection's thread.
pub(crate) fn shape_json_rows(
table: &str,
columns: Vec<String>,
rows: Vec<JsonRow>,
strict: bool,
) -> Result<ShapedRows> {
let mut cells = Vec::with_capacity(rows.len());
for row in rows {
if strict {
for (key, _) in &row.0 {
validate_identifier(key)?;
}
let extra: Vec<&str> = row
.0
.iter()
.map(|(key, _)| &**key)
.filter(|key| !columns.iter().any(|c| c == key))
.collect();
if !extra.is_empty() {
return Err(DbError::SchemaMismatch(format!(
"extra columns not in table {}: {}",
table,
extra.join(", ")
)));
}
let missing: Vec<&str> = columns
.iter()
.map(String::as_str)
.filter(|c| !row.0.iter().any(|(key, _)| **key == **c))
.collect();
if !missing.is_empty() {
return Err(DbError::SchemaMismatch(format!(
"missing columns for table {}: {}",
table,
missing.join(", ")
)));
}
}
let in_order = row.0.len() == columns.len()
&& row.0.iter().zip(&columns).all(|((key, _), c)| **key == **c);
if in_order {
cells.push(row.0.into_iter().map(|(_, v)| json_into_value(v)).collect());
continue;
}
let mut shaped = vec![Value::Null; columns.len()];
for (key, value) in row.0 {
if let Some(i) = columns.iter().position(|c| **c == *key) {
shaped[i] = json_into_value(value);
}
}
cells.push(shaped);
}
Ok(ShapedRows { columns, cells })
}

fn json_into_value(value: serde_json::Value) -> Value {
match value {
serde_json::Value::String(s) => Value::Text(s),
Expand Down Expand Up @@ -566,9 +623,10 @@ impl Db {
/// Return the column names declared in `table`'s DDL.
pub fn get_table_columns(&self, table: &str) -> Result<Vec<String>> {
validate_identifier(table)?;
let mut stmt = self
.conn
.prepare(&format!("PRAGMA table_info({})", table))?;
let mut stmt = self.conn.prepare(&format!(
"PRAGMA table_info({})",
crate::sql_literal::quote_identifier(table)
))?;
let columns: Vec<String> = stmt
.query_map([], |row| row.get::<_, String>(1))?
.filter_map(|r| r.ok())
Expand Down Expand Up @@ -714,54 +772,7 @@ impl Db {
/// a less-actionable "extra columns" mismatch. Every row is shaped before
/// any is returned, so a hook that got one row wrong fails as a unit.
pub fn shape_rows(&self, table: &str, rows: Vec<JsonRow>, strict: bool) -> Result<ShapedRows> {
let columns = self.get_table_columns(table)?;
let mut cells = Vec::with_capacity(rows.len());
for row in rows {
if strict {
for (key, _) in &row.0 {
validate_identifier(key)?;
}
let extra: Vec<&str> = row
.0
.iter()
.map(|(key, _)| &**key)
.filter(|key| !columns.iter().any(|c| c == key))
.collect();
if !extra.is_empty() {
return Err(DbError::SchemaMismatch(format!(
"extra columns not in table {}: {}",
table,
extra.join(", ")
)));
}
let missing: Vec<&str> = columns
.iter()
.map(String::as_str)
.filter(|c| !row.0.iter().any(|(key, _)| **key == **c))
.collect();
if !missing.is_empty() {
return Err(DbError::SchemaMismatch(format!(
"missing columns for table {}: {}",
table,
missing.join(", ")
)));
}
}
let in_order = row.0.len() == columns.len()
&& row.0.iter().zip(&columns).all(|((key, _), c)| **key == **c);
if in_order {
cells.push(row.0.into_iter().map(|(_, v)| json_into_value(v)).collect());
continue;
}
let mut shaped = vec![Value::Null; columns.len()];
for (key, value) in row.0 {
if let Some(i) = columns.iter().position(|c| **c == *key) {
shaped[i] = json_into_value(value);
}
}
cells.push(shaped);
}
Ok(ShapedRows { columns, cells })
shape_json_rows(table, self.get_table_columns(table)?, rows, strict)
}

/// Insert every row of `rows` under `file_path`, in order, through one
Expand Down Expand Up @@ -2476,6 +2487,15 @@ mod tests {
assert!(result.is_err());
}

#[test]
fn get_table_columns_reads_a_table_named_by_a_keyword() {
let db = Db::new().unwrap();
db.create_table("order", "CREATE TABLE \"order\" (name TEXT)")
.unwrap();
let cols = db.get_table_columns("order").unwrap();
assert!(cols.contains(&"name".to_string()), "{cols:?}");
}

#[test]
fn get_table_columns_nonexistent_table_returns_empty() {
let db = Db::new().unwrap();
Expand Down
Loading
Loading