Skip to content

Commit c763de7

Browse files
committed
update documentation for SQL Query validation
1 parent 9f55eb5 commit c763de7

File tree

5 files changed

+123
-8
lines changed

5 files changed

+123
-8
lines changed

README.md

Lines changed: 122 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,9 @@ result match {
179179

180180
---
181181

182-
### **3. SQL to Elasticsearch Query Translation**
182+
### **3. SQL compatible **
183+
184+
### **3.1 SQL to Elasticsearch Query DSL**
183185

184186
SoftClient4ES includes a powerful SQL parser that translates standard SQL `SELECT` queries into native Elasticsearch queries.
185187

@@ -464,6 +466,125 @@ val results = client.search(SQLQuery(sqlQuery))
464466
}
465467
}
466468
```
469+
---
470+
471+
### **3.2. Compile-Time SQL Query Validation**
472+
473+
SoftClient4ES provides **compile-time validation** for SQL queries used with type-safe methods like `searchAs[T]` and `scrollAs[T]`. This ensures that your queries are compatible with your Scala case classes **before your code even runs**, preventing runtime deserialization errors.
474+
475+
#### **Why Compile-Time Validation?**
476+
477+
-**Catch Errors Early**: Detect missing fields, typos, and type mismatches at compile-time
478+
-**Type Safety**: Ensure SQL queries match your domain models
479+
-**Better Developer Experience**: Get helpful error messages with suggestions
480+
-**Prevent Runtime Failures**: No more Jackson deserialization exceptions in production
481+
482+
#### **Validated Operations**
483+
484+
| Validation | Description | Level |
485+
|------------------------|--------------------------------------------------------|------------|
486+
| **SELECT * Rejection** | Prohibits `SELECT *` to ensure compile-time validation | ❌ ERROR |
487+
| **Required Fields** | Verifies that all required fields are selected | ❌ ERROR |
488+
| **Unknown Fields** | Detects fields that don't exist in the case class | ⚠️ WARNING |
489+
| **Nested Objects** | Validates the structure of nested objects | ❌ ERROR |
490+
| **Nested Collections** | Validates the use of UNNEST for collections | ❌ ERROR |
491+
| **Type Compatibility** | Checks compatibility between SQL and Scala types | ❌ ERROR |
492+
493+
#### **Example 1: Missing Required Field with Nested Object**
494+
495+
```scala
496+
case class Address(
497+
street: String,
498+
city: String,
499+
country: String
500+
)
501+
502+
case class User(
503+
id: String,
504+
name: String,
505+
address: Address // ❌ Required nested object
506+
)
507+
508+
// ❌ COMPILE ERROR: Missing required field 'address'
509+
client.searchAs[User]("SELECT id, name FROM users")
510+
```
511+
512+
**Compile Error:**
513+
514+
```
515+
❌ SQL query does not select the required field: address
516+
517+
Example query:
518+
SELECT id, name, address FROM ...
519+
520+
To fix this, either:
521+
1. Add it to the SELECT clause
522+
2. Make it Option[T] in the case class
523+
3. Provide a default value in the case class definition
524+
```
525+
526+
**✅ Solution:**
527+
528+
```scala
529+
// Option 1: Select the entire nested object (recommended)
530+
client.searchAs[User]("SELECT id, name, address FROM users")
531+
532+
// Option 2: Make the field optional
533+
case class User(
534+
id: String,
535+
name: String,
536+
address: Option[Address] = None
537+
)
538+
client.searchAs[User]("SELECT id, name FROM users")
539+
```
540+
541+
#### **Example 2: Typo Detection with Smart Suggestions**
542+
543+
```scala
544+
case class Product(
545+
id: String,
546+
name: String,
547+
price: Double,
548+
stock: Int
549+
)
550+
551+
// ❌ COMPILE ERROR: Typo in 'name' -> 'nam'
552+
client.searchAs[Product]("SELECT id, nam, price, stock FROM products")
553+
```
554+
555+
**Compile Error:**
556+
```
557+
❌ SQL query does not select the required field: name
558+
You have selected unknown field "nam", did you mean "name"?
559+
560+
Example query:
561+
SELECT id, price, stock, name FROM ...
562+
563+
To fix this, either:
564+
1. Add it to the SELECT clause
565+
2. Make it Option[T] in the case class
566+
3. Provide a default value in the case class definition
567+
```
568+
569+
**✅ Solution:**
570+
```scala
571+
// Fix the typo
572+
client.searchAs[Product]("SELECT id, name, price, stock FROM products")
573+
```
574+
575+
#### **Dynamic Queries (Skip Validation)**
576+
577+
For dynamic SQL queries where validation isn't possible, use the `*Unchecked` variants:
578+
579+
```scala
580+
val dynamicQuery = buildQueryAtRuntime()
581+
582+
// ✅ Skip compile-time validation for dynamic queries
583+
client.searchAsUnchecked[Product](SQLQuery(dynamicQuery))
584+
client.scrollAsUnchecked[Product](dynamicQuery)
585+
```
586+
587+
📖 **[Full SQL Validation Documentation](documentation/sql/validation.md)**
467588

468589
📖 **[Full SQL Documentation](documentation/sql/README.md)**
469590

documentation/client/README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
Welcome to the Client Engine Documentation. Navigate through the sections below:
44

55
- [Client Common Principles](common_principles.md)
6-
- [SQL Query Validation](sql_validation.md)
76
- [Version Information](version.md)
87
- [Flush Index](flush.md)
98
- [Refresh Index](refresh.md)

documentation/sql/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
Welcome to the SQL Engine Documentation. Navigate through the sections below:
44

55
- [Query Structure](request_structure.md)
6+
- [Query Validation](validation.md)
67
- [Operators](operators.md)
78
- [Operator Precedence](operator_precedence.md)
89
- [Aggregate Functions](functions_aggregate.md)
File renamed without changes.

macros/src/main/scala/app/softnetwork/elastic/sql/macros/SQLQueryValidator.scala

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -424,9 +424,6 @@ trait SQLQueryValidator {
424424
| FROM ...
425425
| JOIN UNNEST(....$fullFieldName) AS $fieldName
426426
|
427-
|✅ Solution 3: Make the nested object optional (if it's not always needed)
428-
| case class ${tpe.typeSymbol.name}(..., $fieldName: Option[${fieldType.typeSymbol.name}] = None)
429-
|
430427
|📚 Note: This applies to ALL nested objects, not just collections.
431428
|""".stripMargin
432429
)
@@ -519,9 +516,6 @@ trait SQLQueryValidator {
519516
| FROM ...
520517
| JOIN UNNEST(....$fullFieldName) AS $fieldName
521518
|
522-
|✅ Solution 3: Make the collection optional (if it's not always needed)
523-
| case class ${parentType.typeSymbol.name}(..., $fieldName: Option[List[${elementType.typeSymbol.name}]] = None)
524-
|
525519
|📚 Documentation:
526520
| https://www.elastic.co/guide/en/elasticsearch/reference/current/nested.html
527521
|""".stripMargin

0 commit comments

Comments
 (0)