# iOS Core Data Migration Guide: Android Room → iOS Core Data

**Author**: Matthew Raymer  
**Date**: 2025-12-08  
**Status**: 🎯 **ACTIVE** - Database Migration Reference  
**Version**: 1.0.0  
**Last Synced With Plugin Version**: v1.1.0

## Purpose

This document provides a comprehensive mapping guide for migrating Android Room database entities to iOS Core Data entities, ensuring cross-platform data consistency and feature parity.

**Reference**: 
- [Android Database Schema](../android/src/main/java/com/timesafari/dailynotification/DatabaseSchema.kt) - Android Room schema
- [iOS Core Data Model](../ios/Plugin/DailyNotificationModel.xcdatamodeld) - iOS Core Data model
- [Database Consolidation Plan](../android/DATABASE_CONSOLIDATION_PLAN.md) - Unified schema design

---

## 1. Entity Mapping Overview

### 1.1 Complete Entity Mapping

| Android Room Entity | iOS Core Data Entity | Status | Priority |
| ------------------- | -------------------- | ------ | -------- |
| `ContentCache` | `ContentCache` | ✅ Implemented | - |
| `Schedule` | `Schedule` | ✅ Implemented | - |
| `Callback` | `Callback` | ✅ Implemented | - |
| `History` | `History` | ✅ Implemented | - |
| `NotificationContentEntity` | `NotificationContent` | ❌ Missing | **High** |
| `NotificationDeliveryEntity` | `NotificationDelivery` | ❌ Missing | **High** |
| `NotificationConfigEntity` | `NotificationConfig` | ❌ Missing | **Medium** |

### 1.2 Current Implementation Status

**✅ Implemented (4 entities)**:
- `ContentCache` - Fetched content with TTL
- `Schedule` - Recurring schedule patterns
- `Callback` - Callback configurations
- `History` - Execution history

**❌ Missing (3 entities)**:
- `NotificationContent` - Specific notification instances
- `NotificationDelivery` - Delivery tracking/analytics
- `NotificationConfig` - Configuration management

---

## 2. Detailed Entity Mappings

### 2.1 ContentCache Entity

**Status**: ✅ **Implemented**

**Android Room**:
```kotlin
@Entity(tableName = "content_cache")
data class ContentCache(
    @PrimaryKey val id: String,
    val fetchedAt: Long,           // epoch ms
    val ttlSeconds: Int,
    val payload: ByteArray,         // BLOB
    val meta: String? = null
)
```

**iOS Core Data**:
```swift
@objc(ContentCache)
public class ContentCache: NSManagedObject {
    @NSManaged public var id: String?
    @NSManaged public var fetchedAt: Date?
    @NSManaged public var ttlSeconds: Int32
    @NSManaged public var payload: Data?
    @NSManaged public var meta: String?
}
```

**Mapping Notes**:
- ✅ All fields mapped correctly
- ✅ `Long` (epoch ms) → `Date` conversion handled
- ✅ `ByteArray` → `Data` conversion handled
- ✅ Optional fields properly marked

**Migration Status**: ✅ **Complete**

---

### 2.2 Schedule Entity

**Status**: ✅ **Implemented**

**Android Room**:
```kotlin
@Entity(tableName = "schedules")
data class Schedule(
    @PrimaryKey val id: String,
    val kind: String,              // 'fetch' or 'notify'
    val cron: String? = null,
    val clockTime: String? = null, // HH:mm
    val enabled: Boolean = true,
    val lastRunAt: Long? = null,   // epoch ms
    val nextRunAt: Long? = null,   // epoch ms
    val jitterMs: Int = 0,
    val backoffPolicy: String = "exp",
    val stateJson: String? = null
)
```

**iOS Core Data**:
```swift
@objc(Schedule)
public class Schedule: NSManagedObject {
    @NSManaged public var id: String?
    @NSManaged public var kind: String?
    @NSManaged public var cron: String?
    @NSManaged public var clockTime: String?
    @NSManaged public var enabled: Bool
    @NSManaged public var lastRunAt: Date?
    @NSManaged public var nextRunAt: Date?
    @NSManaged public var jitterMs: Int32
    @NSManaged public var backoffPolicy: String?
    @NSManaged public var stateJson: String?
}
```

**Mapping Notes**:
- ✅ All fields mapped correctly
- ✅ `Long` (epoch ms) → `Date` conversion handled
- ✅ Default values preserved
- ✅ Optional fields properly marked

**Migration Status**: ✅ **Complete**

---

### 2.3 Callback Entity

**Status**: ✅ **Implemented**

**Android Room**:
```kotlin
@Entity(tableName = "callbacks")
data class Callback(
    @PrimaryKey val id: String,
    val kind: String,              // 'http', 'local', 'queue'
    val target: String,
    val headersJson: String? = null,
    val enabled: Boolean = true,
    val createdAt: Long            // epoch ms
)
```

**iOS Core Data**:
```swift
@objc(Callback)
public class Callback: NSManagedObject {
    @NSManaged public var id: String?
    @NSManaged public var kind: String?
    @NSManaged public var target: String?
    @NSManaged public var headersJson: String?
    @NSManaged public var enabled: Bool
    @NSManaged public var createdAt: Date?
}
```

**Mapping Notes**:
- ✅ All fields mapped correctly
- ✅ `Long` (epoch ms) → `Date` conversion handled
- ✅ Optional fields properly marked

**Migration Status**: ✅ **Complete**

---

### 2.4 History Entity

**Status**: ✅ **Implemented**

**Android Room**:
```kotlin
@Entity(tableName = "history")
data class History(
    @PrimaryKey(autoGenerate = true) val id: Int = 0,
    val refId: String,
    val kind: String,              // fetch/notify/callback
    val occurredAt: Long,          // epoch ms
    val durationMs: Long? = null,
    val outcome: String,
    val diagJson: String? = null
)
```

**iOS Core Data**:
```swift
@objc(History)
public class History: NSManagedObject {
    @NSManaged public var id: String?
    @NSManaged public var refId: String?
    @NSManaged public var kind: String?
    @NSManaged public var occurredAt: Date?
    @NSManaged public var durationMs: Int32
    @NSManaged public var outcome: String?
    @NSManaged public var diagJson: String?
}
```

**Mapping Notes**:
- ⚠️ `id` type differs: Android uses `Int` (auto-generated), iOS uses `String`
- ✅ `Long` (epoch ms) → `Date` conversion handled
- ✅ Optional fields properly marked

**Migration Consideration**: iOS uses `String` for `id` instead of auto-generated `Int`. This is acceptable as long as IDs are generated as UUIDs.

**Migration Status**: ✅ **Complete** (with note)

---

### 2.5 NotificationContent Entity

**Status**: ❌ **Missing - High Priority**

**Android Room**:
```kotlin
@Entity(tableName = "notification_content")
data class NotificationContentEntity(
    @PrimaryKey val id: String,
    val pluginVersion: String?,
    val timesafariDid: String?,
    val notificationType: String?,
    val title: String?,
    val body: String?,
    val scheduledTime: Long,        // epoch ms
    val timezone: String?,
    val priority: Int,
    val vibrationEnabled: Boolean,
    val soundEnabled: Boolean,
    val mediaUrl: String?,
    val encryptedContent: String?,
    val encryptionKeyId: String?,
    val createdAt: Long,           // epoch ms
    val updatedAt: Long,           // epoch ms
    val ttlSeconds: Long,
    val deliveryStatus: String?,
    val deliveryAttempts: Int,
    val lastDeliveryAttempt: Long, // epoch ms
    val userInteractionCount: Int,
    val lastUserInteraction: Long, // epoch ms
    val metadata: String?
)
```

**Required iOS Core Data Entity**:
```swift
@objc(NotificationContent)
public class NotificationContent: NSManagedObject {
    @NSManaged public var id: String?
    @NSManaged public var pluginVersion: String?
    @NSManaged public var timesafariDid: String?
    @NSManaged public var notificationType: String?
    @NSManaged public var title: String?
    @NSManaged public var body: String?
    @NSManaged public var scheduledTime: Date?
    @NSManaged public var timezone: String?
    @NSManaged public var priority: Int32
    @NSManaged public var vibrationEnabled: Bool
    @NSManaged public var soundEnabled: Bool
    @NSManaged public var mediaUrl: String?
    @NSManaged public var encryptedContent: String?
    @NSManaged public var encryptionKeyId: String?
    @NSManaged public var createdAt: Date?
    @NSManaged public var updatedAt: Date?
    @NSManaged public var ttlSeconds: Int64
    @NSManaged public var deliveryStatus: String?
    @NSManaged public var deliveryAttempts: Int32
    @NSManaged public var lastDeliveryAttempt: Date?
    @NSManaged public var userInteractionCount: Int32
    @NSManaged public var lastUserInteraction: Date?
    @NSManaged public var metadata: String?
}
```

**Required Core Data Model XML**:
```xml
<entity name="NotificationContent" representedClassName="NotificationContent" syncable="YES" codeGenerationType="class">
    <attribute name="id" optional="NO" attributeType="String"/>
    <attribute name="pluginVersion" optional="YES" attributeType="String"/>
    <attribute name="timesafariDid" optional="YES" attributeType="String"/>
    <attribute name="notificationType" optional="YES" attributeType="String"/>
    <attribute name="title" optional="YES" attributeType="String"/>
    <attribute name="body" optional="YES" attributeType="String"/>
    <attribute name="scheduledTime" optional="NO" attributeType="Date" usesScalarValueType="NO"/>
    <attribute name="timezone" optional="YES" attributeType="String"/>
    <attribute name="priority" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
    <attribute name="vibrationEnabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
    <attribute name="soundEnabled" optional="YES" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
    <attribute name="mediaUrl" optional="YES" attributeType="String"/>
    <attribute name="encryptedContent" optional="YES" attributeType="String"/>
    <attribute name="encryptionKeyId" optional="YES" attributeType="String"/>
    <attribute name="createdAt" optional="NO" attributeType="Date" usesScalarValueType="NO"/>
    <attribute name="updatedAt" optional="NO" attributeType="Date" usesScalarValueType="NO"/>
    <attribute name="ttlSeconds" optional="YES" attributeType="Integer 64" defaultValueString="604800" usesScalarValueType="YES"/>
    <attribute name="deliveryStatus" optional="YES" attributeType="String"/>
    <attribute name="deliveryAttempts" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
    <attribute name="lastDeliveryAttempt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
    <attribute name="userInteractionCount" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
    <attribute name="lastUserInteraction" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
    <attribute name="metadata" optional="YES" attributeType="String"/>
    <index name="index_notification_content_timesafari_did">
        <indexElement value="timesafariDid"/>
    </index>
    <index name="index_notification_content_notification_type">
        <indexElement value="notificationType"/>
    </index>
    <index name="index_notification_content_scheduled_time">
        <indexElement value="scheduledTime"/>
    </index>
</entity>
```

**Mapping Notes**:
- `Long` (epoch ms) → `Date` conversion required
- `Int` → `Int32` conversion
- `Long` (ttlSeconds) → `Int64` conversion
- Indexes should be added for performance

**Migration Status**: ❌ **Not Implemented**

---

### 2.6 NotificationDelivery Entity

**Status**: ❌ **Missing - High Priority**

**Android Room**:
```kotlin
@Entity(
    tableName = "notification_delivery",
    foreignKeys = @ForeignKey(
        entity = NotificationContentEntity::class,
        parentColumns = ["id"],
        childColumns = ["notification_id"],
        onDelete = ForeignKey.CASCADE
    )
)
data class NotificationDeliveryEntity(
    @PrimaryKey val id: String,
    val notificationId: String,
    val timesafariDid: String?,
    val deliveryTimestamp: Long,  // epoch ms
    val deliveryStatus: String?,
    val deliveryMethod: String?,
    val deliveryAttemptNumber: Int,
    val deliveryDurationMs: Long,
    val userInteractionType: String?,
    val userInteractionTimestamp: Long, // epoch ms
    val userInteractionDurationMs: Long,
    val errorCode: String?,
    val errorMessage: String?,
    val deviceInfo: String?,
    val networkInfo: String?,
    val batteryLevel: Int,
    val dozeModeActive: Boolean,
    val exactAlarmPermission: Boolean,
    val notificationPermission: Boolean,
    val metadata: String?
)
```

**Required iOS Core Data Entity**:
```swift
@objc(NotificationDelivery)
public class NotificationDelivery: NSManagedObject {
    @NSManaged public var id: String?
    @NSManaged public var notificationId: String?
    @NSManaged public var notificationContent: NotificationContent? // Relationship
    @NSManaged public var timesafariDid: String?
    @NSManaged public var deliveryTimestamp: Date?
    @NSManaged public var deliveryStatus: String?
    @NSManaged public var deliveryMethod: String?
    @NSManaged public var deliveryAttemptNumber: Int32
    @NSManaged public var deliveryDurationMs: Int64
    @NSManaged public var userInteractionType: String?
    @NSManaged public var userInteractionTimestamp: Date?
    @NSManaged public var userInteractionDurationMs: Int64
    @NSManaged public var errorCode: String?
    @NSManaged public var errorMessage: String?
    @NSManaged public var deviceInfo: String?
    @NSManaged public var networkInfo: String?
    @NSManaged public var batteryLevel: Int32
    @NSManaged public var dozeModeActive: Bool
    @NSManaged public var exactAlarmPermission: Bool
    @NSManaged public var notificationPermission: Bool
    @NSManaged public var metadata: String?
}
```

**Required Core Data Model XML**:
```xml
<entity name="NotificationDelivery" representedClassName="NotificationDelivery" syncable="YES" codeGenerationType="class">
    <attribute name="id" optional="NO" attributeType="String"/>
    <attribute name="notificationId" optional="YES" attributeType="String"/>
    <attribute name="timesafariDid" optional="YES" attributeType="String"/>
    <attribute name="deliveryTimestamp" optional="NO" attributeType="Date" usesScalarValueType="NO"/>
    <attribute name="deliveryStatus" optional="YES" attributeType="String"/>
    <attribute name="deliveryMethod" optional="YES" attributeType="String"/>
    <attribute name="deliveryAttemptNumber" optional="YES" attributeType="Integer 32" defaultValueString="1" usesScalarValueType="YES"/>
    <attribute name="deliveryDurationMs" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
    <attribute name="userInteractionType" optional="YES" attributeType="String"/>
    <attribute name="userInteractionTimestamp" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
    <attribute name="userInteractionDurationMs" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
    <attribute name="errorCode" optional="YES" attributeType="String"/>
    <attribute name="errorMessage" optional="YES" attributeType="String"/>
    <attribute name="deviceInfo" optional="YES" attributeType="String"/>
    <attribute name="networkInfo" optional="YES" attributeType="String"/>
    <attribute name="batteryLevel" optional="YES" attributeType="Integer 32" defaultValueString="-1" usesScalarValueType="YES"/>
    <attribute name="dozeModeActive" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
    <attribute name="exactAlarmPermission" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
    <attribute name="notificationPermission" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
    <attribute name="metadata" optional="YES" attributeType="String"/>
    <relationship name="notificationContent" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="NotificationContent" inverseName="deliveries" inverseEntity="NotificationContent"/>
    <index name="index_notification_delivery_notification_id">
        <indexElement value="notificationId"/>
    </index>
    <index name="index_notification_delivery_delivery_timestamp">
        <indexElement value="deliveryTimestamp"/>
    </index>
</entity>
```

**Mapping Notes**:
- Foreign key relationship should be modeled as Core Data relationship
- `Long` (epoch ms) → `Date` conversion required
- `Int` → `Int32` conversion
- `Long` (duration) → `Int64` conversion
- Cascade delete should be configured

**Migration Status**: ❌ **Not Implemented**

---

### 2.7 NotificationConfig Entity

**Status**: ❌ **Missing - Medium Priority**

**Android Room**:
```kotlin
@Entity(tableName = "notification_config")
data class NotificationConfigEntity(
    @PrimaryKey val id: String,
    val timesafariDid: String?,
    val configType: String?,
    val configKey: String?,
    val configValue: String?,
    val configDataType: String?,
    val isEncrypted: Boolean,
    val encryptionKeyId: String?,
    val createdAt: Long,           // epoch ms
    val updatedAt: Long,           // epoch ms
    val ttlSeconds: Long,
    val isActive: Boolean,
    val metadata: String?
)
```

**Required iOS Core Data Entity**:
```swift
@objc(NotificationConfig)
public class NotificationConfig: NSManagedObject {
    @NSManaged public var id: String?
    @NSManaged public var timesafariDid: String?
    @NSManaged public var configType: String?
    @NSManaged public var configKey: String?
    @NSManaged public var configValue: String?
    @NSManaged public var configDataType: String?
    @NSManaged public var isEncrypted: Bool
    @NSManaged public var encryptionKeyId: String?
    @NSManaged public var createdAt: Date?
    @NSManaged public var updatedAt: Date?
    @NSManaged public var ttlSeconds: Int64
    @NSManaged public var isActive: Bool
    @NSManaged public var metadata: String?
}
```

**Mapping Notes**:
- `Long` (epoch ms) → `Date` conversion required
- `Long` (ttlSeconds) → `Int64` conversion
- Indexes should be added for performance

**Migration Status**: ❌ **Not Implemented**

---

## 3. Data Type Conversions

### 3.1 Time Conversions

**Android → iOS**:
- `Long` (epoch milliseconds) → `Date`
- Conversion: `Date(timeIntervalSince1970: Double(milliseconds) / 1000.0)`

**iOS → Android**:
- `Date` → `Long` (epoch milliseconds)
- Conversion: `Int64(date.timeIntervalSince1970 * 1000)`

### 3.2 Numeric Conversions

| Android Type | iOS Type | Notes |
| ------------ | -------- | ----- |
| `Int` | `Int32` | Direct mapping |
| `Long` | `Int64` | For large values |
| `Boolean` | `Bool` | Direct mapping |
| `ByteArray` | `Data` | Binary data |

### 3.3 String Conversions

- `String?` → `String?` (direct mapping)
- JSON fields: `String?` → `String?` (parse as needed)

---

## 4. Index Mapping

### 4.1 Required Indexes

**NotificationContent**:
- `timesafariDid` (for user queries)
- `notificationType` (for type filtering)
- `scheduledTime` (for time-based queries)
- `createdAt` (for chronological queries)

**NotificationDelivery**:
- `notificationId` (for foreign key lookups)
- `deliveryTimestamp` (for time-based queries)
- `deliveryStatus` (for status filtering)
- `timesafariDid` (for user queries)

**NotificationConfig**:
- `timesafariDid` (for user queries)
- `configType` (for type filtering)
- `updatedAt` (for chronological queries)

---

## 5. Relationship Mapping

### 5.1 Foreign Key Relationships

**Android Room**:
```kotlin
@ForeignKey(
    entity = NotificationContentEntity::class,
    parentColumns = ["id"],
    childColumns = ["notification_id"],
    onDelete = ForeignKey.CASCADE
)
```

**iOS Core Data**:
```xml
<relationship 
    name="notificationContent" 
    optional="YES" 
    maxCount="1" 
    deletionRule="Cascade" 
    destinationEntity="NotificationContent" 
    inverseName="deliveries" 
    inverseEntity="NotificationContent"/>
```

**Inverse Relationship** (NotificationContent → NotificationDelivery):
```xml
<relationship 
    name="deliveries" 
    optional="YES" 
    toMany="YES" 
    deletionRule="Nullify" 
    destinationEntity="NotificationDelivery" 
    inverseName="notificationContent" 
    inverseEntity="NotificationDelivery"/>
```

---

## 6. Implementation Checklist

### 6.1 High Priority (Required for Feature Parity)

- [ ] Add `NotificationContent` entity to Core Data model
- [ ] Add `NotificationDelivery` entity to Core Data model
- [ ] Configure foreign key relationship between `NotificationContent` and `NotificationDelivery`
- [ ] Add required indexes for performance
- [ ] Implement Swift extensions for entity classes
- [ ] Add data conversion helpers (Date ↔ Long)
- [ ] Test entity creation and relationships
- [ ] Test cascade delete behavior

### 6.2 Medium Priority (Configuration Management)

- [ ] Add `NotificationConfig` entity to Core Data model
- [ ] Add required indexes
- [ ] Implement Swift extensions
- [ ] Test configuration CRUD operations

### 6.3 Low Priority (Optimization)

- [ ] Add migration policies for schema changes
- [ ] Add data validation rules
- [ ] Optimize fetch requests with predicates
- [ ] Add batch operations support

---

## 7. Migration Steps

### 7.1 Step 1: Update Core Data Model

1. Open `DailyNotificationModel.xcdatamodeld` in Xcode
2. Add `NotificationContent` entity with all attributes
3. Add `NotificationDelivery` entity with all attributes
4. Add `NotificationConfig` entity with all attributes
5. Configure relationships between entities
6. Add indexes for performance
7. Set code generation to "Class Definition"

### 7.2 Step 2: Create Swift Extensions

1. Create `NotificationContent+CoreDataClass.swift`
2. Create `NotificationContent+CoreDataProperties.swift`
3. Create `NotificationDelivery+CoreDataClass.swift`
4. Create `NotificationDelivery+CoreDataProperties.swift`
5. Create `NotificationConfig+CoreDataClass.swift`
6. Create `NotificationConfig+CoreDataProperties.swift`

### 7.3 Step 3: Implement Data Access Layer

1. Create DAO classes for each entity
2. Implement CRUD operations
3. Add data conversion helpers
4. Add query methods with predicates

### 7.4 Step 4: Update Persistence Controller

1. Update `PersistenceController` to handle new entities
2. Add migration policies if needed
3. Test database initialization

### 7.5 Step 5: Testing

1. Test entity creation
2. Test relationships
3. Test cascade delete
4. Test data conversion (Date ↔ Long)
5. Test query performance with indexes

---

## 8. Data Migration (If Needed)

### 8.1 Migration from SQLite to Core Data

If migrating existing SQLite data to Core Data:

1. Read data from SQLite database
2. Convert data types (Long → Date, etc.)
3. Create Core Data entities
4. Save to Core Data store
5. Verify data integrity

### 8.2 Migration Script Example

```swift
func migrateSQLiteToCoreData(sqlitePath: String, coreDataStack: NSPersistentContainer) {
    // 1. Open SQLite database
    // 2. Query all tables
    // 3. Convert each row to Core Data entity
    // 4. Save to Core Data store
    // 5. Verify migration success
}
```

---

## 9. References

- [Android Database Schema](../android/src/main/java/com/timesafari/dailynotification/DatabaseSchema.kt)
- [iOS Core Data Model](../ios/Plugin/DailyNotificationModel.xcdatamodeld)
- [Database Consolidation Plan](../android/DATABASE_CONSOLIDATION_PLAN.md)
- [Core Data Programming Guide](https://developer.apple.com/documentation/coredata)

---

**Document Version**: 1.0.0  
**Last Updated**: 2025-12-08  
**Next Review**: After missing entities are implemented

