Skip to content

Commit a5d05be

Browse files
committed
Add Calcite-based schema validation: update dependencies, implement QueryValidator, and integrate validation in RepositoryProcessor.
1 parent c9b5f86 commit a5d05be

File tree

4 files changed

+187
-3
lines changed

4 files changed

+187
-3
lines changed

gradle/libs.versions.toml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ r2dbc-postgresql = "1.0.7.RELEASE"
2929
# https://central.sonatype.com/artifact/com.github.jsqlparser/jsqlparser
3030
# https://github.com/JSQLParser/JSqlParser
3131
jsqlparser = "5.3"
32+
# https://mvnrepository.com/artifact/org.apache.calcite/calcite-core/1.40.0
33+
# https://github.com/apache/calcite
34+
calcite = "1.40.0"
3235

3336
[libraries]
3437
gradle-kotlin-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
@@ -45,11 +48,13 @@ r2dbc-pool = { module = "io.r2dbc:r2dbc-pool", version.ref = "r2dbc-pool" }
4548
r2dbc-mysql = { module = "io.asyncer:r2dbc-mysql", version.ref = "r2dbc-mysql" }
4649
r2dbc-postgresql = { module = "org.postgresql:r2dbc-postgresql", version.ref = "r2dbc-postgresql" }
4750
jsqlparser = { module = "com.github.jsqlparser:jsqlparser", version.ref = "jsqlparser" }
51+
calcite-core = { module = "org.apache.calcite:calcite-core", version.ref = "calcite" }
52+
calcite-server = { module = "org.apache.calcite:calcite-server", version.ref = "calcite" }
4853

4954
[plugins]
5055
kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
5156
pubhish = { id = "com.vanniktech.maven.publish", version.ref = "publish" }
52-
ksp = { id ="com.google.devtools.ksp", version.ref = "ksp" }
57+
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
5358
dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" }
5459
kotlin-spring = { id = "org.jetbrains.kotlin.plugin.spring", version.ref = "kotlin" }
5560
spring-boot = { id = "org.springframework.boot", version.ref = "spring-boot" }

sqlx4k-codegen/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ kotlin {
1414
dependencies {
1515
implementation(libs.ksp)
1616
implementation(libs.jsqlparser)
17+
implementation(libs.calcite.core)
18+
implementation(libs.calcite.server)
1719
}
1820
}
1921
}
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
package io.github.smyrgeorge.sqlx4k.processor
2+
3+
import net.sf.jsqlparser.parser.CCJSqlParserUtil
4+
import net.sf.jsqlparser.statement.alter.Alter
5+
import net.sf.jsqlparser.statement.create.table.CreateTable
6+
import org.apache.calcite.adapter.java.JavaTypeFactory
7+
import org.apache.calcite.config.CalciteConnectionConfigImpl
8+
import org.apache.calcite.config.CalciteConnectionProperty
9+
import org.apache.calcite.jdbc.CalciteSchema
10+
import org.apache.calcite.jdbc.JavaTypeFactoryImpl
11+
import org.apache.calcite.prepare.CalciteCatalogReader
12+
import org.apache.calcite.rel.type.RelDataType
13+
import org.apache.calcite.rel.type.RelDataTypeFactory
14+
import org.apache.calcite.schema.impl.AbstractTable
15+
import org.apache.calcite.sql.`fun`.SqlStdOperatorTable
16+
import org.apache.calcite.sql.parser.SqlParser
17+
import org.apache.calcite.sql.validate.SqlConformanceEnum
18+
import org.apache.calcite.sql.validate.SqlValidator
19+
import org.apache.calcite.sql.validate.SqlValidatorCatalogReader
20+
import org.apache.calcite.sql.validate.SqlValidatorUtil
21+
import org.apache.calcite.sql.validate.SqlValidatorWithHints
22+
import java.io.File
23+
import java.math.BigDecimal
24+
import java.util.*
25+
26+
object QueryValidator {
27+
private lateinit var schema: Map<String, TableDef>
28+
private lateinit var tables: List<TableDef>
29+
private lateinit var validator: SqlValidator
30+
31+
fun load(path: String) {
32+
val dir = File(path)
33+
if (!dir.exists()) error("Schema directory does not exist: $path")
34+
if (!dir.isDirectory) error("Schema directory is not a directory: $path")
35+
36+
val files = dir.listFiles()
37+
?.filter { it.isFile && it.extension == "sql" }
38+
?: error("Cound not list schema files in directory: $path")
39+
40+
val schema = mutableMapOf<String, TableDef>()
41+
42+
files.map { file ->
43+
val sql = file.readText()
44+
CCJSqlParserUtil.parseStatements(sql).forEach { stmt ->
45+
46+
when (stmt) {
47+
is CreateTable -> {
48+
val cols = stmt.columnDefinitions.map { ColumnDef(it.columnName, it.colDataType.dataType) }
49+
schema[stmt.table.name.lowercase()] = TableDef(stmt.table.name, cols.toMutableList())
50+
}
51+
52+
is Alter -> {
53+
val tableName = stmt.table.name.lowercase()
54+
val table = schema[tableName] ?: error("ALTER TABLE on unknown table $tableName")
55+
56+
stmt.alterExpressions.forEach { expr ->
57+
when (expr.operation.name) {
58+
// ADD COLUMN
59+
"ADD" -> {
60+
expr.colDataTypeList?.forEach { colType ->
61+
val colName = expr.columnName
62+
table.columns.add(ColumnDef(colName, colType.colDataType.dataType))
63+
}
64+
}
65+
66+
// DROP COLUMN
67+
"DROP" -> {
68+
val colName = expr.columnName
69+
table.columns.removeIf { it.name.equals(colName, ignoreCase = true) }
70+
}
71+
72+
else -> {
73+
println("⚠️ Unsupported ALTER operation: ${expr.operation}")
74+
}
75+
}
76+
}
77+
}
78+
79+
else -> {
80+
println("⚠️ Skipping unsupported statement: ${stmt.javaClass.simpleName}")
81+
}
82+
}
83+
}
84+
}
85+
86+
this.schema = schema
87+
this.tables = schema.values.toList()
88+
this.validator = createCalciteValidator()
89+
}
90+
91+
private fun createCalciteValidator(): SqlValidatorWithHints {
92+
val rootSchema = CalciteSchema.createRootSchema(true).apply {
93+
val root = this
94+
tables.forEach { t ->
95+
root.add(t.name, MigrationTable(t.columns))
96+
}
97+
}
98+
99+
val typeFactory: JavaTypeFactory = JavaTypeFactoryImpl()
100+
101+
val props = Properties().apply {
102+
this[CalciteConnectionProperty.CASE_SENSITIVE.camelName()] = "false"
103+
}
104+
105+
val config = CalciteConnectionConfigImpl(props)
106+
107+
val catalogReader: SqlValidatorCatalogReader = CalciteCatalogReader(
108+
/* rootSchema = */ rootSchema,
109+
/* defaultSchema = */ listOf(), // search path (empty = root)
110+
/* typeFactory = */ typeFactory,
111+
/* config = */ config
112+
)
113+
114+
val validator: SqlValidatorWithHints = SqlValidatorUtil.newValidator(
115+
/* opTab = */ SqlStdOperatorTable.instance(),
116+
/* catalogReader = */ catalogReader,
117+
/* typeFactory = */ typeFactory,
118+
/* config = */ SqlValidator.Config.DEFAULT
119+
.withConformance(SqlConformanceEnum.STRICT_2003)
120+
.withTypeCoercionEnabled(false) // disable implicit casts
121+
)
122+
return validator
123+
}
124+
125+
fun validateQuery(sql: String) {
126+
validator.validate(SqlParser.create(sql).parseStmt())
127+
}
128+
129+
data class ColumnDef(val name: String, val type: String)
130+
data class TableDef(val name: String, val columns: MutableList<ColumnDef>)
131+
class MigrationTable(private val columns: List<ColumnDef>) : AbstractTable() {
132+
override fun getRowType(typeFactory: RelDataTypeFactory): RelDataType {
133+
val builder = typeFactory.builder()
134+
for (col in columns) {
135+
val type = when (col.type.uppercase()) {
136+
"INT", "INTEGER" -> typeFactory.createJavaType(Int::class.java)
137+
"VARCHAR", "TEXT" -> typeFactory.createJavaType(String::class.java)
138+
"DECIMAL" -> typeFactory.createJavaType(BigDecimal::class.java)
139+
else -> typeFactory.createJavaType(String::class.java) // fallback
140+
}
141+
builder.add(col.name, type)
142+
}
143+
return builder.build()
144+
}
145+
}
146+
}

sqlx4k-codegen/src/jvmMain/kotlin/io/github/smyrgeorge/sqlx4k/processor/RepositoryProcessor.kt

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,20 @@
11
package io.github.smyrgeorge.sqlx4k.processor
22

3-
import com.google.devtools.ksp.processing.*
4-
import com.google.devtools.ksp.symbol.*
3+
import com.google.devtools.ksp.processing.CodeGenerator
4+
import com.google.devtools.ksp.processing.Dependencies
5+
import com.google.devtools.ksp.processing.KSPLogger
6+
import com.google.devtools.ksp.processing.Resolver
7+
import com.google.devtools.ksp.processing.SymbolProcessor
8+
import com.google.devtools.ksp.symbol.ClassKind
9+
import com.google.devtools.ksp.symbol.KSAnnotated
10+
import com.google.devtools.ksp.symbol.KSAnnotation
11+
import com.google.devtools.ksp.symbol.KSClassDeclaration
12+
import com.google.devtools.ksp.symbol.KSDeclaration
13+
import com.google.devtools.ksp.symbol.KSFunctionDeclaration
14+
import com.google.devtools.ksp.symbol.KSPropertyDeclaration
15+
import com.google.devtools.ksp.symbol.KSType
16+
import com.google.devtools.ksp.symbol.KSValueArgument
17+
import com.google.devtools.ksp.symbol.KSValueParameter
518
import com.google.devtools.ksp.validate
619
import net.sf.jsqlparser.parser.CCJSqlParserUtil
720
import java.io.OutputStream
@@ -35,6 +48,10 @@ class RepositoryProcessor(
3548
val globalCheckSqlSyntax = options[VALIDATE_SQL_SYNTAX_OPTION]?.toBoolean() ?: true
3649
logger.info("[RepositoryProcessor] Validate SQL syntax: $globalCheckSqlSyntax")
3750

51+
val schemaMigrationsPath = options[SCHEMA_MIGRATIONS_PATH_OPTION] ?: "./set-the-path-to-schema-migrations"
52+
53+
if (ENABLE_SCHEMA_VALIDATION) QueryValidator.load(schemaMigrationsPath)
54+
3855
val outputFilename = "GeneratedRepositories"
3956

4057
val file: OutputStream = codeGenerator.createNewFile(
@@ -328,6 +345,7 @@ class RepositoryProcessor(
328345
?: error("Unable to generate query method (could not extract sql query from the @Query): $fn")
329346

330347
if (validateSqlSyntax) validateSqlSyntax(fn.simpleName(), sql)
348+
if (ENABLE_SCHEMA_VALIDATION) QueryValidator.validateQuery(sql)
331349

332350
val params = fn.parameters
333351
val paramSig = params.joinToString { p ->
@@ -465,5 +483,18 @@ class RepositoryProcessor(
465483
* the SQL syntax validation logic.
466484
*/
467485
private const val VALIDATE_SQL_SYNTAX_OPTION: String = "validate-sql-syntax"
486+
487+
/**
488+
* Represents the option key used for specifying the path to schema migration files
489+
* during repository processing and code generation.
490+
*
491+
* This constant is used to configure and resolve the directory containing
492+
* schema migration scripts, which are typically required for database structure
493+
* migration or initialization. The value associated with this option is expected
494+
* to be provided as part of the processing environment or tool configuration.
495+
*/
496+
private const val SCHEMA_MIGRATIONS_PATH_OPTION: String = "schema-migrations-path"
497+
498+
private const val ENABLE_SCHEMA_VALIDATION = false
468499
}
469500
}

0 commit comments

Comments
 (0)