Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 41 additions & 16 deletions core/data/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
load("@rules_kotlin//kotlin:jvm.bzl", "kt_jvm_library", "kt_jvm_test")

package(default_visibility = ["//visibility:public"])

kt_jvm_library(
name = "data",
srcs = glob(["src/main/kotlin/**/*.kt"]),
Expand All @@ -15,67 +13,94 @@ kt_jvm_library(

kt_jvm_test(
name = "seed_database_test",
size = "small",
srcs = ["src/test/kotlin/com/anysoftkeyboard/calque/data/seed/SeedDatabaseTest.kt"],
test_class = "com.anysoftkeyboard.calque.data.seed.SeedDatabaseTest",
deps = [
":data",
"//core/linguistics",
"@maven//:junit_junit",
"@maven//:org_xerial_sqlite_jdbc",
],
)

kt_jvm_test(
name = "seed_dictionary_repository_test",
size = "small",
srcs = ["src/test/kotlin/com/anysoftkeyboard/calque/data/seed/SeedDictionaryRepositoryTest.kt"],
test_class = "com.anysoftkeyboard.calque.data.seed.SeedDictionaryRepositoryTest",
deps = [
":data",
"//core/linguistics",
"@maven//:junit_junit",
"@maven//:org_xerial_sqlite_jdbc",
],
)

kt_jvm_test(
name = "seed_false_cognates_repository_test",
size = "small",
srcs = ["src/test/kotlin/com/anysoftkeyboard/calque/data/seed/SeedFalseCognatesRepositoryTest.kt"],
test_class = "com.anysoftkeyboard.calque.data.seed.SeedFalseCognatesRepositoryTest",
deps = [
":data",
"//core/linguistics",
"@maven//:junit_junit",
"@maven//:org_xerial_sqlite_jdbc",
],
)

kt_jvm_test(
name = "seed_curated_lessons_repository_test",
size = "small",
srcs = ["src/test/kotlin/com/anysoftkeyboard/calque/data/seed/SeedCuratedLessonsRepositoryTest.kt"],
test_class = "com.anysoftkeyboard.calque.data.seed.SeedCuratedLessonsRepositoryTest",
deps = [
":data",
"//core/linguistics",
"@maven//:junit_junit",
"@maven//:org_jetbrains_kotlinx_kotlinx_serialization_json",
"@maven//:org_xerial_sqlite_jdbc",
],
)

kt_jvm_test(
name = "seed_cognate_rules_repository_test",
size = "small",
srcs = ["src/test/kotlin/com/anysoftkeyboard/calque/data/seed/SeedCognateRulesRepositoryTest.kt"],
test_class = "com.anysoftkeyboard.calque.data.seed.SeedCognateRulesRepositoryTest",
deps = [
":data",
"@maven//:junit_junit",
],
)

kt_jvm_test(
name = "user_database_test",
srcs = ["src/test/kotlin/com/anysoftkeyboard/calque/data/user/UserDatabaseTest.kt"],
test_class = "com.anysoftkeyboard.calque.data.user.UserDatabaseTest",
deps = [
":data",
"@maven//:junit_junit",
],
)

kt_jvm_test(
name = "user_word_state_repository_test",
srcs = ["src/test/kotlin/com/anysoftkeyboard/calque/data/user/UserWordStateRepositoryTest.kt"],
test_class = "com.anysoftkeyboard.calque.data.user.UserWordStateRepositoryTest",
deps = [
":data",
"//core/linguistics",
"@maven//:junit_junit",
],
)

kt_jvm_test(
name = "user_custom_text_repository_test",
srcs = ["src/test/kotlin/com/anysoftkeyboard/calque/data/user/UserCustomTextRepositoryTest.kt"],
test_class = "com.anysoftkeyboard.calque.data.user.UserCustomTextRepositoryTest",
deps = [
":data",
"@maven//:junit_junit",
],
)

kt_jvm_test(
name = "calque_word_lookup_provider_test",
srcs = ["src/test/kotlin/com/anysoftkeyboard/calque/data/CalqueWordLookupProviderTest.kt"],
test_class = "com.anysoftkeyboard.calque.data.CalqueWordLookupProviderTest",
deps = [
":data",
"//core/linguistics",
"@maven//:junit_junit",
"@maven//:org_jetbrains_kotlinx_kotlinx_serialization_json",
"@maven//:org_xerial_sqlite_jdbc",
],
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.anysoftkeyboard.calque.data

import com.anysoftkeyboard.calque.data.seed.DictionaryRepository
import com.anysoftkeyboard.calque.data.seed.FalseCognatesRepository
import com.anysoftkeyboard.calque.data.user.UserWordStateRepository
import com.anysoftkeyboard.calque.data.user.batchGetWordStatuses
import com.anysoftkeyboard.calque.linguistics.DictionaryEntry
import com.anysoftkeyboard.calque.linguistics.FalseCognateEntry
import com.anysoftkeyboard.calque.linguistics.UserWordStatus
import com.anysoftkeyboard.calque.linguistics.WordLookupProvider

/**
* Composite [WordLookupProvider] implementation bridging [DictionaryRepository],
* [FalseCognatesRepository], and [UserWordStateRepository] for reader enrichment and linguistic
* lookups.
*/
class CalqueWordLookupProvider(
private val dictionaryRepository: DictionaryRepository,
private val falseCognatesRepository: FalseCognatesRepository,
private val userWordStateRepository: UserWordStateRepository,
) : WordLookupProvider {

override fun batchLookupDictionary(words: Set<String>): Map<String, DictionaryEntry> {
return dictionaryRepository.getBySurfaceForms(words)
}

override fun batchLookupFalseCognates(words: Set<String>): Map<String, FalseCognateEntry> {
return falseCognatesRepository.getBySurfaceForms(words)
}

override fun batchLookupUserState(lemmas: Set<String>): Map<String, UserWordStatus> {
return userWordStateRepository.batchGetWordStatuses(lemmas)
}

override fun lookupSingleWord(word: String): DictionaryEntry? {
return dictionaryRepository.getBySurfaceForm(word)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.anysoftkeyboard.calque.data.models

/** Domain model representing user-imported reading passages and optional AI-simplified versions. */
data class UserCustomText(
val textId: Long = 0L,
val title: String,
val originalSpanish: String,
val simplifiedSpanish: String? = null,
val readabilityLevel: Int = 0,
val createdTimestamp: Long = 0L,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.anysoftkeyboard.calque.data.models

import com.anysoftkeyboard.calque.linguistics.UserWordStatus

/**
* Domain model representing a learner's personal state and vault status for a vocabulary [lemma].
*/
data class UserWordState(
val lemma: String,
val status: UserWordStatus = UserWordStatus.NEW,
val starred: Boolean = false,
val savedContextSentence: String? = null,
val encounterCount: Int = 0,
val lastSeenTimestamp: Long = 0L,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
package com.anysoftkeyboard.calque.data.user

import com.anysoftkeyboard.calque.data.models.UserCustomText
import java.sql.Connection
import java.sql.ResultSet
import java.sql.Statement
import java.sql.Types

/** Repository interface for managing user-imported custom texts and simplified versions. */
interface UserCustomTextRepository {
/** Inserts a new custom text and returns its auto-generated textId. */
fun insert(text: UserCustomText): Long

/**
* Updates an existing custom text by its [UserCustomText.textId]. Returns true if found and
* updated.
*/
fun update(text: UserCustomText): Boolean

/** Retrieves a custom text entry by [textId]. */
fun getById(textId: Long): UserCustomText?

/** Retrieves all saved custom texts ordered from newest to oldest. */
fun getAll(): List<UserCustomText>

/** Deletes a custom text entry by [textId]. Returns true if deleted. */
fun delete(textId: Long): Boolean
}

/** SQLite-backed implementation of [UserCustomTextRepository]. */
class SqliteUserCustomTextRepository(
private val connection: Connection,
) : UserCustomTextRepository {

override fun insert(text: UserCustomText): Long {
val sql =
"""
INSERT INTO user_custom_texts (
title, original_spanish, simplified_spanish, readability_level, created_timestamp
) VALUES (?, ?, ?, ?, ?)
"""
.trimIndent()

connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS).use { stmt ->
stmt.setString(PARAM_TITLE, text.title)
stmt.setString(PARAM_ORIGINAL, text.originalSpanish)
if (text.simplifiedSpanish != null) {
stmt.setString(PARAM_SIMPLIFIED, text.simplifiedSpanish)
} else {
stmt.setNull(PARAM_SIMPLIFIED, Types.VARCHAR)
}
stmt.setInt(PARAM_READABILITY, text.readabilityLevel)
stmt.setLong(PARAM_TIMESTAMP, text.createdTimestamp)
stmt.executeUpdate()

stmt.generatedKeys.use { rs ->
if (rs.next()) {
return rs.getLong(1)
}
}
}
return 0L
}

override fun update(text: UserCustomText): Boolean {
if (text.textId <= 0L) return false

val sql =
"""
UPDATE user_custom_texts
SET title = ?,
original_spanish = ?,
simplified_spanish = ?,
readability_level = ?,
created_timestamp = ?
WHERE text_id = ?
"""
.trimIndent()

connection.prepareStatement(sql).use { stmt ->
stmt.setString(PARAM_TITLE, text.title)
stmt.setString(PARAM_ORIGINAL, text.originalSpanish)
if (text.simplifiedSpanish != null) {
stmt.setString(PARAM_SIMPLIFIED, text.simplifiedSpanish)
} else {
stmt.setNull(PARAM_SIMPLIFIED, Types.VARCHAR)
}
stmt.setInt(PARAM_READABILITY, text.readabilityLevel)
stmt.setLong(PARAM_TIMESTAMP, text.createdTimestamp)
stmt.setLong(PARAM_UPDATE_ID, text.textId)
return stmt.executeUpdate() > 0
}
}

override fun getById(textId: Long): UserCustomText? {
if (textId <= 0L) return null

val sql =
"""
SELECT text_id, title, original_spanish, simplified_spanish, readability_level, created_timestamp
FROM user_custom_texts
WHERE text_id = ?
LIMIT 1
"""
.trimIndent()

connection.prepareStatement(sql).use { stmt ->
stmt.setLong(1, textId)
stmt.executeQuery().use { rs ->
return if (rs.next()) mapRow(rs) else null
}
}
}

override fun getAll(): List<UserCustomText> {
val sql =
"""
SELECT text_id, title, original_spanish, simplified_spanish, readability_level, created_timestamp
FROM user_custom_texts
ORDER BY created_timestamp DESC
"""
.trimIndent()

val results = mutableListOf<UserCustomText>()
connection.createStatement().use { stmt ->
stmt.executeQuery(sql).use { rs ->
while (rs.next()) {
results.add(mapRow(rs))
}
}
}
return results
}

override fun delete(textId: Long): Boolean {
if (textId <= 0L) return false

val sql = "DELETE FROM user_custom_texts WHERE text_id = ?"
connection.prepareStatement(sql).use { stmt ->
stmt.setLong(1, textId)
return stmt.executeUpdate() > 0
}
}

private fun mapRow(rs: ResultSet): UserCustomText {
return UserCustomText(
textId = rs.getLong("text_id"),
title = rs.getString("title"),
originalSpanish = rs.getString("original_spanish"),
simplifiedSpanish = rs.getString("simplified_spanish"),
readabilityLevel = rs.getInt("readability_level"),
createdTimestamp = rs.getLong("created_timestamp"),
)
}

private companion object {
const val PARAM_TITLE = 1
const val PARAM_ORIGINAL = 2
const val PARAM_SIMPLIFIED = 3
const val PARAM_READABILITY = 4
const val PARAM_TIMESTAMP = 5
const val PARAM_UPDATE_ID = 6
}
}
Loading
Loading