xiaohongshu
xiaohongshu
kotlin-schema-first-entities
Use when creating Room entities for Kotlin/Android apps that mirror Django/PostgreSQL backend schemas. Enforces exact field alignment, prevents schema drift, validates type mappings. Critical for maintaining zero impedance mismatch between backend and mobile.
Full skill instructions
MANDATORY when:
@Entity class for Room databaseTriggers:
@Entity annotationAndroid SQLite is a REPLICA of PostgreSQL, not an independent schema.
Schema Authority Hierarchy:
1. PostgreSQL (Django Models) ← SOURCE OF TRUTH
2. Pydantic Validation ← Contract enforcement
3. OpenAPI Schema ← API contract
4. Kotlin DTOs ← Generated from OpenAPI
5. Room Entities ← Hand-coded to match DTOs ± denormalization
STOP and complete this checklist:
Step 1: Locate schema mapping documentation
docs/api-contracts/POSTGRESQL_TO_SQLITE_SCHEMA_MAPPING.md (or equivalent)Step 2: Find the corresponding section
Step 3: Copy EXACT field list
Step 4: Verify type mappings
Step 5: Document denormalization
Step 6: Add KDoc with schema authority
/**
* ⚠️ SCHEMA AUTHORITY: Django model apps/domain/models/model_name.py
*
* FIELD MAPPING: [list Django fields → Kotlin properties]
*
* DENORMALIZATION: [if applicable, explain what's merged and why]
*
* Last Verified Against Django: YYYY-MM-DD
*/
@Entity(tableName = "table_name") // ← PostgreSQL table name
data class EntityName(
@ColumnInfo(name = "django_field") // ← MUST be PostgreSQL column name
val kotlinProperty: Type // ← CAN be idiomatic Kotlin name
)
Example:
@Entity(tableName = "peopleeventlog")
data class AttendanceEntity(
@ColumnInfo(name = "punchintime") // ← Django field (PostgreSQL column)
val checkInTime: Long, // ← Kotlin property (camelCase, descriptive)
@ColumnInfo(name = "people") // ← Django FK field
val personId: Int, // ← Kotlin property (explicit it's ID)
@ColumnInfo(name = "peoplename") // ← Django field
val fullName: String // ← Kotlin property (more descriptive)
)
Rule:
@ColumnInfo(name) = Django field name (becomes SQLite column)@Entity(
tableName = "peopleeventlog",
indices = [
Index(value = ["tenant", "cdtz"]), // ← From Django Meta.indexes
Index(value = ["tenant", "people"]), // ← Column names are Django fields
Index(value = ["tenant", "datefor"]),
Index(value = ["tenant", "people", "shift", "datefor"],
name = "pel_validation_lookup_idx") // ← Composite index
]
)
Rule: Copy PostgreSQL indexes exactly (use Django field names in value arrays).
@Entity(
foreignKeys = [
ForeignKey(
entity = PersonEntity::class,
parentColumns = ["id"],
childColumns = ["people"], // ← Django field name
onDelete = ForeignKey.RESTRICT // ← Matches Django on_delete
)
]
)
data class AttendanceEntity(
@ColumnInfo(name = "people") val personId: Int // ← Store FK ID
)
Django on_delete mapping:
models.CASCADE → ForeignKey.CASCADEmodels.RESTRICT → ForeignKey.RESTRICTmodels.SET_NULL → ForeignKey.SET_NULLCRITICAL: Enable FK constraints in database:
override fun init(configuration: DatabaseConfiguration) {
super.init(configuration)
openHelper.writableDatabase.execSQL("PRAGMA foreign_keys=ON;")
}
For DateTime (MANDATORY):
@TypeConverter
fun fromTimestamp(value: Long?): Instant? =
value?.let { Instant.fromEpochMilliseconds(it) }
@TypeConverter
fun toTimestamp(instant: Instant?): Long? =
instant?.toEpochMilliseconds()
For Typed JSON (REQUIRED for known structures):
// ❌ WRONG: Untyped Map
@ColumnInfo(name = "capabilities") val capabilities: Map<String, Any>
// ✅ CORRECT: Typed data class
@Serializable
data class UserCapabilities(
@SerialName("can_use_ai_query") val canUseAiQuery: Boolean = false,
// ... match Django JSONField structure EXACTLY
)
@TypeConverter
fun fromCapabilities(value: UserCapabilities): String =
Json.encodeToString(value)
@TypeConverter
fun toCapabilities(value: String): UserCapabilities =
Json.decodeFromString(value)
// ❌ WRONG: Field not in Django model
@Entity
data class UserEntity(
val firstName: String, // ← Django doesn't have this!
val lastName: String, // ← Django has "peoplename" only!
val isFavorite: Boolean // ← Not in Django model!
)
Consequence: Sync failures, data loss, schema drift.
How to detect: Missing field in schema mapping doc = FORBIDDEN
// ❌ WRONG: Type mismatch
@ColumnInfo(name = "department") val department: String // ← Django has FK (Int)
@ColumnInfo(name = "punchintime") val checkInTime: String // ← Django DateTimeField → Long
@ColumnInfo(name = "hours_worked") val hoursWorked: Double // ← Django DecimalField → String
Consequence: Type errors, precision loss, sync failures.
How to fix: Check Section 1 type mapping table.
// ❌ WRONG: Generating server-authoritative timestamps
@Entity
data class AttendanceEntity(
val createdAt: Long = Instant.now().toEpochMilliseconds() // ← Django auto_now_add
)
Consequence: Timestamp mismatch between client and server, audit trail corruption.
How to fix: Server-authoritative timestamps come from API response.
// ❌ WRONG: Known structure as untyped map
@ColumnInfo(name = "capabilities") val capabilities: Map<String, Boolean>
Consequence: Runtime errors, no compile-time safety, typos in keys.
How to fix: Create typed data class matching Django JSONField default.
/**
* Django People model has 20+ fields.
* This entity must include ALL exposed via API.
*
* Verified field count: 30 (People: 15, Profile: 8, Organizational: 7)
*/
@Entity
data class UserEntity(
// List all 30 fields here...
)
Test: Count fields in Django model = Count fields in Room entity (± documented exclusions).
// Django TextChoices
class AssignmentStatus(models.TextChoices):
SCHEDULED = 'SCHEDULED', 'Scheduled'
CONFIRMED = 'CONFIRMED', 'Confirmed'
IN_PROGRESS = 'IN_PROGRESS', 'In Progress'
// Kotlin enum MUST match EXACTLY (case-sensitive)
enum class AssignmentStatus {
@SerialName("SCHEDULED") SCHEDULED,
@SerialName("CONFIRMED") CONFIRMED,
@SerialName("IN_PROGRESS") IN_PROGRESS
}
// ❌ WRONG: Different values
enum class AssignmentStatus {
SCHEDULED, // Missing @SerialName - won't deserialize correctly
Confirmed, // Wrong case - won't match Django
InProgress // Wrong format - won't match Django
}
Write test:
@Test
fun `enum values match Django choices`() {
val expected = setOf("SCHEDULED", "CONFIRMED", "IN_PROGRESS")
val actual = AssignmentStatus.values().map { it.name }.toSet()
assertEquals(expected, actual)
}
@Test
fun `DateTime conversion is lossless`() {
val original = Instant.parse("2025-11-08T14:30:00Z")
val epoch = original.toEpochMilliseconds()
val restored = Instant.fromEpochMilliseconds(epoch)
assertEquals(original, restored)
}
@Test
fun `Decimal conversion preserves precision`() {
val original = BigDecimal("8.123456")
val text = original.toPlainString()
val restored = text.toBigDecimal()
assertEquals(original, restored)
}
Before marking Room entity complete:
If ANY checkbox is unchecked → Entity is NOT complete.
✅ Room entity compiles without errors ✅ KSP generates DAO implementation successfully ✅ Type conversion tests pass (round-trip lossless) ✅ Enum validation tests pass (match Django choices) ✅ SQLite column names match PostgreSQL exactly ✅ No fields invented (all from Django model) ✅ No type mismatches (all per mapping table) ✅ All complex types have TypeConverters
Primary: docs/api-contracts/POSTGRESQL_TO_SQLITE_SCHEMA_MAPPING.md
Secondary: docs/api-contracts/SCHEMA_FIRST_DATA_CONTRACT_VALIDATION.md
Skill Guides: docs/api-contracts/skills/ROOM_IMPLEMENTATION_GUIDE.md
Use with:
room-database-implementation - For Room-specific errors (missing TypeConverters, FK issues)kotlin-coroutines-safety - For DAO suspend functions and Flowoffline-first-architecture - For sync status fields and pending operationsThis skill adds: Schema alignment verification on TOP of Room implementation best practices.
/**
* Room Entity for Attendance records (Check-in/Check-out).
*
* ⚠️ SCHEMA AUTHORITY: Django model
* - File: apps/attendance/models/people_eventlog.py
* - Model: PeopleEventlog
*
* DENORMALIZATION: None (exact replica of Django model)
*
* FIELD MAPPING (per POSTGRESQL_TO_SQLITE_SCHEMA_MAPPING.md Section 1.1):
* - Django punchintime (DateTimeField) → Kotlin Long (epoch ms)
* - Django people (ForeignKey) → Kotlin Int (person ID)
* - Django startlocation (PointField) → Kotlin Double lat/lng (denormalized)
* - Django peventlogextras (EncryptedJSONField) → Kotlin String (encrypted JSON)
*
* Last Verified: 2025-11-08
*/
@Entity(
tableName = "peopleeventlog", // ← PostgreSQL table name
foreignKeys = [
ForeignKey(
entity = PersonEntity::class,
parentColumns = ["id"],
childColumns = ["people"],
onDelete = ForeignKey.RESTRICT // ← Matches Django on_delete
)
],
indices = [
Index(value = ["tenant", "cdtz"]), // ← From Django Meta.indexes
Index(value = ["tenant", "people"]),
Index(value = ["tenant", "datefor"])
]
)
data class AttendanceEntity(
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "id") val id: Int? = null,
@ColumnInfo(name = "uuid")
val uuid: String = UUID.randomUUID().toString(),
@ColumnInfo(name = "people") // ← Django FK field name
val personId: Int?, // ← Kotlin property (explicit)
@ColumnInfo(name = "client")
val clientId: Int?,
@ColumnInfo(name = "bu")
val buId: Int?,
@ColumnInfo(name = "punchintime") // ← Django DateTimeField
val checkInTime: Long?, // ← Long epoch (per mapping table)
@ColumnInfo(name = "punchouttime")
val checkOutTime: Long?,
@ColumnInfo(name = "datefor") // ← Django DateField
val attendanceDate: String?, // ← ISO8601 date string
@ColumnInfo(name = "start_latitude") // ← Denormalized from PointField
val startLatitude: Double?,
@ColumnInfo(name = "start_longitude")
val startLongitude: Double?,
@ColumnInfo(name = "peventlogextras") // ← EncryptedJSONField
val eventExtras: String = "{}", // ← Store encrypted JSON
@ColumnInfo(name = "version") // ← VersionField
val version: Int = 1, // ← Optimistic locking
@ColumnInfo(name = "tenant") // ← Multi-tenant FK
val tenantId: Int?,
@ColumnInfo(name = "cdtz") // ← auto_now_add
val createdAt: Long, // ← Server-authoritative
@ColumnInfo(name = "mdtz") // ← auto_now
val updatedAt: Long, // ← Server-authoritative
// Sync metadata (client-managed)
@ColumnInfo(name = "mobile_id")
val mobileId: String = UUID.randomUUID().toString(),
@ColumnInfo(name = "sync_status")
val syncStatus: String = "pending"
)
@Test
fun `UserEntity has all fields from schema mapping doc`() {
// Reference field list from schema doc
val expectedFields = setOf(
"id", "peoplecode", "peoplename", "loginid", "email",
"isadmin", "is_staff", "enable", "gender", "dateofbirth",
"dateofjoin", "department", "designation", "client", "bu",
"capabilities", "tenant", "cdtz", "mdtz"
// ... all 30 fields
)
val actualFields = UserEntity::class.memberProperties
.mapNotNull { it.findAnnotation<ColumnInfo>()?.name }
.toSet()
val missing = expectedFields - actualFields
val extra = actualFields - expectedFields
assertTrue(missing.isEmpty(), "Missing Django fields: $missing")
assertTrue(extra.isEmpty(), "Extra fields not in Django: $extra")
}
@Test
fun `UserCapabilities has exact fields from Django default_capabilities`() {
val expectedKeys = setOf(
"can_use_ai_query",
"can_use_nlp_search",
"can_use_voice_commands",
"can_use_advanced_analytics",
"can_use_predictive_insights",
"can_use_automated_scheduling",
"can_use_smart_recommendations"
)
val json = Json.encodeToString(UserCapabilities())
val jsonObject = Json.parseToJsonElement(json).jsonObject
assertEquals(expectedKeys, jsonObject.keys)
}
Symptom: You want to add field notes: String to entity.
Check: Is notes in the Django model section of schema mapping doc?
Fix: If field is needed, add to Django model FIRST, then update mobile.
Symptom: Capabilities test fails - JSON keys don't match.
Check: Schema mapping doc has exact capabilities structure.
Fix:
// Read schema doc Section 1.2
// Copy EXACT field list for capabilities
// Do NOT invent permission fields
Symptom:
// DTO has: departmentName: String (from API)
// Entity has: @ColumnInfo(name = "department") val departmentId: Int
This is CORRECT if:
Mapper handles enrichment:
fun UserDto.toEntity() = UserEntity(
departmentId = this.departmentId // Use ID from DTO, ignore name
)
Green flags (entity is correct):
Red flags (entity is WRONG):
This skill enforces:
Use this skill BEFORE room-database-implementation skill - it ensures schema alignment, then room-database-implementation ensures Room-specific correctness.
Prevents: Schema drift, sync failures, type mismatches, data loss.
Ensures: Perfect PostgreSQL → SQLite replica with zero impedance mismatch.
xiaohongshu
technical spec
product ux expert
database patterns
Conduct multi-agent task orchestration and workflow coordination.
Initialize project with Conductor artifacts (product definition,
Expert in web animations, transitions, and motion design using Framer Motion and CSS
Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...
PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.
Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...
Display project status, active tracks, and next actions
Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.
Generate, edit, and beat-sync AI video with leading models in one workspace.
The world's fastest calendar for remote work
Transform Your Design with AI Designer by ImgCreator.ai
Revolutionizing Video Production with AI-Powered Creativity
Extend an image past the frame and let AI fill the new aspect ratio.
Discover your celebrity doppelgänger with StarByFace!
ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.
Opus.ai: Revolutionize Your Web Experience