Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
5d5acd7
Add site type and mapping fields to DbSite
oguzkocer Oct 30, 2025
e3eea4a
Move DbSite and DbSiteType to db_types module
oguzkocer Oct 30, 2025
c0409cf
Reorganize mappings module into db_types
oguzkocer Oct 30, 2025
60a1e41
Replace DbSite field with db_site_id in database wrapper types
oguzkocer Oct 30, 2025
8d3a253
Add DbSelfHostedSite and self_hosted_sites table migration
oguzkocer Oct 30, 2025
e5e9ce9
Implement SiteRepository for managing self-hosted sites
oguzkocer Oct 30, 2025
072cc52
Introduce SelfHostedSite domain type for better API design
oguzkocer Oct 30, 2025
90a0165
Use SiteRepository in test fixtures with real test credentials
oguzkocer Oct 31, 2025
012f410
Remove unnecessary DbSelfHostedSite::to_self_hosted_site method
oguzkocer Oct 31, 2025
86777ee
Fix SiteRepository upsert bug with unique constraint
oguzkocer Oct 31, 2025
8aaf40e
Add comprehensive unit tests for SiteRepository
oguzkocer Oct 31, 2025
4e5b231
Rename SelfHostedSiteColumn to DbSelfHostedSiteColumn
oguzkocer Oct 31, 2025
7348d52
Remove public re-export of DbSite and DbSiteType
oguzkocer Oct 31, 2025
4c4c36a
Add create_random_test_site helper to reduce test verbosity
oguzkocer Oct 31, 2025
5f67e28
Implement delete methods for SiteRepository with proper cleanup
oguzkocer Oct 31, 2025
7132d4d
Rename sites table to db_sites for consistent naming
oguzkocer Oct 31, 2025
db9f0d4
Simplify test assertions using struct equality
oguzkocer Oct 31, 2025
844df6a
Replace unwrap() with expect() in sites.rs tests
oguzkocer Nov 1, 2025
e24d30d
Wrap SiteRepository operations in transactions for atomicity
oguzkocer Nov 1, 2025
2179407
Update expected migration counts in Kotlin & Swift tests
oguzkocer Nov 2, 2025
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
1 change: 1 addition & 0 deletions Cargo.lock

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

Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class WordPressApiCacheTest {

@Test
fun testThatMigrationsWork() = runTest {
assertEquals(5, WordPressApiCache().performMigrations())
assertEquals(6, WordPressApiCache().performMigrations())
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ actor Test {

@Test func testMigrationsWork() async throws {
let migrationsPerformed = try await self.cache.performMigrations()
#expect(migrationsPerformed == 5)
#expect(migrationsPerformed == 6)
}

#if !os(Linux)
Expand Down
1 change: 1 addition & 0 deletions wp_mobile_cache/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ wp_api = { path = "../wp_api" }

[dev-dependencies]
chrono = { workspace = true }
integration_test_credentials = { path = "../integration_test_credentials" }
rstest = "0.24"
14 changes: 12 additions & 2 deletions wp_mobile_cache/migrations/0001-create-sites-table.sql
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
CREATE TABLE `sites` (
CREATE TABLE `db_sites` (
-- Internal DB field (auto-incrementing)
`id` INTEGER PRIMARY KEY AUTOINCREMENT
`id` INTEGER PRIMARY KEY AUTOINCREMENT,

-- Type of site (0 = SelfHosted, 1 = WordPressCom)
`site_type` INTEGER NOT NULL,

-- Reference to type-specific table (self_hosted_sites.id or wordpress_com_sites.id)
-- Note: Not a foreign key constraint since it can point to different tables
`mapped_site_id` INTEGER NOT NULL
) STRICT;

-- Unique constraint to prevent duplicate site mappings
CREATE UNIQUE INDEX idx_db_sites_unique_site_type_and_mapped_site_id ON db_sites(site_type, mapped_site_id);
6 changes: 3 additions & 3 deletions wp_mobile_cache/migrations/0002-create-posts-table.sql
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ CREATE TABLE `posts_edit_context` (
-- Internal DB field (auto-incrementing)
`rowid` INTEGER PRIMARY KEY AUTOINCREMENT,

-- Site identifier (foreign key to sites table)
`db_site_id` INTEGER NOT NULL REFERENCES sites(id) ON DELETE CASCADE,
-- Site identifier (foreign key to db_sites table)
`db_site_id` INTEGER NOT NULL REFERENCES db_sites(id) ON DELETE CASCADE,

-- Top-level non-nullable fields
`id` INTEGER NOT NULL,
Expand Down Expand Up @@ -57,7 +57,7 @@ CREATE TABLE `posts_edit_context` (
-- Client-side cache metadata: when this post was last fetched from the WordPress API
`last_fetched_at` TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),

FOREIGN KEY (db_site_id) REFERENCES sites(id) ON DELETE CASCADE
FOREIGN KEY (db_site_id) REFERENCES db_sites(id) ON DELETE CASCADE
) STRICT;

CREATE UNIQUE INDEX idx_posts_edit_context_unique_db_site_id_and_id ON posts_edit_context(db_site_id, id);
Expand Down
4 changes: 2 additions & 2 deletions wp_mobile_cache/migrations/0003-create-term-relationships.sql
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ CREATE TABLE `term_relationships` (
-- Internal DB field (auto-incrementing)
`rowid` INTEGER PRIMARY KEY AUTOINCREMENT,

-- Site identifier (foreign key to sites table)
-- Site identifier (foreign key to db_sites table)
`db_site_id` INTEGER NOT NULL,

-- Object identifier (rowid of post/page/nav_menu_item/etc)
Expand All @@ -15,7 +15,7 @@ CREATE TABLE `term_relationships` (
-- Taxonomy type ('category', 'post_tag', or custom taxonomy)
`taxonomy_type` TEXT NOT NULL,

FOREIGN KEY (db_site_id) REFERENCES sites(id) ON DELETE CASCADE
FOREIGN KEY (db_site_id) REFERENCES db_sites(id) ON DELETE CASCADE
) STRICT;

-- Prevent duplicate associations (same object can't have same term twice in same taxonomy)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ CREATE TABLE `posts_view_context` (
-- Internal DB field (auto-incrementing)
`rowid` INTEGER PRIMARY KEY AUTOINCREMENT,

-- Site identifier (foreign key to sites table)
`db_site_id` INTEGER NOT NULL REFERENCES sites(id) ON DELETE CASCADE,
-- Site identifier (foreign key to db_sites table)
`db_site_id` INTEGER NOT NULL REFERENCES db_sites(id) ON DELETE CASCADE,

-- Top-level non-nullable fields
`id` INTEGER NOT NULL,
Expand Down Expand Up @@ -50,7 +50,7 @@ CREATE TABLE `posts_view_context` (
-- Client-side cache metadata: when this post was last fetched from the WordPress API
`last_fetched_at` TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),

FOREIGN KEY (db_site_id) REFERENCES sites(id) ON DELETE CASCADE
FOREIGN KEY (db_site_id) REFERENCES db_sites(id) ON DELETE CASCADE
) STRICT;

CREATE UNIQUE INDEX idx_posts_view_context_unique_db_site_id_and_id ON posts_view_context(db_site_id, id);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ CREATE TABLE `posts_embed_context` (
-- Internal DB field (auto-incrementing)
`rowid` INTEGER PRIMARY KEY AUTOINCREMENT,

-- Site identifier (foreign key to sites table)
`db_site_id` INTEGER NOT NULL REFERENCES sites(id) ON DELETE CASCADE,
-- Site identifier (foreign key to db_sites table)
`db_site_id` INTEGER NOT NULL REFERENCES db_sites(id) ON DELETE CASCADE,

-- Top-level non-nullable fields (minimal set for embed context)
`id` INTEGER NOT NULL,
Expand All @@ -29,7 +29,7 @@ CREATE TABLE `posts_embed_context` (
-- Client-side cache metadata: when this post was last fetched from the WordPress API
`last_fetched_at` TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),

FOREIGN KEY (db_site_id) REFERENCES sites(id) ON DELETE CASCADE
FOREIGN KEY (db_site_id) REFERENCES db_sites(id) ON DELETE CASCADE
) STRICT;

CREATE UNIQUE INDEX idx_posts_embed_context_unique_db_site_id_and_id ON posts_embed_context(db_site_id, id);
Expand Down
12 changes: 12 additions & 0 deletions wp_mobile_cache/migrations/0006-create-self-hosted-sites-table.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
CREATE TABLE `self_hosted_sites` (
-- Internal DB field (auto-incrementing)
`rowid` INTEGER PRIMARY KEY AUTOINCREMENT,

-- Site URL (unique constraint for upsert logic)
`url` TEXT NOT NULL UNIQUE,

-- WordPress REST API root URL
`api_root` TEXT NOT NULL
) STRICT;

CREATE INDEX idx_self_hosted_sites_url ON self_hosted_sites(url);
5 changes: 5 additions & 0 deletions wp_mobile_cache/src/db_types.rs
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
pub mod db_site;
pub mod db_term_relationship;
pub mod helpers;
pub mod posts;
pub mod row_ext;
pub mod self_hosted_site;
54 changes: 54 additions & 0 deletions wp_mobile_cache/src/db_types/db_site.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
use crate::RowId;
use rusqlite::types::{FromSql, FromSqlResult, ToSql, ToSqlOutput};

/// Type of WordPress site stored in the database.
///
/// Uses integer representation in the database for performance.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
#[repr(i64)]
pub enum DbSiteType {
SelfHosted = 0,
WordPressCom = 1,
}

impl ToSql for DbSiteType {
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
Ok(ToSqlOutput::from(*self as i64))
}
}

impl FromSql for DbSiteType {
fn column_result(value: rusqlite::types::ValueRef<'_>) -> FromSqlResult<Self> {
match i64::column_result(value)? {
0 => Ok(DbSiteType::SelfHosted),
1 => Ok(DbSiteType::WordPressCom),
other => Err(rusqlite::types::FromSqlError::OutOfRange(other)),
}
}
}

/// Represents a cached WordPress site in the database.
///
/// # Design Rationale
///
/// This is intentionally a database-specific type (hence the `Db` prefix) rather than
/// a domain type representing a WordPress site. This design choice prevents confusion:
///
/// - **Not a WordPress.com site ID**: The `row_id` has no relationship to WordPress.com site IDs
/// - **Not a self-hosted site identifier**: Self-hosted sites don't have numeric IDs
/// - **Internal cache identifier only**: This ID exists only for our local database's multi-site support
///
/// # Site Type Mapping
///
/// The `site_type` field indicates which type-specific table contains additional data:
/// - `DbSiteType::SelfHosted` → `mapped_site_id` references `self_hosted_sites` table
/// - `DbSiteType::WordPressCom` → `mapped_site_id` references `wordpress_com_sites` table (future)
///
/// Note: `mapped_site_id` is a reference column, not a foreign key constraint, since it can
/// point to different tables based on `site_type`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub struct DbSite {
pub row_id: RowId,
pub site_type: DbSiteType,
pub mapped_site_id: RowId,
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::{
DbSite, SqliteDbError,
mappings::{ColumnIndex, RowExt},
SqliteDbError,
db_types::row_ext::{ColumnIndex, RowExt},
term_relationships::DbTermRelationship,
};
use rusqlite::Row;
Expand Down Expand Up @@ -31,9 +31,7 @@ impl DbTermRelationship {

Ok(Self {
row_id: row.get_column(Col::Rowid)?,
site: DbSite {
row_id: row.get_column(Col::DbSiteId)?,
},
db_site_id: row.get_column(Col::DbSiteId)?,
object_id: row.get_column(Col::ObjectId)?,
term_id: TermId(row.get_column(Col::TermId)?),
taxonomy_type: row.get_column::<String, _>(Col::TaxonomyType)?.into(),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::{
SqliteDbError,
mappings::{ColumnIndex, RowExt},
db_types::row_ext::{ColumnIndex, RowExt},
};
use rusqlite::Row;
use serde::{Deserialize, Serialize};
Expand Down
6 changes: 3 additions & 3 deletions wp_mobile_cache/src/db_types/posts/edit.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::{DbSite, RowId, mappings::ColumnIndex};
use crate::{RowId, db_types::row_ext::ColumnIndex};
use wp_api::posts::AnyPostWithEditContext;

/// Column indexes for posts_edit_context table.
Expand All @@ -7,7 +7,7 @@ use wp_api::posts::AnyPostWithEditContext;
#[derive(Debug, Clone, Copy)]
pub(crate) enum PostEditContextColumn {
Rowid = 0,
SiteId = 1,
DbSiteId = 1,
Id = 2,
Date = 3,
DateGmt = 4,
Expand Down Expand Up @@ -52,7 +52,7 @@ impl ColumnIndex for PostEditContextColumn {

pub struct DbAnyPostWithEditContext {
pub row_id: RowId,
pub site: DbSite,
pub db_site_id: RowId,
pub post: AnyPostWithEditContext,
pub last_fetched_at: String,
}
6 changes: 3 additions & 3 deletions wp_mobile_cache/src/db_types/posts/embed.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::{DbSite, RowId, mappings::ColumnIndex};
use crate::{RowId, db_types::row_ext::ColumnIndex};
use wp_api::posts::AnyPostWithEmbedContext;

/// Column indexes for posts_embed_context table.
Expand All @@ -7,7 +7,7 @@ use wp_api::posts::AnyPostWithEmbedContext;
#[derive(Debug, Clone, Copy)]
pub(crate) enum PostEmbedContextColumn {
Rowid = 0,
SiteId = 1,
DbSiteId = 1,
Id = 2,
Date = 3,
Link = 4,
Expand All @@ -30,7 +30,7 @@ impl ColumnIndex for PostEmbedContextColumn {

pub struct DbAnyPostWithEmbedContext {
pub row_id: RowId,
pub site: DbSite,
pub db_site_id: RowId,
pub post: AnyPostWithEmbedContext,
pub last_fetched_at: String,
}
6 changes: 3 additions & 3 deletions wp_mobile_cache/src/db_types/posts/view.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::{DbSite, RowId, mappings::ColumnIndex};
use crate::{RowId, db_types::row_ext::ColumnIndex};
use wp_api::posts::AnyPostWithViewContext;

/// Column indexes for posts_view_context table.
Expand All @@ -7,7 +7,7 @@ use wp_api::posts::AnyPostWithViewContext;
#[derive(Debug, Clone, Copy)]
pub(crate) enum PostViewContextColumn {
Rowid = 0,
SiteId = 1,
DbSiteId = 1,
Id = 2,
Date = 3,
DateGmt = 4,
Expand Down Expand Up @@ -45,7 +45,7 @@ impl ColumnIndex for PostViewContextColumn {

pub struct DbAnyPostWithViewContext {
pub row_id: RowId,
pub site: DbSite,
pub db_site_id: RowId,
pub post: AnyPostWithViewContext,
pub last_fetched_at: String,
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
use rusqlite::Row;

pub mod helpers;
pub mod term_relationships;

/// Trait for types that can be used as column indexes.
/// Implemented by column enum types to provide type-safe column access.
pub trait ColumnIndex {
Expand Down
37 changes: 37 additions & 0 deletions wp_mobile_cache/src/db_types/self_hosted_site.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
use crate::{RowId, db_types::row_ext::ColumnIndex};

/// Column indexes for self_hosted_sites table.
/// These must match the order of columns in the CREATE TABLE statement.
#[repr(usize)]
#[derive(Debug, Clone, Copy)]
pub(crate) enum DbSelfHostedSiteColumn {
Rowid = 0,
Url = 1,
ApiRoot = 2,
}

impl ColumnIndex for DbSelfHostedSiteColumn {
fn as_index(&self) -> usize {
*self as usize
}
}

/// Represents a self-hosted WordPress site (domain model).
///
/// This type contains the site data without database metadata.
/// Use this when creating or updating sites.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SelfHostedSite {
pub url: String,
pub api_root: String,
}

/// Represents a self-hosted WordPress site in the database.
///
/// This type includes the database rowid along with the site data.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DbSelfHostedSite {
pub row_id: RowId,
pub url: String,
pub api_root: String,
}
Loading