Skip to content

Commit f7ea233

Browse files
committed
feat: integrate JSQLParser for SQL validation (phodal#508)
- Add JSQLParser 4.9 dependency for SQL parsing and validation - Implement SqlValidator with comprehensive safety checks - Support syntax validation, operation type detection - Add dangerous operation detection (UPDATE/DELETE without WHERE) - Implement complexity estimation (SIMPLE/MEDIUM/COMPLEX) - Add extension functions for easy validation - Add executeValidatedQuery() for safe query execution - Include 16 comprehensive unit tests (all passing) The SqlValidator integrates seamlessly with DatabaseConnection to provide SQL validation before execution, preventing SQL injection and ensuring query safety. Related to phodal#508
1 parent ad4777e commit f7ea233

3 files changed

Lines changed: 434 additions & 0 deletions

File tree

mpp-core/build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,9 @@ kotlin {
209209

210210
// Connection pooling
211211
implementation("com.zaxxer:HikariCP:6.0.0")
212+
213+
// JSQLParser for SQL validation and parsing
214+
implementation("com.github.jsqlparser:jsqlparser:4.9")
212215
}
213216
}
214217

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
package cc.unitmesh.agent.database
2+
3+
import net.sf.jsqlparser.parser.CCJSqlParserUtil
4+
import net.sf.jsqlparser.statement.Statement
5+
import net.sf.jsqlparser.statement.select.Select
6+
import net.sf.jsqlparser.statement.insert.Insert
7+
import net.sf.jsqlparser.statement.update.Update
8+
import net.sf.jsqlparser.statement.delete.Delete
9+
10+
/**
11+
* SQL validation and parsing utilities using JSQLParser
12+
*
13+
* This class provides SQL syntax validation, safety checks, and metadata extraction
14+
* to ensure generated SQL queries are safe and valid before execution.
15+
*/
16+
class SqlValidator {
17+
18+
/**
19+
* Validation result containing parsed statement and any warnings
20+
*/
21+
data class ValidationResult(
22+
val isValid: Boolean,
23+
val statement: Statement? = null,
24+
val errors: List<String> = emptyList(),
25+
val warnings: List<String> = emptyList(),
26+
val metadata: SqlMetadata? = null
27+
) {
28+
val isSafe: Boolean
29+
get() = isValid && errors.isEmpty() && (metadata?.isDangerous != true)
30+
}
31+
32+
/**
33+
* Metadata extracted from SQL statement
34+
*/
35+
data class SqlMetadata(
36+
val type: SqlType,
37+
val tables: List<String>,
38+
val columns: List<String>,
39+
val isDangerous: Boolean,
40+
val hasWildcard: Boolean,
41+
val estimatedComplexity: ComplexityLevel
42+
)
43+
44+
enum class SqlType {
45+
SELECT, INSERT, UPDATE, DELETE,
46+
DDL, DCL, TCL, UNKNOWN
47+
}
48+
49+
enum class ComplexityLevel {
50+
SIMPLE, // Single table, few columns
51+
MEDIUM, // Joins, subqueries
52+
COMPLEX // Multiple joins, nested subqueries, complex conditions
53+
}
54+
55+
/**
56+
* Validate SQL syntax and safety
57+
*/
58+
fun validate(sql: String, allowedOperations: Set<SqlType> = setOf(SqlType.SELECT)): ValidationResult {
59+
val errors = mutableListOf<String>()
60+
val warnings = mutableListOf<String>()
61+
62+
try {
63+
// Parse SQL
64+
val statement = CCJSqlParserUtil.parse(sql)
65+
66+
// Extract metadata
67+
val metadata = extractMetadata(statement)
68+
69+
// Check allowed operations
70+
if (metadata.type !in allowedOperations) {
71+
errors.add("Operation ${metadata.type} is not allowed. Allowed: $allowedOperations")
72+
}
73+
74+
// Safety checks
75+
if (metadata.isDangerous) {
76+
warnings.add("Query contains potentially dangerous operations")
77+
}
78+
79+
// Check for missing WHERE clause in UPDATE/DELETE
80+
if (metadata.type in setOf(SqlType.UPDATE, SqlType.DELETE)) {
81+
if (statement is Update && statement.where == null) {
82+
errors.add("UPDATE without WHERE clause is dangerous")
83+
} else if (statement is Delete && statement.where == null) {
84+
errors.add("DELETE without WHERE clause is dangerous")
85+
}
86+
}
87+
88+
return ValidationResult(
89+
isValid = true,
90+
statement = statement,
91+
errors = errors,
92+
warnings = warnings,
93+
metadata = metadata
94+
)
95+
96+
} catch (e: Exception) {
97+
errors.add("SQL parsing error: ${e.message}")
98+
return ValidationResult(
99+
isValid = false,
100+
errors = errors
101+
)
102+
}
103+
}
104+
105+
/**
106+
* Extract metadata from parsed statement
107+
*/
108+
private fun extractMetadata(statement: Statement): SqlMetadata {
109+
val tables = mutableListOf<String>()
110+
val columns = mutableListOf<String>()
111+
var hasWildcard = false
112+
var isDangerous = false
113+
114+
val type = when (statement) {
115+
is Select -> {
116+
// Extract table names from SELECT
117+
val selectBody = statement.selectBody
118+
tables.addAll(extractTablesFromSelect(selectBody))
119+
120+
// Check for SELECT *
121+
hasWildcard = statement.toString().contains("SELECT *", ignoreCase = true)
122+
123+
SqlType.SELECT
124+
}
125+
is Insert -> {
126+
tables.add(statement.table.name)
127+
SqlType.INSERT
128+
}
129+
is Update -> {
130+
tables.add(statement.table.name)
131+
isDangerous = statement.where == null
132+
SqlType.UPDATE
133+
}
134+
is Delete -> {
135+
tables.add(statement.table.name)
136+
isDangerous = statement.where == null
137+
SqlType.DELETE
138+
}
139+
else -> SqlType.UNKNOWN
140+
}
141+
142+
// Estimate complexity
143+
val complexity = when {
144+
tables.size > 3 || statement.toString().contains("SUBQUERY", ignoreCase = true) -> ComplexityLevel.COMPLEX
145+
tables.size > 1 || statement.toString().contains("JOIN", ignoreCase = true) -> ComplexityLevel.MEDIUM
146+
else -> ComplexityLevel.SIMPLE
147+
}
148+
149+
return SqlMetadata(
150+
type = type,
151+
tables = tables,
152+
columns = columns,
153+
isDangerous = isDangerous,
154+
hasWildcard = hasWildcard,
155+
estimatedComplexity = complexity
156+
)
157+
}
158+
159+
/**
160+
* Extract table names from SELECT body
161+
*/
162+
private fun extractTablesFromSelect(selectBody: Any): List<String> {
163+
val tables = mutableListOf<String>()
164+
165+
try {
166+
// Use toString() and simple parsing for now
167+
// More sophisticated extraction can be added later
168+
val sql = selectBody.toString()
169+
val fromIndex = sql.indexOf("FROM", ignoreCase = true)
170+
if (fromIndex >= 0) {
171+
val afterFrom = sql.substring(fromIndex + 4).trim()
172+
val tableName = afterFrom.split(" ", ",", "\n", "JOIN")[0].trim()
173+
if (tableName.isNotEmpty()) {
174+
tables.add(tableName)
175+
}
176+
}
177+
} catch (e: Exception) {
178+
// Ignore extraction errors
179+
}
180+
181+
return tables
182+
}
183+
184+
/**
185+
* Quick syntax check without full validation
186+
*/
187+
fun checkSyntax(sql: String): Boolean {
188+
return try {
189+
CCJSqlParserUtil.parse(sql)
190+
true
191+
} catch (e: Exception) {
192+
false
193+
}
194+
}
195+
196+
/**
197+
* Check if SQL is a safe read-only query
198+
*/
199+
fun isSafeReadOnly(sql: String): Boolean {
200+
val result = validate(sql, setOf(SqlType.SELECT))
201+
return result.isSafe && result.metadata?.type == SqlType.SELECT
202+
}
203+
204+
companion object {
205+
/**
206+
* Default validator instance
207+
*/
208+
val default = SqlValidator()
209+
210+
/**
211+
* Validate SQL with default validator
212+
*/
213+
fun validateSql(sql: String): ValidationResult {
214+
return default.validate(sql)
215+
}
216+
217+
/**
218+
* Quick syntax check
219+
*/
220+
fun isValidSql(sql: String): Boolean {
221+
return default.checkSyntax(sql)
222+
}
223+
}
224+
}
225+
226+
/**
227+
* Extension function for DatabaseConnection to validate SQL before execution
228+
*/
229+
suspend fun DatabaseConnection.executeValidatedQuery(sql: String): QueryResult {
230+
val validation = SqlValidator.default.validate(sql, setOf(SqlValidator.SqlType.SELECT))
231+
232+
if (!validation.isValid) {
233+
throw DatabaseException("SQL validation failed: ${validation.errors.joinToString(", ")}")
234+
}
235+
236+
if (!validation.isSafe) {
237+
throw DatabaseException("SQL query is not safe: ${validation.warnings.joinToString(", ")}")
238+
}
239+
240+
return executeQuery(sql)
241+
}
242+
243+
/**
244+
* Extension function to validate SQL syntax only
245+
*/
246+
fun String.isValidSql(): Boolean {
247+
return SqlValidator.isValidSql(this)
248+
}
249+
250+
/**
251+
* Extension function to get SQL validation result
252+
*/
253+
fun String.validateSql(): SqlValidator.ValidationResult {
254+
return SqlValidator.validateSql(this)
255+
}

0 commit comments

Comments
 (0)