Skip to content

Commit 092cd15

Browse files
authored
Add SiteRepository with polymorphic site support (#1000)
* Add site type and mapping fields to DbSite Extend DbSite to support multiple site types (self-hosted, WordPress.com). Add DbSiteType enum and mapped_site_id field to reference type-specific tables. Changes: - Add DbSiteType enum with SelfHosted and WordPressCom variants - Implement ToSql/FromSql for DbSiteType using i64 representation - Add site_type and mapped_site_id fields to DbSite struct - Update all DbSite construction in tests and fixtures - Add TODO comments in repository code for fetching site data from sites table * Move DbSite and DbSiteType to db_types module Relocate site-related types to db_types for better organization. Changes: - Create db_types/db_site.rs module with DbSite and DbSiteType - Re-export types from lib.rs for backward compatibility - Update module exports in db_types.rs * Reorganize mappings module into db_types Move type-related code from mappings module to db_types for better organization. The mappings name was misleading since we're not doing mappings anymore. Changes: - Move `mappings/term_relationships.rs` to `db_types/db_term_relationship.rs` - Move `mappings/helpers.rs` to `db_types/helpers.rs` - Create `db_types/row_ext.rs` with `ColumnIndex` and `RowExt` traits - Update all imports throughout the codebase - Remove old `mappings` module * Replace DbSite field with db_site_id in database wrapper types Use `db_site_id: RowId` instead of `site: DbSite` in database wrapper types. This avoids unnecessary joins since we already know the site context when querying. Changes: - Replace `site: DbSite` with `db_site_id: RowId` in DbAnyPostWithEditContext, DbAnyPostWithViewContext, DbAnyPostWithEmbedContext - Replace `site: DbSite` with `db_site_id: RowId` in DbTermRelationship - Rename column enum variants from `SiteId` to `DbSiteId` for clarity - Update all construction and test assertions to use `db_site_id` - Improve comment clarity for db_site_id field in DbTermRelationship * Add DbSelfHostedSite and self_hosted_sites table migration Create database structure for storing self-hosted WordPress sites with URL and API root. Changes: - Add `DbSelfHostedSite` struct in `db_types/self_hosted_site.rs` - Create `0006-create-self-hosted-sites-table.sql` migration - Modify `sites` table to add `site_type` and `mapped_site_id` columns - Update test fixtures to properly insert into both tables - Add migration to `MIGRATION_QUERIES` array * Implement SiteRepository for managing self-hosted sites Add repository layer for upserting and querying self-hosted WordPress sites. Changes: - Create `SiteRepository` in `repository/sites.rs` - Add table name constants for `self_hosted_sites` and `sites` - Implement `upsert_self_hosted_site` with RETURNING optimization - Implement `select_self_hosted_site` to query by DbSite reference - Implement `select_self_hosted_site_by_url` to query by URL - Add module export in `repository/mod.rs` * Introduce SelfHostedSite domain type for better API design Create a separate domain type for self-hosted sites that doesn't include database metadata. This improves the API design and makes it easier to add more fields in the future. Changes: - Add `SelfHostedSite` type with `url` and `api_root` fields - Update `upsert_self_hosted_site` to take `&SelfHostedSite` instead of individual parameters - Add `to_self_hosted_site()` method on `DbSelfHostedSite` for conversion * Use SiteRepository in test fixtures with real test credentials Update test fixtures to use `SiteRepository` for creating sites instead of manually constructing `DbSite` instances. This ensures test sites are properly inserted into the database and uses real test credentials from `integration_test_credentials`. Changes: - Add `integration_test_credentials` as dev dependency - Modify `test_db()` to return both `Connection` and `DbSite` - Update `test_ctx()` to use actual `DbSite` from repository - Change `create_test_site()` signature to accept `&SelfHostedSite` parameter - Update all test call sites to create `SelfHostedSite` instances - Use real test credentials in default test site creation * Remove unnecessary DbSelfHostedSite::to_self_hosted_site method * Fix SiteRepository upsert bug with unique constraint The `upsert_self_hosted_site` method had a data consistency bug where updating an existing site URL would create orphaned entries in the sites table. This occurred because only the self_hosted_sites table used upsert logic while the sites table always inserted. Changes: - Add unique constraint on `sites(site_type, mapped_site_id)` in migration 0001 - Update `upsert_self_hosted_site` to use ON CONFLICT clause for sites table - Ensures referential integrity between sites and self_hosted_sites tables * Add comprehensive unit tests for SiteRepository Implemented test suite covering all SiteRepository methods with both positive and negative test cases. Tests use a simple fixture approach without TestContext to avoid pollution from default site creation. Changes: - Rename column from `id` to `rowid` in self_hosted_sites table for consistency with posts tables - Add `Debug`, `Clone`, `PartialEq`, `Eq` derives to `DbSelfHostedSite` for testability - Add `count_all_sites()` method to `SiteRepository` (for testing) - Add schema validation test for `SelfHostedSiteColumn` enum - Add tests for `upsert_self_hosted_site` (insert and update behavior) - Add test verifying the duplicate sites bug fix using `count_all_sites()` method - Add tests for `select_self_hosted_site` (by DbSite reference) - Add tests for `select_self_hosted_site_by_url` - Add edge case tests (non-existent sites, wrong site types) - Add test for multiple sites coexisting - Tests use simple `test_conn` fixture instead of `TestContext` - Tests rely on rowid equality to prove update behavior (no unnecessary counts) All tests pass (67 total, 10 new for SiteRepository). * Rename SelfHostedSiteColumn to DbSelfHostedSiteColumn For consistency with the struct naming pattern where `DbSelfHostedSite` is the database representation, the column enum should be `DbSelfHostedSiteColumn`. Changes: - Rename `SelfHostedSiteColumn` to `DbSelfHostedSiteColumn` - Update all references in `SiteRepository` - Update test name to match new enum name * Remove public re-export of DbSite and DbSiteType These types are only used internally by repositories and tests, not exposed in any public API methods. Keeping them internal provides better encapsulation. Changes: - Remove `pub use db_types::db_site::{DbSite, DbSiteType}` from lib.rs - Update all modules to import directly from `crate::db_types::db_site` - Update test modules that were using `crate::DbSiteType` to use the direct import All tests pass (67 total). * Add create_random_test_site helper to reduce test verbosity Multi-site tests were verbose, requiring explicit URL construction for each test site. This helper uses an internal atomic counter to generate unique URLs automatically. Changes: - Add `create_random_test_site()` helper function - Uses `RANDOM_TEST_SITE_COUNTER` static atomic to generate unique URLs - Update `posts_multi_site_tests.rs` to use new helper - Update `term_relationships_multi_site_tests.rs` to use new helper - Reduces test site creation from 6 lines to 1 line All tests pass (67 total). * Implement delete methods for SiteRepository with proper cleanup Added core `delete_site()` method that handles deletion across both the sites table and type-specific tables (self_hosted_sites). Also added a convenience wrapper `delete_self_hosted_site_by_url()` for common use cases. The implementation ensures proper cleanup since foreign key constraints cannot be used with polymorphic site references. Panics with a clear message if attempting to delete WordPress.com sites (not yet implemented). Changes: - Add `delete_site()` method that deletes from both tables based on site type - Add `delete_self_hosted_site_by_url()` convenience wrapper - Add `count_all_self_hosted_sites()` for testing - Panic with clear message for unimplemented WordPress.com site deletion - Add 5 comprehensive tests covering: - Deletion from both tables (using repository methods, no raw SQL) - Return value for non-existent sites - Deletion isolation between sites - URL-based deletion - Error cases All tests pass (72 total, 5 new for delete functionality). * Rename sites table to db_sites for consistent naming The `sites` table has been renamed to `db_sites` to match the pattern established by `DbSite` and other database types. This makes the naming more consistent and clear that it's a database-level polymorphic table. Changes: - Rename `sites` table to `db_sites` in migration 0001 - Update unique index name to `idx_db_sites_unique_site_type_and_mapped_site_id` - Rename `SITES_TABLE` constant to `DB_SITES_TABLE` - Update all foreign key references from `sites(id)` to `db_sites(id)` in: - posts_edit_context table - posts_view_context table - posts_embed_context table - term_relationships table - Update all comments to reference `db_sites table` instead of `sites table` - Rename `count_all_sites()` method to `count_all_db_sites()` - Update all test references to use new naming All tests pass (72 total). * Simplify test assertions using struct equality Replace field-by-field comparisons with direct struct equality checks in site repository tests, leveraging the PartialEq implementations on DbSite and DbSelfHostedSite. Changes: - Use assert_eq! on structs instead of comparing individual fields - Reduces test verbosity while maintaining same validation coverage * Replace unwrap() with expect() in sites.rs tests Standardize error handling across all tests in sites.rs by replacing unwrap() calls with descriptive expect() messages. This improves debugging by providing clear context when tests fail. Changes: - Replace unwrap() with expect() in test_conn fixture (2 calls) - Replace unwrap() with expect() in all test functions (21 calls) - Add descriptive error messages for database operations - Total: 23 unwrap() calls eliminated * Wrap SiteRepository operations in transactions for atomicity Ensure upsert and delete operations maintain data consistency between the polymorphic sites table and type-specific tables by wrapping them in atomic transactions. Changes: - Wrap `upsert_self_hosted_site()` in transaction to prevent orphaned entries - Wrap `delete_site()` in transaction to ensure proper cleanup of both tables - Scope SQL statements in blocks to ensure they drop before `tx.commit()` - Update method signatures to use `TransactionManager` instead of `QueryExecutor` - Update test helpers to accept `&mut Connection` for transactional operations - Add comment explaining `DO UPDATE SET` requirement for `RETURNING` clause * Update expected migration counts in Kotlin & Swift tests
1 parent 3773f24 commit 092cd15

30 files changed

+898
-125
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

native/kotlin/api/kotlin/src/integrationTest/kotlin/WordPressApiCacheTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ class WordPressApiCacheTest {
1111

1212
@Test
1313
fun testThatMigrationsWork() = runTest {
14-
assertEquals(5, WordPressApiCache().performMigrations())
14+
assertEquals(6, WordPressApiCache().performMigrations())
1515
}
1616

1717
@Test

native/swift/Tests/wordpress-api-cache/WordPressApiCacheTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ actor Test {
1313

1414
@Test func testMigrationsWork() async throws {
1515
let migrationsPerformed = try await self.cache.performMigrations()
16-
#expect(migrationsPerformed == 5)
16+
#expect(migrationsPerformed == 6)
1717
}
1818

1919
#if !os(Linux)

wp_mobile_cache/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,5 @@ wp_api = { path = "../wp_api" }
1616

1717
[dev-dependencies]
1818
chrono = { workspace = true }
19+
integration_test_credentials = { path = "../integration_test_credentials" }
1920
rstest = "0.24"
Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,14 @@
1-
CREATE TABLE `sites` (
1+
CREATE TABLE `db_sites` (
22
-- Internal DB field (auto-incrementing)
3-
`id` INTEGER PRIMARY KEY AUTOINCREMENT
3+
`id` INTEGER PRIMARY KEY AUTOINCREMENT,
4+
5+
-- Type of site (0 = SelfHosted, 1 = WordPressCom)
6+
`site_type` INTEGER NOT NULL,
7+
8+
-- Reference to type-specific table (self_hosted_sites.id or wordpress_com_sites.id)
9+
-- Note: Not a foreign key constraint since it can point to different tables
10+
`mapped_site_id` INTEGER NOT NULL
411
) STRICT;
12+
13+
-- Unique constraint to prevent duplicate site mappings
14+
CREATE UNIQUE INDEX idx_db_sites_unique_site_type_and_mapped_site_id ON db_sites(site_type, mapped_site_id);

wp_mobile_cache/migrations/0002-create-posts-table.sql

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ CREATE TABLE `posts_edit_context` (
22
-- Internal DB field (auto-incrementing)
33
`rowid` INTEGER PRIMARY KEY AUTOINCREMENT,
44

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

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

60-
FOREIGN KEY (db_site_id) REFERENCES sites(id) ON DELETE CASCADE
60+
FOREIGN KEY (db_site_id) REFERENCES db_sites(id) ON DELETE CASCADE
6161
) STRICT;
6262

6363
CREATE UNIQUE INDEX idx_posts_edit_context_unique_db_site_id_and_id ON posts_edit_context(db_site_id, id);

wp_mobile_cache/migrations/0003-create-term-relationships.sql

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ CREATE TABLE `term_relationships` (
22
-- Internal DB field (auto-incrementing)
33
`rowid` INTEGER PRIMARY KEY AUTOINCREMENT,
44

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

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

18-
FOREIGN KEY (db_site_id) REFERENCES sites(id) ON DELETE CASCADE
18+
FOREIGN KEY (db_site_id) REFERENCES db_sites(id) ON DELETE CASCADE
1919
) STRICT;
2020

2121
-- Prevent duplicate associations (same object can't have same term twice in same taxonomy)

wp_mobile_cache/migrations/0004-create-posts-view-context-table.sql

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ CREATE TABLE `posts_view_context` (
22
-- Internal DB field (auto-incrementing)
33
`rowid` INTEGER PRIMARY KEY AUTOINCREMENT,
44

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

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

53-
FOREIGN KEY (db_site_id) REFERENCES sites(id) ON DELETE CASCADE
53+
FOREIGN KEY (db_site_id) REFERENCES db_sites(id) ON DELETE CASCADE
5454
) STRICT;
5555

5656
CREATE UNIQUE INDEX idx_posts_view_context_unique_db_site_id_and_id ON posts_view_context(db_site_id, id);

wp_mobile_cache/migrations/0005-create-posts-embed-context-table.sql

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ CREATE TABLE `posts_embed_context` (
22
-- Internal DB field (auto-incrementing)
33
`rowid` INTEGER PRIMARY KEY AUTOINCREMENT,
44

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

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

32-
FOREIGN KEY (db_site_id) REFERENCES sites(id) ON DELETE CASCADE
32+
FOREIGN KEY (db_site_id) REFERENCES db_sites(id) ON DELETE CASCADE
3333
) STRICT;
3434

3535
CREATE UNIQUE INDEX idx_posts_embed_context_unique_db_site_id_and_id ON posts_embed_context(db_site_id, id);
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
CREATE TABLE `self_hosted_sites` (
2+
-- Internal DB field (auto-incrementing)
3+
`rowid` INTEGER PRIMARY KEY AUTOINCREMENT,
4+
5+
-- Site URL (unique constraint for upsert logic)
6+
`url` TEXT NOT NULL UNIQUE,
7+
8+
-- WordPress REST API root URL
9+
`api_root` TEXT NOT NULL
10+
) STRICT;
11+
12+
CREATE INDEX idx_self_hosted_sites_url ON self_hosted_sites(url);

0 commit comments

Comments
 (0)