Skip to content

Commit 18621b0

Browse files
committed
add comprehensive sqlitenow-gradle-plugin regression coverage
1 parent 2bce764 commit 18621b0

32 files changed

Lines changed: 3949 additions & 171 deletions

sqlitenow-gradle-plugin/src/main/kotlin/dev/goquick/sqlitenow/gradle/TempDatabaseConnector.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,12 @@ internal class TempDatabaseConnector(
6565

6666
// Create a connection to the database
6767
val conn: Connection = DriverManager.getConnection(jdbcUrl)
68-
conn.autoCommit = false
6968

70-
// Enable foreign keys
69+
// Enable foreign keys before disabling auto-commit so SQLite does not ignore the pragma.
7170
conn.createStatement().use { stmt ->
7271
stmt.execute("PRAGMA foreign_keys = ON;")
7372
}
73+
conn.autoCommit = false
7474

7575
return conn
7676
}

sqlitenow-gradle-plugin/src/main/kotlin/dev/goquick/sqlitenow/gradle/processing/DynamicFieldMapper.kt

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -108,16 +108,24 @@ class DynamicFieldMapper {
108108
selectStatement: SelectStatement,
109109
sourceTableAlias: String
110110
): String? {
111-
// Find JOIN condition where the right side matches our source table alias
111+
// Find JOIN condition where either side matches our source table alias.
112+
// When the collection source appears on the left side of the predicate
113+
// (e.g. "pkg.person_id = p.id"), the grouping column lives on the right side.
112114
selectStatement.joinConditions.forEach { joinCondition ->
113-
if (joinCondition.rightTable == sourceTableAlias) {
114-
// Find the field in SELECT that matches the left side of the JOIN
115-
// e.g., for JOIN condition "p.id = a.person_id", find field with table "p" and column "id"
116-
val leftTable = joinCondition.leftTable.lowercase()
117-
val leftColumn = joinCondition.leftColumn.lowercase()
115+
val (groupTable, groupColumn) = when {
116+
joinCondition.rightTable.equals(sourceTableAlias, ignoreCase = true) ->
117+
joinCondition.leftTable to joinCondition.leftColumn
118+
119+
joinCondition.leftTable.equals(sourceTableAlias, ignoreCase = true) ->
120+
joinCondition.rightTable to joinCondition.rightColumn
121+
122+
else -> null to null
123+
}
124+
125+
if (groupTable != null && groupColumn != null) {
118126
val matchingField = selectStatement.fields.find { field ->
119-
field.tableName.lowercase() == leftTable &&
120-
field.originalColumnName.lowercase() == leftColumn
127+
field.tableName.equals(groupTable, ignoreCase = true) &&
128+
field.originalColumnName.equals(groupColumn, ignoreCase = true)
121129
}
122130

123131
// Return the field name (which might be aliased, e.g., "person_id" from "p.id AS person_id")

sqlitenow-gradle-plugin/src/main/kotlin/dev/goquick/sqlitenow/gradle/processing/FieldAnnotationMerger.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ object FieldAnnotationMerger {
5555
if (fieldAnnotations.sourceTable != null) {
5656
targetAnnotations[AnnotationConstants.SOURCE_TABLE] = fieldAnnotations.sourceTable
5757
}
58+
if (fieldAnnotations.collectionKey != null) {
59+
targetAnnotations[AnnotationConstants.COLLECTION_KEY] = fieldAnnotations.collectionKey
60+
}
5861
if (fieldAnnotations.sqlTypeHint != null) {
5962
targetAnnotations[AnnotationConstants.SQL_TYPE_HINT] = fieldAnnotations.sqlTypeHint
6063
}

sqlitenow-gradle-plugin/src/main/kotlin/dev/goquick/sqlitenow/gradle/processing/StatementProcessingHelper.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ class StatementProcessingHelper(
130130
?.associate { namespaceDir ->
131131
val namespace = namespaceDir.name
132132
val sqlFiles = namespaceDir.listFiles()
133-
?.filter { it.isFile && it.extension == "sql" }
133+
?.filter { it.isFile && it.extension.equals("sql", ignoreCase = true) }
134134
// Likewise, stabilise file ordering inside each namespace
135135
?.sortedBy { it.name }
136136
?: emptyList()

sqlitenow-gradle-plugin/src/main/kotlin/dev/goquick/sqlitenow/gradle/sqlinspect/NamedParametersProcessor.kt

Lines changed: 83 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ class NamedParametersProcessor(
5252
val seen = mutableListOf<String>()
5353
val buffer = StringBuilder()
5454
val castTypes = mutableMapOf<String, String>()
55+
val inParameters = mutableSetOf<String>()
5556

5657
// Extract common parameter processing logic
5758
fun processParameter(param: JdbcNamedParameter) {
@@ -65,12 +66,30 @@ class NamedParametersProcessor(
6566

6667
if (value is InExpression) {
6768
// We are using json_each() to expand the array into individual values
69+
inParameters += param.name
6870
buffer.append("(SELECT value FROM json_each(?))")
6971
} else {
7072
buffer.append('?')
7173
}
7274
}
7375

76+
fun collectMetadataOnly(expression: net.sf.jsqlparser.expression.Expression?) {
77+
expression ?: return
78+
expression.accept(object : ExpressionDeParser() {
79+
override fun visit(param: JdbcNamedParameter) {
80+
seen += param.name
81+
val parent = param.astNode?.jjtGetParent() as? SimpleNode
82+
val value = parent?.jjtGetValue()
83+
if (value is CastExpression && value.leftExpression == param) {
84+
castTypes[param.name] = value.colDataType.dataType
85+
}
86+
if (value is InExpression) {
87+
inParameters += param.name
88+
}
89+
}
90+
})
91+
}
92+
7493
val exprDp = object : ExpressionDeParser() {
7594
override fun visit(param: JdbcNamedParameter) = processParameter(param)
7695
}
@@ -81,65 +100,86 @@ class NamedParametersProcessor(
81100
// Use custom StatementDeParser that handles INSERT ON CONFLICT properly
82101
val stmtDp = object : StatementDeParser(exprDp, selectDp, buffer) {
83102
override fun visit(insert: Insert) {
84-
// Use the default INSERT handling but with custom expression visitor
85103
val customExprDp = object : ExpressionDeParser(exprDp.selectVisitor, buffer) {
86104
override fun visit(param: JdbcNamedParameter) = processParameter(param)
87105
}
88-
89106
val customSelectDp = SelectDeParser(customExprDp, buffer)
90107
customExprDp.selectVisitor = customSelectDp
108+
InsertDeParser(customExprDp, customSelectDp, buffer).deParse(insert)
109+
}
110+
}
91111

92-
// Create a custom INSERT deparser that handles conflict actions
93-
val insertDp = object : InsertDeParser(customExprDp, customSelectDp, buffer) {
94-
override fun deParse(insert: Insert) {
95-
// Manually handle the entire INSERT statement to avoid duplication
96-
buffer.append("INSERT INTO ")
97-
buffer.append(insert.table.name)
98-
99-
if (insert.columns != null) {
100-
buffer.append(" (")
101-
buffer.append(insert.columns.joinToString(", ") { it.columnName })
102-
buffer.append(")")
103-
}
112+
stmt.accept(stmtDp)
104113

105-
if (insert.select != null) {
106-
buffer.append(" VALUES (")
107-
insert.select.values.expressions.forEachIndexed { index, expr ->
108-
if (index > 0) buffer.append(", ")
109-
expr.accept(customExprDp)
110-
}
111-
buffer.append(")")
112-
}
114+
if (stmt is Insert) {
115+
stmt.conflictTarget?.indexExpression?.let(::collectMetadataOnly)
116+
stmt.conflictTarget?.whereExpression?.let(::collectMetadataOnly)
117+
stmt.conflictAction?.updateSets.orEmpty().forEach { updateSet ->
118+
updateSet.values.orEmpty().forEach { expr ->
119+
collectMetadataOnly(expr)
120+
}
121+
}
122+
stmt.conflictAction?.whereExpression?.let(::collectMetadataOnly)
123+
}
113124

114-
// Handle ON CONFLICT clause manually
115-
insert.conflictAction?.let { conflictAction ->
116-
buffer.append(" ON CONFLICT")
117-
buffer.append(" DO UPDATE SET ")
118-
conflictAction.updateSets.forEachIndexed { index, updateSet ->
119-
if (index > 0) buffer.append(", ")
120-
buffer.append(updateSet.columns[0].columnName)
121-
buffer.append(" = ")
122-
updateSet.values[0].accept(customExprDp)
123-
}
124-
}
125+
return Triple(rewriteRemainingNamedParameters(buffer.toString(), inParameters), seen, castTypes)
126+
}
127+
128+
private fun rewriteRemainingNamedParameters(sql: String, inParameters: Set<String>): String {
129+
val out = StringBuilder()
130+
var index = 0
131+
var inSingleQuote = false
132+
var inDoubleQuote = false
133+
134+
while (index < sql.length) {
135+
val current = sql[index]
136+
137+
when (current) {
138+
'\'' -> {
139+
out.append(current)
140+
if (!inDoubleQuote) {
141+
inSingleQuote = !inSingleQuote
142+
}
143+
index++
144+
}
125145

126-
// Handle RETURNING clause manually
127-
insert.returningClause?.let { returningClause ->
128-
buffer.append(" RETURNING ")
129-
returningClause.forEachIndexed { index, selectItem ->
130-
if (index > 0) buffer.append(", ")
131-
buffer.append(selectItem.toString())
146+
'"' -> {
147+
out.append(current)
148+
if (!inSingleQuote) {
149+
inDoubleQuote = !inDoubleQuote
150+
}
151+
index++
152+
}
153+
154+
':' -> {
155+
if (!inSingleQuote && !inDoubleQuote) {
156+
val start = index + 1
157+
var end = start
158+
while (end < sql.length && (sql[end].isLetterOrDigit() || sql[end] == '_')) {
159+
end++
160+
}
161+
if (end > start) {
162+
val paramName = sql.substring(start, end)
163+
if (paramName in inParameters) {
164+
out.append("(SELECT value FROM json_each(?))")
165+
} else {
166+
out.append('?')
132167
}
168+
index = end
169+
continue
133170
}
134171
}
172+
out.append(current)
173+
index++
135174
}
136175

137-
insertDp.deParse(insert)
176+
else -> {
177+
out.append(current)
178+
index++
179+
}
138180
}
139181
}
140182

141-
stmt.accept(stmtDp)
142-
143-
return Triple(buffer.toString(), seen, castTypes)
183+
return out.toString()
144184
}
145185
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package dev.goquick.sqlitenow.gradle
2+
3+
import dev.goquick.sqlitenow.gradle.context.AdapterConfig
4+
import dev.goquick.sqlitenow.gradle.context.ColumnLookup
5+
import dev.goquick.sqlitenow.gradle.model.AnnotatedCreateViewStatement
6+
import dev.goquick.sqlitenow.gradle.processing.AnnotationConstants
7+
import dev.goquick.sqlitenow.gradle.processing.FieldAnnotationOverrides
8+
import dev.goquick.sqlitenow.gradle.sqlinspect.CreateViewStatement
9+
import kotlin.test.assertTrue
10+
import org.junit.jupiter.api.DisplayName
11+
import org.junit.jupiter.api.Test
12+
13+
class AdapterConfigTest {
14+
15+
@Test
16+
@DisplayName("hasAdapterAnnotation resolves annotations from field, table, and view sources")
17+
fun hasAdapterAnnotationResolvesAcrossFieldTableAndView() {
18+
val personTable = annotatedCreateTable(
19+
tableName = "person",
20+
columns = listOf(
21+
annotatedTableColumn("id", "INTEGER"),
22+
annotatedTableColumn("birth_date", "TEXT", notNull = false, adapter = true),
23+
),
24+
)
25+
val view = AnnotatedCreateViewStatement(
26+
name = "PersonView",
27+
src = CreateViewStatement(
28+
sql = "CREATE VIEW person_view AS SELECT p.birth_date AS birth_date FROM person p",
29+
viewName = "person_view",
30+
selectStatement = selectStatement(
31+
fields = listOf(fieldSource("birth_date", "p", originalColumnName = "birth_date")),
32+
fromTable = "person_view",
33+
tableAliases = mapOf("p" to "person"),
34+
),
35+
),
36+
annotations = statementAnnotations(),
37+
fields = listOf(
38+
AnnotatedCreateViewStatement.Field(
39+
src = fieldSource("birth_date", "person", originalColumnName = "birth_date"),
40+
annotations = FieldAnnotationOverrides(
41+
propertyName = null,
42+
propertyType = null,
43+
notNull = null,
44+
adapter = null,
45+
),
46+
),
47+
),
48+
dynamicFields = emptyList(),
49+
)
50+
val adapterConfig = AdapterConfig(
51+
columnLookup = ColumnLookup(listOf(personTable), listOf(view)),
52+
createTableStatements = listOf(personTable),
53+
createViewStatements = listOf(view),
54+
packageName = "fixture.db",
55+
)
56+
57+
val directField = regularField(
58+
fieldName = "birth_date",
59+
tableName = "person",
60+
adapter = true,
61+
)
62+
val tableField = regularField(
63+
fieldName = "birth_date",
64+
tableName = "person",
65+
)
66+
val viewField = regularField(
67+
fieldName = "birth_date",
68+
tableName = "person_view",
69+
)
70+
71+
assertTrue(adapterConfig.hasAdapterAnnotation(directField))
72+
assertTrue(adapterConfig.hasAdapterAnnotation(tableField))
73+
assertTrue(adapterConfig.hasAdapterAnnotation(viewField))
74+
}
75+
76+
@Test
77+
@DisplayName("collectAllParamConfigs exposes field, table, and mapTo adapter configs")
78+
fun collectAllParamConfigsIncludesFieldTableAndMapToAdapters() {
79+
val personTable = annotatedCreateTable(
80+
tableName = "person",
81+
columns = listOf(
82+
annotatedTableColumn("id", "INTEGER"),
83+
annotatedTableColumn("birth_date", "TEXT", notNull = false, adapter = true),
84+
),
85+
)
86+
val statement = annotatedSelectStatement(
87+
name = "SelectSummary",
88+
src = selectStatementWithParameters(
89+
fields = listOf(fieldSource("birth_date", "p", originalColumnName = "birth_date")),
90+
namedParameters = listOf("birthDateStart"),
91+
namedParametersToColumns = linkedMapOf(
92+
"birthDateStart" to dev.goquick.sqlitenow.gradle.sqlinspect.AssociatedColumn.Default("birth_date"),
93+
),
94+
fromTable = "person",
95+
tableAliases = mapOf("p" to "person"),
96+
),
97+
fields = listOf(
98+
regularField("birth_date", "p", originalColumnName = "birth_date"),
99+
),
100+
queryResult = "PersonSummaryRow",
101+
mapTo = "fixture.model.PersonSummary",
102+
)
103+
val adapterConfig = AdapterConfig(
104+
columnLookup = ColumnLookup(listOf(personTable), emptyList()),
105+
createTableStatements = listOf(personTable),
106+
packageName = "fixture.db",
107+
)
108+
109+
val configs = adapterConfig.collectAllParamConfigs(statement, namespace = "person")
110+
111+
assertTrue(configs.any { it.kind == AdapterConfig.AdapterKind.INPUT && it.adapterFunctionName == "birthDateToSqlValue" })
112+
assertTrue(configs.any { it.kind == AdapterConfig.AdapterKind.RESULT_FIELD && it.adapterFunctionName == "sqlValueToBirthDate" })
113+
assertTrue(configs.any {
114+
it.kind == AdapterConfig.AdapterKind.MAP_RESULT &&
115+
it.adapterFunctionName == "personSummaryRowMapper" &&
116+
it.providerNamespace == "person"
117+
})
118+
assertTrue(configs.none { it.adapterFunctionName == AnnotationConstants.ADAPTER_CUSTOM })
119+
}
120+
}

0 commit comments

Comments
 (0)