diff --git a/.jpb/jpb-settings.xml b/.jpb/jpb-settings.xml
new file mode 100644
index 0000000..fb1f21c
--- /dev/null
+++ b/.jpb/jpb-settings.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
index 3786e2d..d3f0d18 100644
--- a/README.md
+++ b/README.md
@@ -124,67 +124,48 @@ fun yourFunctionNameHere(@SearchSpec specs: Specification): ResponseE
}
```
-1. Using the equal operator `:`
-Request : `/cars?search=color:Red`
-![equal operator example](./docs/images/equal-example.gif)
-
-2. Using the not equal operator `!`
-Request : `/cars?search=color!Red`
-![not equal operator example](./docs/images/not-equal-example.gif)
-
-3. Using the greater than operator `>`
-Request : `/cars?search=creationyear>2017`
-> Note: You can use the `>:` operator as well.
-
-![greater than operator example](./docs/images/greater-than-example.gif)
-
-4. Using the less than operator `<`
-Request : `/cars?search=price<100000`
-> Note: You can use the `<:` operator as well.
-
-![less than operator example](./docs/images/less-than-example.gif)
-
-5. Using the starts with operator `*`
-*For the ends with operator, simply place `*` at the beginning of the word*.
-*For the contains operator, simply place `*` at the beginning and the end of the word*.
-Request : `/cars?search=brand:Aston*`
-![starts with operator example](./docs/images/starts-with-example.gif)
-
-6. Using the `OR` operator
-Request : `/cars?search=color:Red OR color:Blue`
-![or operator example](./docs/images/or-example.gif)
-
-7. Using the `AND` operator
-Request : `/cars?search=brand:Aston* AND price<300000`
-![and operator example](./docs/images/and-example.gif)
-
-8. Checking if value is or not in a list
-Request : `/cars?search=color IN ['Red', 'Blue']`
-Request : `/cars?search=color NOT IN ['Red', 'Blue']`
-*Note: Spaces inside the brackets are not necessary*
-*Note: You will need to encode the value (e.g. encodeURI) as brackets are not valid url parts*
-
-9. Using the `IS EMPTY` and `IS NOT EMPTY` operators for collection fields
-Request : `/users?search=cars IS EMPTY`
-
-10. Using parenthesis
+## Operators
+
+| Operator | Description | Example |
+|----------------|---------------------------------|----------------------------------------------------|
+| `:` | Equal | `color:Red` |
+| `!` | Not equal | `color!Red` |
+| `>` | Greater than | `creationyear>2017` |
+| `>:` | Greater than eq | `creationyear>2017` |
+| `<` | Less than | `price<100000` |
+| `<:` | Less than eq | `price<100000` |
+| `*` | Starts with | `brand:*Martin` |
+| `*` | Ends with | `brand:Aston*` |
+| `*` | Contains | `brand:*Martin*` |
+| `OR` | Logical OR | `color:Red OR color:Blue` |
+| `AND` | Logical AND | `brand:Aston* AND price<300000` |
+| `IN` | Value is in list | `color IN ['Red', 'Blue']` |
+| `NOT IN` | Value is not in list | `color NOT IN ['Red', 'Blue']` |
+| `IS EMPTY` | Collection field is empty | `cars IS EMPTY` |
+| `IS NOT EMPTY` | Collection field is not empty | `cars IS NOT EMPTY` |
+| `IS NULL` | Field is null | `brand IS NULL` |
+| `IS NOT NULL` | Field is not null | `brand IS NOT NULL` |
+| `()` | Parenthesis | `brand:Nissan OR (brand:Chevrolet AND color:Blue)` |
+| `BETWEEN` | Value is between two values | `creationyear BETWEEN 2017 AND 2019` |
+| `NOT BETWEEN` | Value is not between two values | `creationyear NOT BETWEEN 2017 AND 2019` |
+
+
+## Examples
+
+1. Using parenthesis
Request : `/cars?search=( brand:Nissan OR brand:Chevrolet ) AND color:Blue`
*Note: Spaces inside the parenthesis are not necessary*
![parenthesis example](./docs/images/parenthesis-example.gif)
-
-11. Using space in nouns
+2. Using space in nouns
Request : `/cars?search=model:'Spacetourer Business Lounge'`
![space example](./docs/images/space-example.gif)
-
-12. Using special characters
+3. Using special characters
Request: `/cars?search=model:中华V7`
![special characters example](./docs/images/special-characters-example.gif)
-
-13. Using deep fields
+4. Using deep fields
Request : `/cars?search=options.transmission:Auto`
![deep field example](./docs/images/deep-field-example.gif)
-
-14. Complex example
+5. Complex example
Request : `/cars?search=creationyear:2018 AND price<300000 AND (color:Yellow OR color:Blue) AND options.transmission:Auto`
![complex example](./docs/images/complex-example.gif)
15. Using the BETWEEN operator
diff --git a/src/main/antlr4/Query.g4 b/src/main/antlr4/Query.g4
index fba830f..4234fd6 100644
--- a/src/main/antlr4/Query.g4
+++ b/src/main/antlr4/Query.g4
@@ -24,6 +24,7 @@ criteria
is_value
: EMPTY
+ | NULL
;
key
@@ -57,6 +58,10 @@ BOOL
| 'false'
;
+NULL
+ : 'NULL'
+ ;
+
STRING
: '"' DoubleStringCharacter* '"'
| '\'' SingleStringCharacter* '\''
diff --git a/src/main/kotlin/com/sipios/springsearch/QueryVisitorImpl.kt b/src/main/kotlin/com/sipios/springsearch/QueryVisitorImpl.kt
index e2367c4..d5fbcd7 100644
--- a/src/main/kotlin/com/sipios/springsearch/QueryVisitorImpl.kt
+++ b/src/main/kotlin/com/sipios/springsearch/QueryVisitorImpl.kt
@@ -37,8 +37,7 @@ class QueryVisitorImpl(private val searchSpecAnnotation: SearchSpec) : QueryB
} else {
SearchOperation.IS_NOT
}
- val value = ctx.is_value!!.text
- return toSpec(key, op, value)
+ return toSpec(key, op, ctx.is_value().text)
}
override fun visitEqArrayCriteria(ctx: QueryParser.EqArrayCriteriaContext): Specification {
diff --git a/src/main/kotlin/com/sipios/springsearch/SearchOperation.kt b/src/main/kotlin/com/sipios/springsearch/SearchOperation.kt
index e44c49f..aa941db 100644
--- a/src/main/kotlin/com/sipios/springsearch/SearchOperation.kt
+++ b/src/main/kotlin/com/sipios/springsearch/SearchOperation.kt
@@ -28,6 +28,8 @@ enum class SearchOperation {
val AND_OPERATOR = "AND"
val LEFT_PARANTHESIS = "("
val RIGHT_PARANTHESIS = ")"
+ val EMPTY = "EMPTY"
+ val NULL = "NULL"
/**
* Parse a string into an operation.
diff --git a/src/main/kotlin/com/sipios/springsearch/strategies/BooleanStrategy.kt b/src/main/kotlin/com/sipios/springsearch/strategies/BooleanStrategy.kt
index b87c182..2d854be 100644
--- a/src/main/kotlin/com/sipios/springsearch/strategies/BooleanStrategy.kt
+++ b/src/main/kotlin/com/sipios/springsearch/strategies/BooleanStrategy.kt
@@ -1,9 +1,11 @@
package com.sipios.springsearch.strategies
+import com.sipios.springsearch.SearchOperation
import kotlin.reflect.KClass
class BooleanStrategy : ParsingStrategy {
override fun parse(value: String?, fieldClass: KClass): Any? {
+ if (value == SearchOperation.NULL) return value
return value?.toBoolean()
}
}
diff --git a/src/main/kotlin/com/sipios/springsearch/strategies/CollectionStrategy.kt b/src/main/kotlin/com/sipios/springsearch/strategies/CollectionStrategy.kt
index 468f84e..9004e93 100644
--- a/src/main/kotlin/com/sipios/springsearch/strategies/CollectionStrategy.kt
+++ b/src/main/kotlin/com/sipios/springsearch/strategies/CollectionStrategy.kt
@@ -4,9 +4,10 @@ import com.sipios.springsearch.SearchOperation
import jakarta.persistence.criteria.CriteriaBuilder
import jakarta.persistence.criteria.Path
import jakarta.persistence.criteria.Predicate
+import org.springframework.http.HttpStatus
+import org.springframework.web.server.ResponseStatusException
class CollectionStrategy : ParsingStrategy {
-
override fun buildPredicate(
builder: CriteriaBuilder,
path: Path<*>,
@@ -14,12 +15,15 @@ class CollectionStrategy : ParsingStrategy {
ops: SearchOperation?,
value: Any?
): Predicate? {
- if (ops == SearchOperation.IS && value != null) {
+ if (ops == SearchOperation.IS && value == SearchOperation.EMPTY) {
return builder.isEmpty(path[fieldName])
}
- if (ops == SearchOperation.IS_NOT && value != null) {
+ if (ops == SearchOperation.IS_NOT && value == SearchOperation.EMPTY) {
return builder.isNotEmpty(path[fieldName])
}
- throw IllegalArgumentException("Unsupported operation $ops for collection field $fieldName, only IS and IS_NOT are supported")
+ throw ResponseStatusException(HttpStatus.BAD_REQUEST,
+ "Unsupported operation $ops $value for collection field $fieldName, " +
+ "only IS EMPTY and IS NOT EMPTY are supported"
+ )
}
}
diff --git a/src/main/kotlin/com/sipios/springsearch/strategies/DateStrategy.kt b/src/main/kotlin/com/sipios/springsearch/strategies/DateStrategy.kt
index 61f5c69..b4b5597 100644
--- a/src/main/kotlin/com/sipios/springsearch/strategies/DateStrategy.kt
+++ b/src/main/kotlin/com/sipios/springsearch/strategies/DateStrategy.kt
@@ -29,6 +29,7 @@ class DateStrategy : ParsingStrategy {
}
override fun parse(value: String?, fieldClass: KClass): Any? {
+ if (value == SearchOperation.NULL) return value
return standardDateFormat.parse(value)
}
}
diff --git a/src/main/kotlin/com/sipios/springsearch/strategies/DoubleStrategy.kt b/src/main/kotlin/com/sipios/springsearch/strategies/DoubleStrategy.kt
index 7b1ccc0..85cbbe8 100644
--- a/src/main/kotlin/com/sipios/springsearch/strategies/DoubleStrategy.kt
+++ b/src/main/kotlin/com/sipios/springsearch/strategies/DoubleStrategy.kt
@@ -24,6 +24,7 @@ class DoubleStrategy : ParsingStrategy {
}
override fun parse(value: String?, fieldClass: KClass): Any? {
+ if (value == SearchOperation.NULL) return value
return value?.toDouble()
}
}
diff --git a/src/main/kotlin/com/sipios/springsearch/strategies/DurationStrategy.kt b/src/main/kotlin/com/sipios/springsearch/strategies/DurationStrategy.kt
index a722fe2..dee1e03 100644
--- a/src/main/kotlin/com/sipios/springsearch/strategies/DurationStrategy.kt
+++ b/src/main/kotlin/com/sipios/springsearch/strategies/DurationStrategy.kt
@@ -25,6 +25,7 @@ class DurationStrategy : ParsingStrategy {
}
override fun parse(value: String?, fieldClass: KClass): Any? {
+ if (value == SearchOperation.NULL) return value
return Duration.parse(value)
}
}
diff --git a/src/main/kotlin/com/sipios/springsearch/strategies/EnumStrategy.kt b/src/main/kotlin/com/sipios/springsearch/strategies/EnumStrategy.kt
index 4e74ee5..451d31b 100644
--- a/src/main/kotlin/com/sipios/springsearch/strategies/EnumStrategy.kt
+++ b/src/main/kotlin/com/sipios/springsearch/strategies/EnumStrategy.kt
@@ -1,9 +1,11 @@
package com.sipios.springsearch.strategies
+import com.sipios.springsearch.SearchOperation
import kotlin.reflect.KClass
class EnumStrategy : ParsingStrategy {
override fun parse(value: String?, fieldClass: KClass): Any? {
+ if (value == SearchOperation.NULL) return value
return Class.forName(fieldClass.qualifiedName).getMethod("valueOf", String::class.java).invoke(null, value)
}
}
diff --git a/src/main/kotlin/com/sipios/springsearch/strategies/FloatStrategy.kt b/src/main/kotlin/com/sipios/springsearch/strategies/FloatStrategy.kt
index ea1d1e6..85f292d 100644
--- a/src/main/kotlin/com/sipios/springsearch/strategies/FloatStrategy.kt
+++ b/src/main/kotlin/com/sipios/springsearch/strategies/FloatStrategy.kt
@@ -24,6 +24,7 @@ class FloatStrategy : ParsingStrategy {
}
override fun parse(value: String?, fieldClass: KClass): Any? {
+ if (value == SearchOperation.NULL) return value
return value?.toFloat()
}
}
diff --git a/src/main/kotlin/com/sipios/springsearch/strategies/InstantStrategy.kt b/src/main/kotlin/com/sipios/springsearch/strategies/InstantStrategy.kt
index 66294cd..65af72e 100644
--- a/src/main/kotlin/com/sipios/springsearch/strategies/InstantStrategy.kt
+++ b/src/main/kotlin/com/sipios/springsearch/strategies/InstantStrategy.kt
@@ -25,6 +25,7 @@ class InstantStrategy : ParsingStrategy {
}
override fun parse(value: String?, fieldClass: KClass): Any? {
+ if (value == SearchOperation.NULL) return value
return Instant.parse(value)
}
}
diff --git a/src/main/kotlin/com/sipios/springsearch/strategies/IntStrategy.kt b/src/main/kotlin/com/sipios/springsearch/strategies/IntStrategy.kt
index 21f3863..ba6ee55 100644
--- a/src/main/kotlin/com/sipios/springsearch/strategies/IntStrategy.kt
+++ b/src/main/kotlin/com/sipios/springsearch/strategies/IntStrategy.kt
@@ -24,6 +24,7 @@ class IntStrategy : ParsingStrategy {
}
override fun parse(value: String?, fieldClass: KClass): Any? {
+ if (value == SearchOperation.NULL) return value
return value?.toInt()
}
}
diff --git a/src/main/kotlin/com/sipios/springsearch/strategies/LocalDateStrategy.kt b/src/main/kotlin/com/sipios/springsearch/strategies/LocalDateStrategy.kt
index 2b435f6..c9c0b31 100644
--- a/src/main/kotlin/com/sipios/springsearch/strategies/LocalDateStrategy.kt
+++ b/src/main/kotlin/com/sipios/springsearch/strategies/LocalDateStrategy.kt
@@ -25,6 +25,7 @@ class LocalDateStrategy : ParsingStrategy {
}
override fun parse(value: String?, fieldClass: KClass): Any? {
+ if (value == SearchOperation.NULL) return value
return LocalDate.parse(value)
}
}
diff --git a/src/main/kotlin/com/sipios/springsearch/strategies/LocalDateTimeStrategy.kt b/src/main/kotlin/com/sipios/springsearch/strategies/LocalDateTimeStrategy.kt
index 763dae5..00ef88b 100644
--- a/src/main/kotlin/com/sipios/springsearch/strategies/LocalDateTimeStrategy.kt
+++ b/src/main/kotlin/com/sipios/springsearch/strategies/LocalDateTimeStrategy.kt
@@ -25,6 +25,7 @@ class LocalDateTimeStrategy : ParsingStrategy {
}
override fun parse(value: String?, fieldClass: KClass): Any? {
+ if (value == SearchOperation.NULL) return value
return LocalDateTime.parse(value)
}
}
diff --git a/src/main/kotlin/com/sipios/springsearch/strategies/LocalTimeStrategy.kt b/src/main/kotlin/com/sipios/springsearch/strategies/LocalTimeStrategy.kt
index 7682689..cc8b34b 100644
--- a/src/main/kotlin/com/sipios/springsearch/strategies/LocalTimeStrategy.kt
+++ b/src/main/kotlin/com/sipios/springsearch/strategies/LocalTimeStrategy.kt
@@ -25,6 +25,7 @@ class LocalTimeStrategy : ParsingStrategy {
}
override fun parse(value: String?, fieldClass: KClass): Any? {
+ if (value == SearchOperation.NULL) return value
return LocalTime.parse(value)
}
}
diff --git a/src/main/kotlin/com/sipios/springsearch/strategies/ParsingStrategy.kt b/src/main/kotlin/com/sipios/springsearch/strategies/ParsingStrategy.kt
index 2986bf8..b2670c3 100644
--- a/src/main/kotlin/com/sipios/springsearch/strategies/ParsingStrategy.kt
+++ b/src/main/kotlin/com/sipios/springsearch/strategies/ParsingStrategy.kt
@@ -14,7 +14,8 @@ import java.util.Date
import java.util.UUID
import kotlin.reflect.KClass
import kotlin.reflect.full.isSubclassOf
-
+import org.springframework.http.HttpStatus
+import org.springframework.web.server.ResponseStatusException
interface ParsingStrategy {
/**
* Method to parse the value specified to the corresponding strategy
@@ -59,6 +60,27 @@ interface ParsingStrategy {
builder.not(inClause)
}
+ SearchOperation.IS -> {
+ if (value == SearchOperation.NULL) {
+ builder.isNull(path.get(fieldName))
+ } else {
+ // we should not call parent method for collection fields
+ // so this makes no sense to search for EMPTY with a non-collection field
+ throw ResponseStatusException(HttpStatus.BAD_REQUEST,
+ "Unsupported operation $ops $value for field $fieldName")
+ }
+ }
+ SearchOperation.IS_NOT -> {
+ if (value == SearchOperation.NULL) {
+ builder.isNotNull(path.get(fieldName))
+ } else {
+ // we should not call parent method for collection fields
+ // so this makes no sense to search for NOT EMPTY with a non-collection field
+ throw ResponseStatusException(HttpStatus.BAD_REQUEST,
+ "Unsupported operation $ops $value for field $fieldName")
+ }
+ }
+
SearchOperation.EQUALS -> builder.equal(path.get(fieldName), value)
SearchOperation.NOT_EQUALS -> builder.notEqual(path.get(fieldName), value)
SearchOperation.STARTS_WITH -> builder.like(path[fieldName], "$value%")
diff --git a/src/main/kotlin/com/sipios/springsearch/strategies/UUIDStrategy.kt b/src/main/kotlin/com/sipios/springsearch/strategies/UUIDStrategy.kt
index 22542f2..013eff7 100644
--- a/src/main/kotlin/com/sipios/springsearch/strategies/UUIDStrategy.kt
+++ b/src/main/kotlin/com/sipios/springsearch/strategies/UUIDStrategy.kt
@@ -1,10 +1,12 @@
package com.sipios.springsearch.strategies
+import com.sipios.springsearch.SearchOperation
import java.util.UUID
import kotlin.reflect.KClass
class UUIDStrategy : ParsingStrategy {
override fun parse(value: String?, fieldClass: KClass): Any? {
+ if (value == SearchOperation.NULL) return value
return UUID.fromString(value)
}
}
diff --git a/src/test/kotlin/com/sipios/springsearch/SpringSearchApplicationTest.kt b/src/test/kotlin/com/sipios/springsearch/SpringSearchApplicationTest.kt
index f7f43ac..632e180 100644
--- a/src/test/kotlin/com/sipios/springsearch/SpringSearchApplicationTest.kt
+++ b/src/test/kotlin/com/sipios/springsearch/SpringSearchApplicationTest.kt
@@ -7,6 +7,7 @@ import java.time.Instant
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime
+import java.util.Date
import java.util.UUID
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
@@ -1283,6 +1284,31 @@ class SpringSearchApplicationTest {
Assertions.assertTrue(users.isEmpty())
}
+ @Test
+ fun cantSearchForEmptyWithNonFieldProperties() {
+ val johnBook = Book()
+ val john = Author()
+ john.name = "john"
+ john.addBook(johnBook)
+ authorRepository.save(john)
+ val janeBook = Book()
+ val jane = Author()
+ jane.name = "jane"
+ jane.addBook(janeBook)
+ authorRepository.save(jane)
+ val specification = SpecificationsBuilder(
+ SearchSpec::class.constructors.first().call("", false)
+ ).withSearch("name IS EMPTY").build()
+ Assertions.assertThrows(
+ ResponseStatusException::class.java
+ ) { authorRepository.findAll(specification) }
+ val specification2 = SpecificationsBuilder(
+ SearchSpec::class.constructors.first().call("", false)
+ ).withSearch("name IS NOT EMPTY").build()
+ Assertions.assertThrows(
+ ResponseStatusException::class.java
+ ) { authorRepository.findAll(specification2) }
+ }
@Test
fun canGetAuthorsWithEmptyBookWithResult() {
val johnBook = Book()
@@ -1352,6 +1378,27 @@ class SpringSearchApplicationTest {
val users = authorRepository.findAll(specification)
Assertions.assertTrue(users.size == 0)
}
+ @Test
+ fun cantGetAuthorsWithBooksNull() {
+ val john = Author()
+ john.name = "john"
+ authorRepository.save(john)
+ val jane = Author()
+ jane.name = "jane"
+ authorRepository.save(jane)
+ val spec = SpecificationsBuilder(
+ SearchSpec::class.constructors.first().call("", false)
+ ).withSearch("books IS NULL").build()
+ Assertions.assertThrows(
+ ResponseStatusException::class.java
+ ) { authorRepository.findAll(spec) }
+ val specNotNull = SpecificationsBuilder(
+ SearchSpec::class.constructors.first().call("", false)
+ ).withSearch("books IS NOT NULL").build()
+ Assertions.assertThrows(
+ ResponseStatusException::class.java
+ ) { authorRepository.findAll(specNotNull) }
+ }
@Test
fun canGetUsersWithNumberOfChildrenBetween() {
@@ -1519,4 +1566,145 @@ class SpringSearchApplicationTest {
).withSearch("userId IN [").build()
}
}
+
+ @Test
+ fun canGetUsersWithNullColumn() {
+ userRepository.save(Users(userFirstName = "john", type = null))
+ userRepository.save(Users(userFirstName = "jane", type = UserType.ADMINISTRATOR))
+ userRepository.save(Users(userFirstName = "joe", type = UserType.MANAGER))
+ userRepository.save(Users(userFirstName = "jean", type = null))
+ val specification = SpecificationsBuilder(
+ SearchSpec::class.constructors.first().call("", false)
+ ).withSearch("type IS NULL").build()
+ val users = userRepository.findAll(specification)
+ Assertions.assertEquals(2, users.size)
+ val setNames = users.map { user -> user.userFirstName }.toSet()
+ Assertions.assertEquals(setOf("john", "jean"), setNames)
+ }
+
+ @Test
+ fun canGetUsersWithNotNullColumn() {
+ userRepository.save(Users(userFirstName = "john", type = null))
+ userRepository.save(Users(userFirstName = "jane", type = UserType.ADMINISTRATOR))
+ userRepository.save(Users(userFirstName = "joe", type = UserType.MANAGER))
+ userRepository.save(Users(userFirstName = "jean", type = null))
+ val specification = SpecificationsBuilder(
+ SearchSpec::class.constructors.first().call("", false)
+ ).withSearch("type IS NOT NULL").build()
+ val users = userRepository.findAll(specification)
+ Assertions.assertEquals(2, users.size)
+ val setNames = users.map { user -> user.userFirstName }.toSet()
+ Assertions.assertEquals(setOf("jane", "joe"), setNames)
+ }
+
+ @Test
+ fun canGetUsersWithNotNullFirstName() {
+ userRepository.save(Users(userFirstName = "john", type = null))
+ userRepository.save(Users(userFirstName = "jane", type = UserType.ADMINISTRATOR))
+ userRepository.save(Users(userFirstName = "joe", type = UserType.MANAGER))
+ userRepository.save(Users(userFirstName = "jean", type = null))
+ val specification = SpecificationsBuilder(
+ SearchSpec::class.constructors.first().call("", false)
+ ).withSearch("userFirstName IS NOT NULL").build()
+ val users = userRepository.findAll(specification)
+ Assertions.assertEquals(4, users.size)
+ val setNames = users.map { user -> user.userFirstName }.toSet()
+ Assertions.assertEquals(setOf("john", "jane", "joe", "jean"), setNames)
+ }
+
+ @Test
+ fun canGetUserWithNullSalary() {
+ userRepository.save(Users(userFirstName = "john", userSalary = 100.0F))
+ userRepository.save(Users(userFirstName = "jane", userSalary = 1000.0F))
+ val specification = SpecificationsBuilder(
+ SearchSpec::class.constructors.first().call("", false)
+ ).withSearch("userSalary IS NULL").build()
+ val users = userRepository.findAll(specification)
+ Assertions.assertEquals(0, users.size)
+ }
+
+ @Test
+ fun canGetUsersWithUUIDNull() {
+ val userUUID = UUID.randomUUID()
+ userRepository.save(Users(userFirstName = "Diego", uuid = userUUID))
+ userRepository.save(Users(userFirstName = "Diego two", uuid = null))
+ val specification = SpecificationsBuilder(
+ SearchSpec::class.constructors.first().call("", false)
+ ).withSearch("uuid IS NULL").build()
+ val robotUsers = userRepository.findAll(specification)
+ Assertions.assertEquals(1, robotUsers.size)
+ Assertions.assertEquals(null, robotUsers[0].uuid)
+ }
+
+ @Test
+ fun canGetUsersWithUpdatedDateAtNull() {
+ userRepository.save(Users(userFirstName = "john", updatedDateAt = LocalDate.parse("2020-01-10")))
+ userRepository.save(Users(userFirstName = "jane", updatedDateAt = LocalDate.parse("2020-01-11")))
+ userRepository.save(Users(userFirstName = "joe", updatedDateAt = null))
+ userRepository.save(Users(userFirstName = "jean", updatedDateAt = null))
+ val specification = SpecificationsBuilder(
+ SearchSpec::class.constructors.first().call("", false)
+ ).withSearch("updatedDateAt IS NULL").build()
+ val users = userRepository.findAll(specification)
+ Assertions.assertEquals(2, users.size)
+ val setNames = users.map { user -> user.userFirstName }.toSet()
+ Assertions.assertEquals(setOf("joe", "jean"), setNames)
+ }
+
+ @Test
+ fun canGetUsersWithUpdatedDateTimeAtNotNull() {
+ userRepository.save(Users(userFirstName = "john", updatedAt = LocalDateTime.parse("2020-01-10T10:15:30")))
+ userRepository.save(Users(userFirstName = "jane", updatedAt = LocalDateTime.parse("2020-01-11T10:15:30")))
+ userRepository.save(Users(userFirstName = "joe", updatedAt = null))
+ userRepository.save(Users(userFirstName = "jean", updatedAt = LocalDateTime.parse("2020-01-13T10:15:30")))
+ val specification = SpecificationsBuilder(
+ SearchSpec::class.constructors.first().call("", false)
+ ).withSearch("updatedAt IS NOT NULL").build()
+ val users = userRepository.findAll(specification)
+ Assertions.assertEquals(3, users.size)
+ val setNames = users.map { user -> user.userFirstName }.toSet()
+ Assertions.assertEquals(setOf("john", "jane", "jean"), setNames)
+ }
+ @Test
+ fun canGetUsersWithActiveNull() {
+ userRepository.save(Users(userFirstName = "john", active = true))
+ userRepository.save(Users(userFirstName = "jane", active = false))
+ userRepository.save(Users(userFirstName = "joe", active = null))
+ userRepository.save(Users(userFirstName = "jean", active = null))
+ val specification = SpecificationsBuilder(
+ SearchSpec::class.constructors.first().call("", false)
+ ).withSearch("active IS NULL").build()
+ val users = userRepository.findAll(specification)
+ Assertions.assertEquals(2, users.size)
+ val setNames = users.map { user -> user.userFirstName }.toSet()
+ Assertions.assertEquals(setOf("joe", "jean"), setNames)
+ }
+ @Test
+ fun canGetUsersWithUserChildrenNumberNull() {
+ userRepository.save(Users(userFirstName = "john", userChildrenNumber = 2))
+ userRepository.save(Users(userFirstName = "jane", userChildrenNumber = 3))
+ userRepository.save(Users(userFirstName = "joe", userChildrenNumber = null))
+ userRepository.save(Users(userFirstName = "jean", userChildrenNumber = null))
+ val specification = SpecificationsBuilder(
+ SearchSpec::class.constructors.first().call("", false)
+ ).withSearch("userChildrenNumber IS NULL").build()
+ val users = userRepository.findAll(specification)
+ Assertions.assertEquals(2, users.size)
+ val setNames = users.map { user -> user.userFirstName }.toSet()
+ Assertions.assertEquals(setOf("joe", "jean"), setNames)
+ }
+ @Test
+ fun canGetUsersWithCreatedAtNull() {
+ userRepository.save(Users(userFirstName = "john", createdAt = Date()))
+ userRepository.save(Users(userFirstName = "jane", createdAt = Date()))
+ userRepository.save(Users(userFirstName = "joe", createdAt = null))
+ userRepository.save(Users(userFirstName = "jean", createdAt = null))
+ val specification = SpecificationsBuilder(
+ SearchSpec::class.constructors.first().call("", false)
+ ).withSearch("createdAt IS NULL").build()
+ val users = userRepository.findAll(specification)
+ Assertions.assertEquals(2, users.size)
+ val setNames = users.map { user -> user.userFirstName }.toSet()
+ Assertions.assertEquals(setOf("joe", "jean"), setNames)
+ }
}
diff --git a/src/test/kotlin/com/sipios/springsearch/Users.kt b/src/test/kotlin/com/sipios/springsearch/Users.kt
index 24a47cc..8b91107 100644
--- a/src/test/kotlin/com/sipios/springsearch/Users.kt
+++ b/src/test/kotlin/com/sipios/springsearch/Users.kt
@@ -37,7 +37,7 @@ data class Users(
var userAddress: String = "1 rue de l'angleterre",
@Column(name = "NumberOfChildren")
- var userChildrenNumber: Int = 3,
+ var userChildrenNumber: Int? = 3,
@Column(name = "Salary")
var userSalary: Float = 3000.0F,
@@ -46,16 +46,16 @@ data class Users(
var userAgeInSeconds: Double = 1261440000.0,
@Column
- var createdAt: Date = Date(),
+ var createdAt: Date? = Date(),
@Column
- var updatedAt: LocalDateTime = LocalDateTime.now(),
+ var updatedAt: LocalDateTime? = LocalDateTime.now(),
@Column
var updatedTimeAt: LocalTime = LocalTime.now(),
@Column
- var updatedDateAt: LocalDate = LocalDate.now(),
+ var updatedDateAt: LocalDate? = LocalDate.now(),
@Column
var updatedInstantAt: Instant = Instant.now(),
@@ -67,5 +67,8 @@ data class Users(
var type: UserType? = UserType.TEAM_MEMBER,
@Column
- var uuid: UUID = UUID.randomUUID()
+ var uuid: UUID? = UUID.randomUUID(),
+
+ @Column
+ var active: Boolean? = true
)