Skip to content

Commit e97db90

Browse files
oguzkocerjkmassel
andauthored
Caching support (#976)
* Add SQLite post mappings for wp_mobile_cache Implements bidirectional mapping between `AnyPostWithEditContext` and SQLite for the mobile cache layer. Key features: - Helper functions for common conversions (ID types, enums, JSON, dates) - Type-safe column access via `ColumnIndex` trait and `RowExt` extension - Inline struct construction minimizes intermediate variables - Type inference using `From` trait reduces boilerplate Changes: - Add `mappings` module with `TryFromDbRow` and `InsertIntoDb` traits - Add `helpers` module with reusable conversion functions - Implement post mappings in `mappings/posts.rs` - Update schema to match REST API field names - Add `serde` and `serde_json` dependencies * Add unit tests for SQLite post mappings Implements comprehensive round-trip testing to validate that `AnyPostWithEditContext` can be correctly serialized to and deserialized from the SQLite database. Changes: - Add test fixtures for creating minimal and fully-populated posts - Add `setup_test_db()` helper that applies all migrations - Add round-trip tests covering all field types and edge cases - Test optional field handling, enum variants, and empty collections * Add Repository pattern for database operations in wp_mobile_cache Implements a flexible repository pattern to reduce boilerplate for database CRUD operations while allowing type-specific customization. Changes: - Add `QueryExecutor` trait to abstract database query execution - Add `DbEntity` trait for types that can be persisted to database - Add `Repository` trait with default implementations for `insert` and `insert_batch` - Implement `PostRepository` with custom query methods (`select_by_post_id`, `select_by_author`, `select_by_status`, `delete_by_post_id`, `update_by_post_id`, `count`) - Update existing unit tests to use `PostRepository` instead of raw SQL queries - Add comprehensive tests for all repository methods * Add upsert method to PostRepository for atomic insert/update operations Implements SQLite's INSERT ... ON CONFLICT ... DO UPDATE syntax to handle both insert and update operations in a single SQL statement. This ensures database observers receive a single INSERT or UPDATE action, not DELETE + INSERT. Changes: - Add `upsert()` method that inserts or updates posts by `post_id` - Uses the UNIQUE INDEX on `id` column to detect conflicts - Writes SQL field list only once (shared between INSERT and UPDATE) - Preserves rowid when updating (same row, not delete+insert) - Add comprehensive tests for both insert and update scenarios * Add site_id to post database structure Update the post database schema and repository to support multi-site functionality by adding a site_id field. The combination of (site_id, id) is now unique instead of just id, allowing posts from different sites to have the same WordPress post ID without conflicts. Changes: - Add SiteId newtype wrapper in lib.rs - Update database migration to include site_id column - Change unique index from id to (site_id, id) - Update DbAnyPostWithEditContext to include site_id field - Modify InsertIntoDb trait to require site_id parameter - Update all Repository methods to accept site_id - Update PostRepository query methods to filter by site_id - Update upsert to use ON CONFLICT(site_id, id) - Update all tests to pass site_id parameter * Add sites table migration and foreign key constraint Reorganize migrations to add a sites table as the foundation for multi-site support. Posts now reference the sites table via a foreign key constraint. Changes: - Create 0001-create-sites-table.sql with scaffold sites table - Rename posts migration from 0001 to 0002 - Delete users migration (not needed yet) - Add foreign key constraint from posts.site_id to sites.id - Add index on posts.site_id for query performance - Update MIGRATION_QUERIES array to reference new migrations - Update setup_test_db to insert test site for FK constraint * Add Repository pattern design documentation Document the repository pattern implementation with comprehensive coverage of design decisions, multi-site architecture, and usage examples. The documentation includes: - Core components (QueryExecutor, DbEntity, Repository traits) - Design decisions and rationale - Multi-site architecture with foreign key constraints - UPSERT implementation using composite keys - Complete usage examples with site_id scoping - File organization and migration structure * Introduce RowId type and replace SiteId with DbSite Replace i64 row IDs with strongly-typed RowId (u64) and refactor SiteId to DbSite to prevent confusion with WordPress.com site IDs. SQLite autoincrement guarantees non-negative values, making u64 appropriate. The DbSite type is explicitly database-internal (Db prefix) and prevents mixing up cache database row IDs with WordPress domain identifiers. Repository methods now require &DbSite, forcing callers to fetch valid sites before querying, which prevents invalid ID construction. Changes: - Add `RowId` type wrapping u64 with `ToSql`/`FromSql` traits for seamless rusqlite integration - Replace `SiteId` with `DbSite` struct containing `row_id: RowId` field - Update all repository methods to take `&DbSite` instead of `SiteId` - Update `DbAnyPostWithEditContext` to use `RowId` and `DbSite` types - Add comprehensive documentation explaining DbSite design rationale - Update REPOSITORY_PATTERN_DESIGN.md with multi-site architecture details * Split QueryExecutor and TransactionManager traits for type-level transaction safety This commit refactors the database abstraction layer to prevent nested transactions at compile time and ensure consistent use of the QueryExecutor abstraction throughout the codebase. Key changes: **Trait Separation:** - Split `QueryExecutor` into two traits: `QueryExecutor` and `TransactionManager` - `QueryExecutor` provides core query operations: `prepare()`, `execute()`, `last_insert_rowid()` - `TransactionManager` extends `QueryExecutor` and adds `transaction()` method - `Connection` implements both traits (can execute queries and create transactions) - `Transaction` implements only `QueryExecutor` (prevents nested transactions at compile time) **Type Safety Improvements:** - Updated `InsertIntoDb` trait to accept `&impl QueryExecutor` instead of `&Connection` - Changed return type from `i64` to `RowId` for consistency with type wrappers - Repository methods now use `&impl QueryExecutor` for queries - `insert_batch()` requires `&mut impl TransactionManager` making transaction requirement explicit **Documentation Updates:** - Added "Decision 8: Split QueryExecutor and TransactionManager Traits" section - Updated all trait signatures to reflect QueryExecutor/TransactionManager split - Updated all code examples to use `RowId` instead of `i64` - Updated method signatures in concrete repository examples - Clarified that Transaction implements QueryExecutor (compile-time nested transaction prevention) **Benefits:** - Compile-time enforcement prevents nested transactions (no runtime panics) - Clear separation of concerns between query execution and transaction management - Repository method signatures explicitly show when transactions are needed - Consistent abstraction layer throughout codebase (no direct Connection usage) - Type-safe return values using `RowId` wrapper instead of raw `i64` * Rename site_id to db_site_id for consistency Rename the site_id column to db_site_id in the posts table and all related queries to maintain consistency with database-internal naming conventions. Changes: - Updated migration: site_id → db_site_id column and index names - Updated all SQL queries in PostRepository - Updated INSERT query in InsertIntoDb implementation - Updated documentation examples in REPOSITORY_PATTERN_DESIGN.md * Document term_relationships design and add reorganization prompt Add comprehensive documentation for Decision 9: Term Relationships Normalization, including database schema, type design, repository patterns, and usage examples. Also add a prompt file for a future session to reorganize the growing documentation into a more maintainable structure with separate files in a docs/ folder. Changes: - Add Decision 9: Term Relationships Normalization to design doc - Document sync-based approach for observer compatibility - Add upsert_with_terms usage examples - Clarify referential integrity (FK on db_site_id, not object_id) - Update file organization to include term_relationships files - Add REORGANIZE_DOCS_PROMPT.md for future documentation refactoring * Implement term relationships normalization Store post categories and tags in a normalized `term_relationships` table instead of JSON arrays to enable efficient querying and improve data integrity. This implementation uses a sync-based approach that only generates database events for actual changes (inserts/deletes), making it observer-friendly. The design is extensible to support any taxonomy type and object type. Changes: - Add migration 0003 creating term_relationships table with indexes - Create DbTermRelationship type with TaxonomyType and TermId from wp_api - Implement TermRelationshipRepository with sync_terms_for_object method - Update PostRepository to populate terms from term_relationships on read - Add upsert_with_terms for atomic post and term operations - Update delete_by_post_id to cascade term relationship cleanup - Fix upsert to query rowid after UPDATE (last_insert_rowid doesn't change) - Add 13 comprehensive tests for term relationship operations * Add cache freshness tracking with last_fetched_at Add `last_fetched_at` timestamp to cached posts for tracking data staleness. This enables cache refresh logic and displaying "Last updated X ago" in offline UX scenarios like pull-to-refresh. SQLite automatically manages timestamps using DEFAULT on INSERT and explicit setting on UPDATE, requiring zero boilerplate from developers. Changes: - Add `last_fetched_at` column to posts_edit_context table with UTC timestamp default - Add `last_fetched_at` field to DbAnyPostWithEditContext type - Update upsert SQL to set timestamp on UPDATE via strftime - Add column enum and mapping for last_fetched_at in posts mappings - Add helper function assert_recent_timestamp for timestamp validation in tests - Add test_insert_sets_last_fetched_at to verify INSERT behavior - Add test_upsert_updates_last_fetched_at_on_update to verify UPDATE behavior - Document Decision 10 in REPOSITORY_PATTERN_DESIGN.md with rationale and examples * Reorganize documentation into focused topic-specific files Split monolithic REPOSITORY_PATTERN_DESIGN.md into well-organized documentation structure for better maintainability and navigation. The new structure provides: - Clear navigation via docs/README.md - Focused architecture documents - Individual design decision files (10 decisions) - Complete repository API documentation - Practical usage examples and migration guide Changes: - Create docs/ directory structure with architecture/, design-decisions/, and repositories/ subdirectories - Create docs/README.md with comprehensive navigation and quick start - Create docs/architecture/core-traits.md for QueryExecutor, TransactionManager, Repository traits - Create docs/architecture/database-schema.md for complete schema documentation - Create docs/architecture/type-system.md for RowId, DbSite, and wrapper types - Create 10 focused design decision documents (01-executor-passing.md through 10-cache-freshness.md) - Create docs/repositories/post-repository.md with full PostRepository API - Create docs/repositories/term-relationship-repository.md with TermRelationshipRepository API - Create docs/usage-examples.md with comprehensive code examples - Create docs/migration-guide.md for adding new entities - Remove REPOSITORY_PATTERN_DESIGN.md (content fully migrated) - Remove REORGANIZE_DOCS_PROMPT.md (task completed) * Add comprehensive test suite for PostRepository Implement transaction rollback, constraint, and multi-site isolation tests. This commit adds 19 new tests across four test modules to verify critical database behavior and data integrity guarantees. The test suite now includes comprehensive coverage of transaction safety, constraint enforcement, and multi-site isolation. Changes: - Add rstest framework for fixture-based testing - Create `PostBuilder` with thread-safe auto-incrementing IDs - Add rstest fixtures (`test_db`, `test_site`) to reduce boilerplate - Implement transaction rollback tests (4 tests) - Verify constraint violation rollback - Verify foreign key violation rollback - Verify term relationship consistency - Verify happy path batch inserts - Implement constraint enforcement tests (7 tests) - Duplicate post ID detection - Foreign key constraint validation - Error handling for non-existent entities - Implement multi-site isolation tests (8 tests total) - Verify posts isolated between sites - Verify same post ID allowed in different sites - Verify operations respect site boundaries - Verify term relationships isolated by site * Consolidate test helpers by migrating to rstest fixtures Standardize all test infrastructure to use rstest fixtures for consistency and better test ergonomics. The previous setup had two separate approaches: `setup_test_db()` function in `unit_test_common.rs` and rstest fixtures in `test_helpers.rs`. This consolidation eliminates the redundancy. Changes: - Convert all tests in `repository/posts.rs` to use rstest fixtures (17 tests) - Convert all tests in `repository/term_relationships.rs` to rstest fixtures (7 tests) - Convert all tests in `mappings/posts.rs` to rstest fixtures (6 tests) - Replace `#[test]` with `#[rstest]` throughout test suite - Update imports from `unit_test_common::setup_test_db` to `test_helpers::{test_db, test_site}` - Replace function parameters from manual setup to rstest fixtures - Delete `unit_test_common.rs` module - Remove `unit_test_common` from `lib.rs` module declarations * Increase the migration count in WordPressApiCacheTest * Consolidate post insertion logic into single upsert method Simplified the post repository API by consolidating three separate insertion code paths into a single atomic `upsert()` method that always handles term relationships correctly. Changes: - Remove JSON `categories` and `tags` columns from posts table migration - Update column indices in post mappings after removing JSON columns - Remove `insert()` and `insert_batch()` from Repository trait - Integrate term relationships sync directly into `upsert()` method - Delete redundant `upsert_with_terms()` method - Add `upsert_batch()` that calls `upsert()` for each post - Update all tests to use consolidated `upsert()` method - Fix test expectations for UPSERT behavior (updates instead of errors) The new `upsert()` method: - Uses SQLite's INSERT...ON CONFLICT...DO UPDATE for atomic operations - Syncs term relationships in the same transaction - Ensures database observers see single INSERT/UPDATE events - Makes it impossible to accidentally insert posts without handling terms All 50 tests passing. * Remove InsertIntoDb trait The InsertIntoDb trait was a leaky abstraction that couldn't handle cross-repository concerns. Posts require term relationships to be synced via TermRelationshipRepository, making it impossible to implement proper insertion through a trait on the entity type itself. The only correct way to insert/update posts is through PostRepository::upsert(), which coordinates with the term repository in a single transaction. Changes: - Remove InsertIntoDb trait definition from mappings module - Remove InsertIntoDb implementation for AnyPostWithEditContext - Remove InsertIntoDb trait bound from DbEntity - Remove unused helper imports (bool_to_integer, serialize_value_to_json) - Clean up test code that only existed to satisfy the trait bound This simplifies the codebase by removing ~95 lines of dead code. * Remove Repository trait The Repository trait was premature abstraction. PostRepository is currently the only repository, and future repositories (users, media, comments) will likely have different requirements: - Posts need term relationship coordination - Other entities won't have categories/tags - Different entities will need different query methods Rather than forcing a common interface that may not fit all use cases, each repository will define its own API based on its specific needs. The trait can be reintroduced later if a clear pattern emerges across multiple repositories. Changes: - Remove Repository trait definition and documentation - Remove impl Repository for PostRepository - Remove TestRepository test scaffolding - Update PostRepository doc comment The DbEntity marker trait remains as it's genuinely generic (just TABLE_NAME constant). * Update documentation to reflect consolidated post insertion logic Remove outdated references to removed traits and methods: - Remove all references to InsertIntoDb trait (removed in recent commits) - Remove all references to Repository trait (removed as premature abstraction) - Update insert() and insert_batch() references to upsert() and upsert_batch() - Update upsert_with_terms() references to upsert() (terms now handled automatically) - Update method signatures to reflect new TransactionManager requirement Updated files: - docs/architecture/core-traits.md: Removed Repository trait section - docs/repositories/post-repository.md: Updated all method signatures and examples - docs/repositories/term-relationship-repository.md: Removed Repository trait note - docs/usage-examples.md: Updated all code examples to use upsert() - docs/migration-guide.md: Removed InsertIntoDb implementation steps - docs/design-decisions/*.md: Updated all code examples and added historical note All examples now accurately reflect the current implementation where: - PostRepository::upsert() handles term synchronization automatically - Each repository defines its own API (no shared Repository trait) - All operations use TransactionManager for atomic commits * Temporarily remove wp_mobile_cache documentation * Remove unnecessary comment from DbSite * Add From<String> and From<&str> implementations for TaxonomyType Implement infallible string conversions for TaxonomyType in wp_api, leveraging the Custom(String) variant as a fallback. This provides a cleaner, more reusable API than serde_json for simple string parsing. Implementation uses delegation pattern to avoid code duplication: - From<&str> contains the actual conversion logic - From<String> delegates to From<&str> via s.as_str() Changes: - Add From<&str> for TaxonomyType in wp_api/src/taxonomies.rs - Add From<String> for TaxonomyType (delegates to From<&str>) - Update term_relationships mapping to use .into() instead of serde_json Benefits: - Cleaner code: row.get_column(...)?.into() vs serde_json boilerplate - Reusable: From implementations available everywhere, not just in mapping layer - Infallible: No error handling needed thanks to Custom variant fallback - No code duplication: From<String> delegates to From<&str> - No unnecessary allocations from serde_json path All existing tests pass. * Remove unused test helpers and redundant test Clean up test infrastructure by removing unused fixtures and duplicate test coverage. Changes: - Remove unused `second_site` fixture from test_helpers - Remove redundant `test_site_isolation_basic_verification` test - Remove emoji from comment in term_relationships.rs * Refactor test infrastructure to use centralized TestContext Consolidate test fixture usage by introducing a comprehensive `TestContext` that bundles all common test dependencies (database connection, site, and repositories). This reduces boilerplate, improves maintainability, and prepares the codebase for future service layer patterns. Changes: - Add `term_repo` fixture and `TermRelationshipRepository` to `TestContext` - Rename `TestContext.repo` to `post_repo` for clarity - Update all test functions to use `TestContext` fixture instead of manually instantiating repositories - Update 49 test functions across 6 test files (posts.rs, posts_constraint_tests.rs, posts_multi_site_tests.rs, posts_transaction_tests.rs, term_relationships.rs, term_relationships_multi_site_tests.rs) - Remove repetitive `let repo = Repository;` lines from all tests * Complete TestContext migration and simplify fixture structure Finish migrating remaining tests to use `TestContext` and streamline the fixture implementation by removing intermediate fixtures. Changes: - Update 7 mapping tests in `posts.rs` to use `TestContext` - Remove unused individual fixtures (`test_site`, `post_repo`, `term_repo`) - Simplify `test_ctx` fixture to construct all dependencies inline - Remove unused imports from mapping tests * Rename test_helpers to test_fixtures for consistency Consolidate test infrastructure by renaming the test helpers module to match the existing test_fixtures module naming convention, providing a clearer and more consistent structure. Changes: - Rename test_helpers.rs to test_fixtures.rs - Update module declaration in lib.rs - Update all test imports to use test_fixtures instead of test_helpers - Remove test_fixtures/mod.rs directory * Remove unused query methods from PostRepository Remove select_by_author and select_by_status methods along with their tests to avoid premature commitment to APIs that may not be needed. These can be added back when actual use cases emerge, preventing unnecessary maintenance burden during early development. Changes: - Remove select_by_author method and test - Remove select_by_status method and test - Remove unused UserId import from tests * Remove redundant tests from posts mapping Remove test_round_trip_with_optional_fields_none and test_round_trip_with_sticky_boolean_variants as they duplicate coverage already provided by existing tests. The first test explicitly sets fields to None that are already None in create_minimal_post(). The second test exhaustively checks all Option<bool> variants when standard SQLite boolean handling is already validated by existing tests. Changes: - Remove test_round_trip_with_optional_fields_none test - Remove test_round_trip_with_sticky_boolean_variants test * Remove DbEntity trait and centralize table names in repositories Replace the DbEntity trait with const TABLE_NAME in each repository. This is simpler and achieves the same goal without adding ceremony that doesn't provide compile-time guarantees. Changes: - Remove DbEntity trait from repository/mod.rs - Remove DbEntity implementation from AnyPostWithEditContext - Add const TABLE_NAME to PostRepository and TermRelationshipRepository - Replace all hardcoded table names with Self::TABLE_NAME in SQL queries * Enforce transaction requirement for sync_terms_for_object Change sync_terms_for_object to require a Transaction parameter instead of generic QueryExecutor to guarantee atomicity at compile-time. This prevents accidental usage without a transaction which could lead to inconsistent state if any step fails. The method performs multiple operations (SELECT, DELETE, INSERT) that must be atomic, so requiring Transaction prevents bugs where the method is called with a bare Connection. Changes: - Change sync_terms_for_object parameter from &impl QueryExecutor to &rusqlite::Transaction - Update all test calls to create and commit transactions * Enforce term loading via type-safe PostTerms parameter Refactor post entity construction to guarantee terms are always loaded from the database at compile time. This prevents bugs where terms might be accidentally omitted, and adds efficient batch loading to avoid N+1 query problems. Changes: - Add `PostTerms` struct to encapsulate categories and tags - Add `get_post_terms()` for single post term loading - Add `get_post_terms_batch()` for efficient batch loading of multiple posts - Change `DbAnyPostWithEditContext` to use `from_row_with_terms(row, terms)` requiring `PostTerms` parameter - Update all `PostRepository` select methods to load terms before constructing posts - Remove `TryFromDbRow` trait (no longer needed) - Remove `populate_terms()` mutation method (replaced by explicit loading) - Mark `mappings/term_relationships.rs` as unused * Refactor term loading to use generic repository pattern Replace domain-specific `PostTerms` with generic `DbTermRelationship` entities to improve separation of concerns and repository reusability. Changes: - Add `RowId::to_sql_list()` helper to create comma-separated ID lists for SQL IN clauses - Restore `DbTermRelationship::from_row()` mapping in `mappings/term_relationships.rs` - Replace `get_post_terms()` and `get_post_terms_batch()` with generic `get_terms_for_objects()` - Repository layer now returns raw `DbTermRelationship` entities - Move domain logic (extracting categories/tags) to mapping layer in `from_row_with_terms()` - Use functional programming (fold) for cleaner term extraction - Update `PostRepository` to use `get_terms_for_objects()` - Interpolate object_ids into SQL IN clause (safe since they're internal IDs) - Keep db_site_id as parameterized query for consistency - Repository is now reusable for other object types (pages, nav items, etc.) * Fix term relationships to use WordPress object IDs instead of SQLite rowids The `term_relationships` table was incorrectly storing SQLite rowids as `object_id` values instead of WordPress object IDs (e.g., post.id). This caused term relationships to be lost when querying by WordPress ID. All term relationship operations now consistently use WordPress object IDs throughout the codebase, ensuring terms are properly associated with their WordPress objects regardless of how the object is queried. Changes: - Update `TermRelationshipRepository` methods to use `i64` for `object_id` instead of `RowId` - Fix `PostRepository.upsert()` to sync terms using `post.id.0` instead of `post_rowid` - Fix `PostRepository` select methods to query WordPress post IDs for term loading - Fix `PostRepository.delete_by_post_id()` to delete terms using WordPress post ID - Update all tests to use WordPress post IDs instead of SQLite rowids - Remove unused `RowId` import from `term_relationships.rs` * Enforce PostBuilder pattern and improve test ergonomics Made `create_minimal_post()` and `create_full_post()` private to enforce using the builder pattern. Added convenience methods and improved the API to reduce boilerplate in tests. Changes: - Make `create_minimal_post()` and `create_full_post()` private - Add `PostBuilder::minimal()` and `PostBuilder::full()` convenience methods - Change `with_id()` to accept `i64` instead of `PostId` for better ergonomics - Update all tests to use `PostBuilder::minimal()` instead of `create_minimal_post()` - Update all tests to use `PostBuilder::full()` instead of `create_full_post()` - Remove unnecessary `.with_id()` calls where tests don't need specific IDs - Convert mutation patterns to builder patterns for immutability - Add comprehensive documentation for `PostBuilder` and `PostBuilderInitialState` * Add with_post_id() builder method for PostId convenience Added `with_post_id(PostId)` method alongside `with_id(i64)` to provide flexibility when working with existing `PostId` variables. Updated test to demonstrate the pattern of declaring the ID once and reusing it throughout. Changes: - Add `PostBuilder::with_post_id(PostId)` method - Refactor `test_upsert_maintains_consistency_on_success` to use `post_id_500` variable - Use `with_post_id()` when working with `PostId` variables for better ergonomics * Replace for loops with functional programming patterns Converted imperative for loops to functional patterns using iterators for better clarity and idiomatic Rust code. Changes: - Replace `upsert_batch()` for loop with `.map().collect()` - Replace `delete_terms()` param building for loop with `.extend()` - Replace `insert_terms()` for loop with `.try_for_each()` - Replace `get_all_terms_for_object()` for loop with `.try_fold()` - Replace test verification for loop with `.for_each()` * Refactor tests to use repository instances from test context Improves test consistency by using repository instances from `TestContext` instead of creating local instances in each test. Changes: - Use `test_ctx.post_repo` instead of local `PostRepository` instances - Use `test_ctx.term_repo` instead of local `TermRelationshipRepository` instances - Fix module comment in `posts_transaction_tests.rs` to accurately reflect both success and failure test cases * Combine duplicate round-trip tests into parameterized test Replace separate `test_round_trip_with_minimal_fields` and `test_round_trip_with_all_fields` with a single parameterized test using rstest's `#[case]` attribute. Changes: - Use `#[case]` to test both minimal and full post scenarios - Eliminate duplicate test code while maintaining coverage * Improve timestamp assertion with proper date parsing Replace weak string-based timestamp validation with robust date parsing that verifies timestamps are recent and valid. Changes: - Add `assert_recent_timestamp` helper in `test_fixtures` module - Parse timestamps using chrono's RFC3339 parser - Verify timestamps are within 5 seconds of current time - Replace manual assertions in `repository/posts.rs` tests - Remove duplicate helper from `mappings/posts.rs` in favor of shared implementation - Add chrono to dev-dependencies for timestamp parsing * Move mapping tests to repository module Consolidate all database-related tests in the repository module for better organization and clearer test coverage. Changes: - Move `test_round_trip` tests from `mappings/posts.rs` to `repository/posts.rs` - Move `test_round_trip_with_different_enum_variants` to repository module - Move `test_round_trip_with_empty_json_arrays` to repository module - Remove test module from `mappings/posts.rs` * Add InTransaction marker trait for compile-time transaction enforcement Introduce `InTransaction` trait to enforce transaction requirements at compile-time while maintaining the trait abstraction layer. Changes: - Add `InTransaction` marker trait implemented only by `Transaction` - Update `sync_terms_for_object` to accept `&impl InTransaction` - Replace direct `&rusqlite::Transaction` dependency with trait bound - Maintain compile-time guarantee that method runs in transaction context * Fix migration count --------- Co-authored-by: Jeremy Massel <1123407+jkmassel@users.noreply.github.com>
1 parent 26dcca9 commit e97db90

25 files changed

+3392
-49
lines changed

Cargo.lock

Lines changed: 41 additions & 8 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(2, WordPressApiCache().performMigrations())
14+
assertEquals(3, 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 == 2)
16+
#expect(migrationsPerformed == 3)
1717
}
1818

1919
#if !os(Linux)

wp_api/src/taxonomies.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,24 @@ impl Display for TaxonomyType {
3737
}
3838
}
3939

40+
impl From<&str> for TaxonomyType {
41+
fn from(s: &str) -> Self {
42+
match s {
43+
"category" => Self::Category,
44+
"post_tag" => Self::PostTag,
45+
"nav_menu" => Self::NavMenu,
46+
"wp_pattern_category" => Self::WpPatternCategory,
47+
_ => Self::Custom(s.to_string()),
48+
}
49+
}
50+
}
51+
52+
impl From<String> for TaxonomyType {
53+
fn from(s: String) -> Self {
54+
Self::from(s.as_str())
55+
}
56+
}
57+
4058
#[derive(
4159
Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, uniffi::Enum,
4260
)]

wp_mobile_cache/Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ default = ["rusqlite/bundled"]
88

99
[dependencies]
1010
rusqlite = { version = "0.37.0", features = ["hooks"] }
11+
serde = { workspace = true }
12+
serde_json = { workspace = true }
1113
thiserror = { workspace = true }
1214
uniffi = { workspace = true }
1315
wp_api = { path = "../wp_api" }
16+
17+
[dev-dependencies]
18+
chrono = { workspace = true }
19+
rstest = "0.18"

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

Lines changed: 0 additions & 22 deletions
This file was deleted.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
CREATE TABLE `sites` (
2+
-- Internal DB field (auto-incrementing)
3+
`id` INTEGER PRIMARY KEY AUTOINCREMENT
4+
) STRICT;
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
CREATE TABLE `posts_edit_context` (
2+
-- Internal DB field (auto-incrementing)
3+
`rowid` INTEGER PRIMARY KEY AUTOINCREMENT,
4+
5+
-- Site identifier (foreign key to sites table)
6+
`db_site_id` INTEGER NOT NULL REFERENCES sites(id) ON DELETE CASCADE,
7+
8+
-- Top-level non-nullable fields
9+
`id` INTEGER NOT NULL,
10+
`date` TEXT NOT NULL,
11+
`date_gmt` TEXT NOT NULL,
12+
`link` TEXT NOT NULL,
13+
`modified` TEXT NOT NULL,
14+
`modified_gmt` TEXT NOT NULL,
15+
`slug` TEXT NOT NULL,
16+
`status` TEXT NOT NULL,
17+
`post_type` TEXT NOT NULL,
18+
`password` TEXT NOT NULL,
19+
`template` TEXT NOT NULL,
20+
21+
-- Top-level optional fields
22+
`permalink_template` TEXT,
23+
`generated_slug` TEXT,
24+
`author` INTEGER,
25+
`featured_media` INTEGER,
26+
`sticky` INTEGER,
27+
`parent` INTEGER,
28+
`menu_order` INTEGER,
29+
30+
-- Optional enums (stored as TEXT)
31+
`comment_status` TEXT,
32+
`ping_status` TEXT,
33+
`format` TEXT,
34+
35+
-- Complex optional fields (JSON)
36+
`meta` TEXT,
37+
38+
-- Nested: guid (guid is non-optional, but guid.raw is optional)
39+
`guid_raw` TEXT,
40+
`guid_rendered` TEXT NOT NULL,
41+
42+
-- Nested: title (title is non-optional, but title.raw is optional)
43+
`title_raw` TEXT,
44+
`title_rendered` TEXT NOT NULL,
45+
46+
-- Nested: content (content is non-optional, but some fields are optional)
47+
`content_raw` TEXT,
48+
`content_rendered` TEXT NOT NULL,
49+
`content_protected` INTEGER,
50+
`content_block_version` INTEGER,
51+
52+
-- Nested: excerpt (entire struct is optional)
53+
`excerpt_raw` TEXT,
54+
`excerpt_rendered` TEXT,
55+
`excerpt_protected` INTEGER,
56+
57+
-- Client-side cache metadata: when this post was last fetched from the WordPress API
58+
`last_fetched_at` TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
59+
60+
FOREIGN KEY (db_site_id) REFERENCES sites(id) ON DELETE CASCADE
61+
) STRICT;
62+
63+
CREATE UNIQUE INDEX idx_posts_edit_context_unique_db_site_id_and_id ON posts_edit_context(db_site_id, id);
64+
CREATE INDEX idx_posts_edit_context_db_site_id ON posts_edit_context(db_site_id);

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

Lines changed: 0 additions & 14 deletions
This file was deleted.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
CREATE TABLE `term_relationships` (
2+
-- Internal DB field (auto-incrementing)
3+
`rowid` INTEGER PRIMARY KEY AUTOINCREMENT,
4+
5+
-- Site identifier (foreign key to sites table)
6+
`db_site_id` INTEGER NOT NULL,
7+
8+
-- Object identifier (rowid of post/page/nav_menu_item/etc)
9+
-- Note: No FK constraint since this can reference different tables
10+
`object_id` INTEGER NOT NULL,
11+
12+
-- WordPress term ID
13+
`term_id` INTEGER NOT NULL,
14+
15+
-- Taxonomy type ('category', 'post_tag', or custom taxonomy)
16+
`taxonomy_type` TEXT NOT NULL,
17+
18+
FOREIGN KEY (db_site_id) REFERENCES sites(id) ON DELETE CASCADE
19+
) STRICT;
20+
21+
-- Prevent duplicate associations (same object can't have same term twice in same taxonomy)
22+
CREATE UNIQUE INDEX idx_term_relationships_unique
23+
ON term_relationships(db_site_id, object_id, term_id, taxonomy_type);
24+
25+
-- Query: "Find all objects with taxonomy X and term Y"
26+
CREATE INDEX idx_term_relationships_by_term
27+
ON term_relationships(db_site_id, taxonomy_type, term_id);
28+
29+
-- Query: "Find all terms for object X" (used in joins when reading posts)
30+
CREATE INDEX idx_term_relationships_by_object
31+
ON term_relationships(db_site_id, object_id);

0 commit comments

Comments
 (0)