I would like to use Jooq with the Kotlin language to create a way to dynamically generate Jooq Condition objects.
I want to put Jooq Field objects in a Kotlin Map from String to TableField (a Jooq class). I want to pull values from this map and use them to build the Condition object.
I have a simple proof of concept of this working with Java that I coded in the form of a unit test. That code looks like this (omitted the imports and class, but this is the idea.
private static final Map<String, TableField> FIELDS = Map.of(
"tagId", TagV2Table.Companion.getTAG_V2().getTAG_ID(),
"createdDate", ApplicationTable.Companion.getAPPLICATION().getCREATED_DATE()
);
@Test
void testDynamicGeneration() {
TableField idField = FIELDS.get("tagId");
TableField createTimeField = FIELDS.get("createdDate");
final Condition condition = noCondition()
.and(idField.eq("myId"))
.and(createTimeField.lt(Instant.ofEpochMilli(123456)));
final String expected = "(\n" +
" \"public\".\"tag_v2\".\"tag_id\" = 'myId'\n" +
" and \"public\".\"application\".\"created_date\" < timestamp with time zone '1970-01-01 00:02:03.456+00:00'\n" +
")";
assertEquals(expected, condition.toString());
The condition that gets created is what is in "expected", which is what I want.
If I try and translate the same code into Kotlin, I get compilation errors, which I am pretty sure is due to the "out" on the Map. But I don't know enough to know how to fix it. This is the equivalent Kotlin code I wrote:
private companion object {
// The IntelliJ inferred type is:
// Map<String, TableField<out UpdatableRecordImpl<*>, out Any?>>
// AFAICT, the "out" of "out Any?" is making the ".eq" and ".lt" functions unavailable
// because it's unsafe.
val fields = mapOf(
"tagId" to TAG_V2.TAG_ID,
"createdDate" to APPLICATION.CREATED_DATE,
)
}
@Test
fun testDynamicGenerationKotlin() {
val idField = fields["tagId"]
val createTimeField = fields["createdDate"]
val condition = noCondition()
// .eq and .lt "None of the following functions can be called with the arguments supplied"
// and the accepted arguments are all Nothing
.and(idField.eq("myId"))
.and(createTimeField.lt(Instant.ofEpochMilli(123456)))
}
The issue is that the functions .eq(...)
and .lt(...)
are not available on the objects idField
and createTimeField
that get pulled out of the Map.
Is there a way for me to declare this Map such that the Kotlin code will work (like the Java code does)?
Kotlin doesn't have the equivalent of Java's rawtypes, but you can always unsafe cast like this:
.and((idField as Field<String>).eq("myId"))
.and((createTimeField as Field<Instant>).lt(Instant.ofEpochMilli(123456)))
Or, rather than using a simple map, you write a more sophisticated utility (wrapping a map) that takes care of the typing.
Your actual problem seems to be having reusable columns across your schema. You could declare an interface returning those columns from any table and let generated tables extend this interface using a generator strategy or a matcher strategy.
E.g.
interface MyJooqTable<R : Record> : Table<R> {
fun tagId(): Field<String> = field("tag_id", SQLDataType.STRING)
fun createdDate(): Field<Instant> = field("CREATED_DATE", SQLDataType.INSTANT)
}
Now you can pass around MyJooqTable
references to your utilities and type safely dereference your columns from it, assuming the columns exist.