Skip to content

Commit e47cec8

Browse files
committed
Add database foundation for MetadataService (Phase 1)
Implement the database layer for list metadata storage, replacing the in-memory KV store. This enables proper observer patterns and persistence between app launches. Database schema (3 tables): - list_metadata: headers with pagination, version for concurrency control - list_metadata_items: ordered entity IDs (rowid = display order) - list_metadata_state: sync state (idle, fetching_first_page, fetching_next_page, error) Changes: - Add DbTable variants: ListMetadata, ListMetadataItems, ListMetadataState - Add migration 0007-create-list-metadata-tables.sql - Add list_metadata module with DbListMetadata, DbListMetadataItem, DbListMetadataState structs and ListState enum - Add db_types/db_list_metadata.rs with column enums and from_row impls - Add repository/list_metadata.rs with read and write operations
1 parent 51a9c5e commit e47cec8

File tree

7 files changed

+1062
-1
lines changed

7 files changed

+1062
-1
lines changed
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
-- Table 1: List header/pagination info
2+
CREATE TABLE `list_metadata` (
3+
`rowid` INTEGER PRIMARY KEY AUTOINCREMENT,
4+
`db_site_id` INTEGER NOT NULL,
5+
`key` TEXT NOT NULL, -- e.g., "edit:posts:publish"
6+
`total_pages` INTEGER,
7+
`total_items` INTEGER,
8+
`current_page` INTEGER NOT NULL DEFAULT 0,
9+
`per_page` INTEGER NOT NULL DEFAULT 20,
10+
`last_first_page_fetched_at` TEXT,
11+
`last_updated_at` TEXT,
12+
`version` INTEGER NOT NULL DEFAULT 0,
13+
14+
FOREIGN KEY (db_site_id) REFERENCES db_sites(id) ON DELETE CASCADE
15+
) STRICT;
16+
17+
CREATE UNIQUE INDEX idx_list_metadata_unique_key ON list_metadata(db_site_id, key);
18+
19+
-- Table 2: List items (rowid = insertion order = display order)
20+
CREATE TABLE `list_metadata_items` (
21+
`rowid` INTEGER PRIMARY KEY AUTOINCREMENT,
22+
`db_site_id` INTEGER NOT NULL,
23+
`key` TEXT NOT NULL,
24+
`entity_id` INTEGER NOT NULL, -- post/comment/etc ID
25+
`modified_gmt` TEXT, -- nullable for entities without it
26+
27+
FOREIGN KEY (db_site_id) REFERENCES db_sites(id) ON DELETE CASCADE
28+
) STRICT;
29+
30+
CREATE INDEX idx_list_metadata_items_key ON list_metadata_items(db_site_id, key);
31+
CREATE INDEX idx_list_metadata_items_entity ON list_metadata_items(db_site_id, entity_id);
32+
33+
-- Table 3: Sync state (FK to list_metadata, not duplicating key)
34+
CREATE TABLE `list_metadata_state` (
35+
`rowid` INTEGER PRIMARY KEY AUTOINCREMENT,
36+
`list_metadata_id` INTEGER NOT NULL,
37+
`state` TEXT NOT NULL DEFAULT 'idle', -- idle, fetching_first_page, fetching_next_page, error
38+
`error_message` TEXT,
39+
`updated_at` TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
40+
41+
FOREIGN KEY (list_metadata_id) REFERENCES list_metadata(rowid) ON DELETE CASCADE
42+
) STRICT;
43+
44+
CREATE UNIQUE INDEX idx_list_metadata_state_unique ON list_metadata_state(list_metadata_id);

wp_mobile_cache/src/db_types.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
pub mod db_list_metadata;
12
pub mod db_site;
23
pub mod db_term_relationship;
34
pub mod helpers;
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
use crate::{
2+
SqliteDbError,
3+
db_types::row_ext::{ColumnIndex, RowExt},
4+
list_metadata::{DbListMetadata, DbListMetadataItem, DbListMetadataState, ListState},
5+
};
6+
use rusqlite::Row;
7+
8+
/// Column indexes for list_metadata table.
9+
/// These must match the order of columns in the CREATE TABLE statement.
10+
#[repr(usize)]
11+
#[derive(Debug, Clone, Copy)]
12+
pub enum ListMetadataColumn {
13+
Rowid = 0,
14+
DbSiteId = 1,
15+
Key = 2,
16+
TotalPages = 3,
17+
TotalItems = 4,
18+
CurrentPage = 5,
19+
PerPage = 6,
20+
LastFirstPageFetchedAt = 7,
21+
LastUpdatedAt = 8,
22+
Version = 9,
23+
}
24+
25+
impl ColumnIndex for ListMetadataColumn {
26+
fn as_index(&self) -> usize {
27+
*self as usize
28+
}
29+
}
30+
31+
impl DbListMetadata {
32+
/// Construct a list metadata entity from a database row.
33+
pub fn from_row(row: &Row) -> Result<Self, SqliteDbError> {
34+
use ListMetadataColumn as Col;
35+
36+
Ok(Self {
37+
row_id: row.get_column(Col::Rowid)?,
38+
db_site_id: row.get_column(Col::DbSiteId)?,
39+
key: row.get_column(Col::Key)?,
40+
total_pages: row.get_column(Col::TotalPages)?,
41+
total_items: row.get_column(Col::TotalItems)?,
42+
current_page: row.get_column(Col::CurrentPage)?,
43+
per_page: row.get_column(Col::PerPage)?,
44+
last_first_page_fetched_at: row.get_column(Col::LastFirstPageFetchedAt)?,
45+
last_updated_at: row.get_column(Col::LastUpdatedAt)?,
46+
version: row.get_column(Col::Version)?,
47+
})
48+
}
49+
}
50+
51+
/// Column indexes for list_metadata_items table.
52+
/// These must match the order of columns in the CREATE TABLE statement.
53+
#[repr(usize)]
54+
#[derive(Debug, Clone, Copy)]
55+
pub enum ListMetadataItemColumn {
56+
Rowid = 0,
57+
DbSiteId = 1,
58+
Key = 2,
59+
EntityId = 3,
60+
ModifiedGmt = 4,
61+
}
62+
63+
impl ColumnIndex for ListMetadataItemColumn {
64+
fn as_index(&self) -> usize {
65+
*self as usize
66+
}
67+
}
68+
69+
impl DbListMetadataItem {
70+
/// Construct a list metadata item from a database row.
71+
pub fn from_row(row: &Row) -> Result<Self, SqliteDbError> {
72+
use ListMetadataItemColumn as Col;
73+
74+
Ok(Self {
75+
row_id: row.get_column(Col::Rowid)?,
76+
db_site_id: row.get_column(Col::DbSiteId)?,
77+
key: row.get_column(Col::Key)?,
78+
entity_id: row.get_column(Col::EntityId)?,
79+
modified_gmt: row.get_column(Col::ModifiedGmt)?,
80+
})
81+
}
82+
}
83+
84+
/// Column indexes for list_metadata_state table.
85+
/// These must match the order of columns in the CREATE TABLE statement.
86+
#[repr(usize)]
87+
#[derive(Debug, Clone, Copy)]
88+
pub enum ListMetadataStateColumn {
89+
Rowid = 0,
90+
ListMetadataId = 1,
91+
State = 2,
92+
ErrorMessage = 3,
93+
UpdatedAt = 4,
94+
}
95+
96+
impl ColumnIndex for ListMetadataStateColumn {
97+
fn as_index(&self) -> usize {
98+
*self as usize
99+
}
100+
}
101+
102+
impl DbListMetadataState {
103+
/// Construct a list metadata state from a database row.
104+
pub fn from_row(row: &Row) -> Result<Self, SqliteDbError> {
105+
use ListMetadataStateColumn as Col;
106+
107+
let state_str: String = row.get_column(Col::State)?;
108+
109+
Ok(Self {
110+
row_id: row.get_column(Col::Rowid)?,
111+
list_metadata_id: row.get_column(Col::ListMetadataId)?,
112+
state: ListState::from(state_str),
113+
error_message: row.get_column(Col::ErrorMessage)?,
114+
updated_at: row.get_column(Col::UpdatedAt)?,
115+
})
116+
}
117+
}

wp_mobile_cache/src/lib.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use std::sync::Mutex;
66
pub mod context;
77
pub mod db_types;
88
pub mod entity;
9+
pub mod list_metadata;
910
pub mod repository;
1011
pub mod term_relationships;
1112

@@ -64,6 +65,12 @@ pub enum DbTable {
6465
DbSites,
6566
/// Term relationships (post-category, post-tag associations)
6667
TermRelationships,
68+
/// List metadata headers (pagination, version)
69+
ListMetadata,
70+
/// List metadata items (entity IDs with ordering)
71+
ListMetadataItems,
72+
/// List metadata sync state (idle, fetching, error)
73+
ListMetadataState,
6774
}
6875

6976
impl DbTable {
@@ -79,6 +86,9 @@ impl DbTable {
7986
DbTable::SelfHostedSites => "self_hosted_sites",
8087
DbTable::DbSites => "db_sites",
8188
DbTable::TermRelationships => "term_relationships",
89+
DbTable::ListMetadata => "list_metadata",
90+
DbTable::ListMetadataItems => "list_metadata_items",
91+
DbTable::ListMetadataState => "list_metadata_state",
8292
}
8393
}
8494
}
@@ -107,6 +117,9 @@ impl TryFrom<&str> for DbTable {
107117
"self_hosted_sites" => Ok(DbTable::SelfHostedSites),
108118
"db_sites" => Ok(DbTable::DbSites),
109119
"term_relationships" => Ok(DbTable::TermRelationships),
120+
"list_metadata" => Ok(DbTable::ListMetadata),
121+
"list_metadata_items" => Ok(DbTable::ListMetadataItems),
122+
"list_metadata_state" => Ok(DbTable::ListMetadataState),
110123
_ => Err(DbTableError::UnknownTable(table_name.to_string())),
111124
}
112125
}
@@ -350,13 +363,14 @@ impl From<Connection> for WpApiCache {
350363
}
351364
}
352365

353-
static MIGRATION_QUERIES: [&str; 6] = [
366+
static MIGRATION_QUERIES: [&str; 7] = [
354367
include_str!("../migrations/0001-create-sites-table.sql"),
355368
include_str!("../migrations/0002-create-posts-table.sql"),
356369
include_str!("../migrations/0003-create-term-relationships.sql"),
357370
include_str!("../migrations/0004-create-posts-view-context-table.sql"),
358371
include_str!("../migrations/0005-create-posts-embed-context-table.sql"),
359372
include_str!("../migrations/0006-create-self-hosted-sites-table.sql"),
373+
include_str!("../migrations/0007-create-list-metadata-tables.sql"),
360374
];
361375

362376
pub struct MigrationManager<'a> {
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
use crate::RowId;
2+
3+
/// Represents list metadata header in the database.
4+
///
5+
/// Stores pagination info and version for a specific list (e.g., "edit:posts:publish").
6+
#[derive(Debug, Clone, PartialEq, Eq)]
7+
pub struct DbListMetadata {
8+
/// SQLite rowid of this list metadata
9+
pub row_id: RowId,
10+
/// Database site ID (rowid from sites table)
11+
pub db_site_id: RowId,
12+
/// List key (e.g., "edit:posts:publish")
13+
pub key: String,
14+
/// Total number of pages from API response
15+
pub total_pages: Option<i64>,
16+
/// Total number of items from API response
17+
pub total_items: Option<i64>,
18+
/// Current page that has been loaded (0 = no pages loaded)
19+
pub current_page: i64,
20+
/// Items per page
21+
pub per_page: i64,
22+
/// ISO 8601 timestamp of when page 1 was last fetched
23+
pub last_first_page_fetched_at: Option<String>,
24+
/// ISO 8601 timestamp of last update
25+
pub last_updated_at: Option<String>,
26+
/// Version number, incremented on page 1 refresh for concurrency control
27+
pub version: i64,
28+
}
29+
30+
/// Represents a single item in a list metadata collection.
31+
///
32+
/// Items are ordered by rowid (insertion order = display order).
33+
#[derive(Debug, Clone, PartialEq, Eq)]
34+
pub struct DbListMetadataItem {
35+
/// SQLite rowid (determines display order)
36+
pub row_id: RowId,
37+
/// Database site ID
38+
pub db_site_id: RowId,
39+
/// List key this item belongs to
40+
pub key: String,
41+
/// Entity ID (post ID, comment ID, etc.)
42+
pub entity_id: i64,
43+
/// Last modified timestamp (for staleness detection)
44+
pub modified_gmt: Option<String>,
45+
}
46+
47+
/// Represents sync state for a list metadata.
48+
#[derive(Debug, Clone, PartialEq, Eq)]
49+
pub struct DbListMetadataState {
50+
/// SQLite rowid
51+
pub row_id: RowId,
52+
/// Foreign key to list_metadata.rowid
53+
pub list_metadata_id: RowId,
54+
/// Current sync state
55+
pub state: ListState,
56+
/// Error message if state is error
57+
pub error_message: Option<String>,
58+
/// ISO 8601 timestamp of last state change
59+
pub updated_at: String,
60+
}
61+
62+
/// Sync state for a list.
63+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, uniffi::Enum)]
64+
pub enum ListState {
65+
/// No sync in progress
66+
#[default]
67+
Idle,
68+
/// Fetching first page (pull-to-refresh)
69+
FetchingFirstPage,
70+
/// Fetching subsequent page (load more)
71+
FetchingNextPage,
72+
/// Last sync failed
73+
Error,
74+
}
75+
76+
impl ListState {
77+
/// Convert to database string representation.
78+
pub fn as_db_str(&self) -> &'static str {
79+
match self {
80+
ListState::Idle => "idle",
81+
ListState::FetchingFirstPage => "fetching_first_page",
82+
ListState::FetchingNextPage => "fetching_next_page",
83+
ListState::Error => "error",
84+
}
85+
}
86+
}
87+
88+
impl From<&str> for ListState {
89+
fn from(s: &str) -> Self {
90+
match s {
91+
"idle" => ListState::Idle,
92+
"fetching_first_page" => ListState::FetchingFirstPage,
93+
"fetching_next_page" => ListState::FetchingNextPage,
94+
"error" => ListState::Error,
95+
_ => {
96+
// Default to Idle for unknown states to avoid panics
97+
eprintln!("Warning: Unknown ListState '{}', defaulting to Idle", s);
98+
ListState::Idle
99+
}
100+
}
101+
}
102+
}
103+
104+
impl From<String> for ListState {
105+
fn from(s: String) -> Self {
106+
ListState::from(s.as_str())
107+
}
108+
}

0 commit comments

Comments
 (0)