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
112 changes: 88 additions & 24 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ shift-backends = { path = "crates/shift-backends" }
shift-bridge = { path = "crates/shift-bridge" }
shift-edit = { path = "crates/shift-edit" }
shift-ir = { path = "crates/shift-ir" }
shift-store = { path = "crates/shift-store" }
11 changes: 11 additions & 0 deletions crates/shift-store/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[package]
name = "shift-store"
version = "0.1.0"
edition = "2024"

[dependencies]
rusqlite = { version = "0.37.0", features = ["blob", "backup", "limits"] }
thiserror = "2"

[dev-dependencies]
tempfile = "3"
40 changes: 40 additions & 0 deletions crates/shift-store/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# shift-store

`shift-store` owns Shift's durable source database.

The crate provides the storage boundary between the rest of the Rust application and SQLite. Callers should use typed store APIs exposed by this crate instead of preparing SQL statements or opening database connections directly.

## Responsibilities

- open and configure the SQLite connection;
- create and update the database schema;
- expose typed Rust APIs for source-store reads and writes;
- keep raw SQL local to this crate;
- preserve stable internal IDs for source entities;
- separate canonical source data from derived/index data as the store grows.

## Design Goals

- incremental writes;
- durability and crash-safe recovery;
- stable IDs;
- performant reads and writes for large fonts;
- clear handling of CJK-scale glyph inventories;
- efficient queries for references to and from source entities;
- support for components, kerning, and feature-related source data;
- clear separation between canonical source data and derived/index data;
- import/export support through typed store APIs rather than ad hoc SQL access.

## Shape

```text
src/
connection.rs # opening and configuring SQLite
error.rs # store error type
glyph.rs # glyph persistence API
schema.rs # schema creation and versioning entry point
store.rs # ShiftStore connection owner
types.rs # storage-facing IDs and small value types
```

Tests live in `tests/store_test.rs` and should start as small persistence checks before broader integration tests are added.
97 changes: 97 additions & 0 deletions crates/shift-store/src/component.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
use crate::{ComponentId, GlyphId, LayerId, ShiftStore, StoreError};

pub struct NewGlyphComponent {
pub id: ComponentId,
pub layer_id: LayerId,
pub base_glyph_id: GlyphId,
pub order_index: i64,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GlyphComponentRecord {
pub id: ComponentId,
pub layer_id: LayerId,
pub base_glyph_id: GlyphId,
pub order_index: i64,
}

impl ShiftStore {
pub fn create_glyph_component(
&mut self,
component: NewGlyphComponent,
) -> Result<(), StoreError> {
self.conn.execute(
"
INSERT INTO glyph_components (
id,
layer_id,
base_glyph_id,
order_index
)
VALUES (?1, ?2, ?3, ?4)
",
rusqlite::params![
component.id.as_str(),
component.layer_id.as_str(),
component.base_glyph_id.as_str(),
component.order_index,
],
)?;

Ok(())
}

pub fn get_glyph_component(
&self,
id: &ComponentId,
) -> Result<Option<GlyphComponentRecord>, StoreError> {
let mut stmt = self.conn.prepare(
"
SELECT
id,
layer_id,
base_glyph_id,
order_index
FROM glyph_components
WHERE id = ?1
",
)?;

match stmt.query_row([id.as_str()], map_glyph_component_row) {
Ok(component) => Ok(Some(component)),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(err) => Err(err.into()),
}
}

pub fn list_glyph_components_for_layer(
&self,
layer_id: &LayerId,
) -> Result<Vec<GlyphComponentRecord>, StoreError> {
let mut stmt = self.conn.prepare(
"
SELECT
id,
layer_id,
base_glyph_id,
order_index
FROM glyph_components
WHERE layer_id = ?1
ORDER BY order_index, id
",
)?;

let rows = stmt.query_map([layer_id.as_str()], map_glyph_component_row)?;
rows.collect::<Result<Vec<_>, _>>()
.map_err(StoreError::from)
}
}

fn map_glyph_component_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<GlyphComponentRecord> {
Ok(GlyphComponentRecord {
id: ComponentId::new(row.get::<_, String>(0)?),
layer_id: LayerId::new(row.get::<_, String>(1)?),
base_glyph_id: GlyphId::new(row.get::<_, String>(2)?),
order_index: row.get(3)?,
})
}
25 changes: 25 additions & 0 deletions crates/shift-store/src/connection.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
use std::path::Path;

use crate::{ShiftStore, StoreError, schema};

impl ShiftStore {
pub fn open(path: impl AsRef<Path>) -> Result<Self, StoreError> {
let conn = rusqlite::Connection::open(path)?;
configure_connection(&conn)?;
schema::ensure_current(&conn)?;
Ok(Self { conn })
}

pub fn open_memory_for_test() -> Result<Self, StoreError> {
let conn = rusqlite::Connection::open_in_memory()?;
configure_connection(&conn)?;
schema::ensure_current(&conn)?;
Ok(Self { conn })
}
}

fn configure_connection(conn: &rusqlite::Connection) -> Result<(), StoreError> {
conn.pragma_update(None, "foreign_keys", "ON")?;
conn.busy_timeout(std::time::Duration::from_millis(5_000))?;
Ok(())
}
Loading
Loading