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+ }
0 commit comments