Skip to content

Commit 9b853ed

Browse files
committed
fix conversion of value classes to dataframe
1 parent 63fdb46 commit 9b853ed

File tree

4 files changed

+189
-2
lines changed
  • core

4 files changed

+189
-2
lines changed

core/generated-sources/src/main/kotlin/org/jetbrains/kotlinx/dataframe/impl/api/toDataFrame.kt

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,16 @@ import org.jetbrains.kotlinx.dataframe.impl.getListType
2121
import org.jetbrains.kotlinx.dataframe.impl.projectUpTo
2222
import org.jetbrains.kotlinx.dataframe.impl.schema.getPropertiesOrder
2323
import java.lang.reflect.InvocationTargetException
24+
import java.lang.reflect.Method
2425
import java.time.temporal.Temporal
2526
import kotlin.reflect.KClass
2627
import kotlin.reflect.KProperty
2728
import kotlin.reflect.KVisibility
2829
import kotlin.reflect.full.isSubclassOf
2930
import kotlin.reflect.full.memberProperties
31+
import kotlin.reflect.full.primaryConstructor
3032
import kotlin.reflect.full.withNullability
33+
import kotlin.reflect.jvm.isAccessible
3134
import kotlin.reflect.jvm.javaField
3235
import kotlin.reflect.typeOf
3336

@@ -141,7 +144,26 @@ internal fun convertToDataFrame(
141144
val property = it
142145
if (excludes.contains(property)) return@mapNotNull null
143146

147+
class ValueClassConverter(val unbox: Method, val box: Method)
148+
149+
val valueClassConverter = (it.returnType.classifier as? KClass<*>)?.let { kClass ->
150+
if (!kClass.isValue) null else {
151+
val constructor =
152+
requireNotNull(kClass.primaryConstructor) { "value class $kClass is expected to have primary constructor, but couldn't obtain it" }
153+
val parameter = constructor.parameters.singleOrNull()
154+
?: error("conversion of value class $kClass with multiple parameters in constructor is not yet supported")
155+
// there's no need to unwrap if underlying field is nullable
156+
if (parameter.type.isMarkedNullable) return@let null
157+
// box and unbox impl methods are part of binary API of value classes
158+
// https://youtrack.jetbrains.com/issue/KT-50518/Boxing-Unboxing-methods-for-JvmInline-value-classes-should-be-public-accessible
159+
val unbox = kClass.java.getMethod("unbox-impl")
160+
val box = kClass.java.methods.single { it.name == "box-impl" }
161+
val valueClassConverter = ValueClassConverter(unbox, box)
162+
valueClassConverter
163+
}
164+
}
144165
property.javaField?.isAccessible = true
166+
// property.isAccessible = true
145167

146168
var nullable = false
147169
var hasExceptions = false
@@ -151,7 +173,19 @@ internal fun convertToDataFrame(
151173
null
152174
} else {
153175
val value = try {
154-
it.call(obj)
176+
val value = it.call(obj)
177+
/**
178+
* here we do what compiler does
179+
* @see org.jetbrains.kotlinx.dataframe.api.CreateDataFrameTests.testKPropertyGetLibrary
180+
*/
181+
if (valueClassConverter != null) {
182+
val var1 = value?.let {
183+
valueClassConverter.unbox.invoke(it)
184+
}
185+
var1?.let { valueClassConverter.box.invoke(null, var1) }
186+
} else {
187+
value
188+
}
155189
} catch (e: InvocationTargetException) {
156190
hasExceptions = true
157191
e.targetException

core/generated-sources/src/test/kotlin/org/jetbrains/kotlinx/dataframe/api/toDataFrame.kt

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import org.jetbrains.kotlinx.dataframe.kind
1010
import org.jetbrains.kotlinx.dataframe.type
1111
import org.junit.Ignore
1212
import org.junit.Test
13+
import kotlin.reflect.KProperty
1314
import kotlin.reflect.typeOf
1415

1516
class CreateDataFrameTests {
@@ -224,4 +225,63 @@ class CreateDataFrameTests {
224225
println()
225226
}
226227
}
228+
229+
// nullable field here - no generated unwrapping code
230+
@JvmInline
231+
internal value class Speed(val kmh: Number?)
232+
233+
internal class PathSegment(
234+
val id: String,
235+
val speedLimit: Speed? = null,
236+
)
237+
238+
@Test
239+
fun valueClassNullableField() {
240+
val segments = listOf(PathSegment("foo", Speed(2.3)), PathSegment("bar"))
241+
242+
val df = segments.toDataFrame()
243+
df["speedLimit"].values() shouldBe listOf(Speed(2.3), null)
244+
}
245+
246+
@Test
247+
fun valueClassNullableField1() {
248+
val segments = listOf(PathSegment("foo", Speed(2.3)), PathSegment("bar", Speed(null)))
249+
250+
val df = segments.toDataFrame()
251+
df["speedLimit"].values() shouldBe listOf(Speed(2.3), Speed(null))
252+
}
253+
254+
@JvmInline
255+
internal value class Speed1(val kmh: Number)
256+
257+
internal class PathSegment1(
258+
val id: String,
259+
val speedLimit: Speed1? = null,
260+
)
261+
262+
@Test
263+
fun valueClass() {
264+
val segments = listOf(PathSegment1("foo", Speed1(2.3)), PathSegment1("bar"))
265+
266+
val df = segments.toDataFrame()
267+
df["speedLimit"].values() shouldBe listOf(Speed1(2.3), null)
268+
}
269+
270+
@Test
271+
fun testKPropertyGet() {
272+
val segment = PathSegment("bar")
273+
val result = PathSegment::speedLimit.call(segment)
274+
result shouldBe null
275+
}
276+
277+
fun call(kProperty0: KProperty<*>, obj: Any) = kProperty0.call(obj)
278+
279+
@Test
280+
fun testKPropertyCallLibrary() {
281+
val segment = PathSegment1("bar")
282+
val result = call(PathSegment1::speedLimit, segment)
283+
// Sudden result! I cannot create this value, so toString.
284+
// In the test above you can see decompiled code that "fixes" this strange wrapping
285+
result.toString() shouldBe "Speed1(kmh=null)"
286+
}
227287
}

core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/impl/api/toDataFrame.kt

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,16 @@ import org.jetbrains.kotlinx.dataframe.impl.getListType
2121
import org.jetbrains.kotlinx.dataframe.impl.projectUpTo
2222
import org.jetbrains.kotlinx.dataframe.impl.schema.getPropertiesOrder
2323
import java.lang.reflect.InvocationTargetException
24+
import java.lang.reflect.Method
2425
import java.time.temporal.Temporal
2526
import kotlin.reflect.KClass
2627
import kotlin.reflect.KProperty
2728
import kotlin.reflect.KVisibility
2829
import kotlin.reflect.full.isSubclassOf
2930
import kotlin.reflect.full.memberProperties
31+
import kotlin.reflect.full.primaryConstructor
3032
import kotlin.reflect.full.withNullability
33+
import kotlin.reflect.jvm.isAccessible
3134
import kotlin.reflect.jvm.javaField
3235
import kotlin.reflect.typeOf
3336

@@ -141,6 +144,24 @@ internal fun convertToDataFrame(
141144
val property = it
142145
if (excludes.contains(property)) return@mapNotNull null
143146

147+
class ValueClassConverter(val unbox: Method, val box: Method)
148+
149+
val valueClassConverter = (it.returnType.classifier as? KClass<*>)?.let { kClass ->
150+
if (!kClass.isValue) null else {
151+
val constructor =
152+
requireNotNull(kClass.primaryConstructor) { "value class $kClass is expected to have primary constructor, but couldn't obtain it" }
153+
val parameter = constructor.parameters.singleOrNull()
154+
?: error("conversion of value class $kClass with multiple parameters in constructor is not yet supported")
155+
// there's no need to unwrap if underlying field is nullable
156+
if (parameter.type.isMarkedNullable) return@let null
157+
// box and unbox impl methods are part of binary API of value classes
158+
// https://youtrack.jetbrains.com/issue/KT-50518/Boxing-Unboxing-methods-for-JvmInline-value-classes-should-be-public-accessible
159+
val unbox = kClass.java.getMethod("unbox-impl")
160+
val box = kClass.java.methods.single { it.name == "box-impl" }
161+
val valueClassConverter = ValueClassConverter(unbox, box)
162+
valueClassConverter
163+
}
164+
}
144165
property.javaField?.isAccessible = true
145166

146167
var nullable = false
@@ -151,7 +172,19 @@ internal fun convertToDataFrame(
151172
null
152173
} else {
153174
val value = try {
154-
it.call(obj)
175+
val value = it.call(obj)
176+
/**
177+
* here we do what compiler does
178+
* @see org.jetbrains.kotlinx.dataframe.api.CreateDataFrameTests.testKPropertyGetLibrary
179+
*/
180+
if (valueClassConverter != null) {
181+
val var1 = value?.let {
182+
valueClassConverter.unbox.invoke(it)
183+
}
184+
var1?.let { valueClassConverter.box.invoke(null, var1) }
185+
} else {
186+
value
187+
}
155188
} catch (e: InvocationTargetException) {
156189
hasExceptions = true
157190
e.targetException

core/src/test/kotlin/org/jetbrains/kotlinx/dataframe/api/toDataFrame.kt

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import org.jetbrains.kotlinx.dataframe.kind
1010
import org.jetbrains.kotlinx.dataframe.type
1111
import org.junit.Ignore
1212
import org.junit.Test
13+
import kotlin.reflect.KProperty
1314
import kotlin.reflect.typeOf
1415

1516
class CreateDataFrameTests {
@@ -224,4 +225,63 @@ class CreateDataFrameTests {
224225
println()
225226
}
226227
}
228+
229+
// nullable field here - no generated unwrapping code
230+
@JvmInline
231+
internal value class Speed(val kmh: Number?)
232+
233+
internal class PathSegment(
234+
val id: String,
235+
val speedLimit: Speed? = null,
236+
)
237+
238+
@Test
239+
fun valueClassNullableField() {
240+
val segments = listOf(PathSegment("foo", Speed(2.3)), PathSegment("bar"))
241+
242+
val df = segments.toDataFrame()
243+
df["speedLimit"].values() shouldBe listOf(Speed(2.3), null)
244+
}
245+
246+
@Test
247+
fun valueClassNullableField1() {
248+
val segments = listOf(PathSegment("foo", Speed(2.3)), PathSegment("bar", Speed(null)))
249+
250+
val df = segments.toDataFrame()
251+
df["speedLimit"].values() shouldBe listOf(Speed(2.3), Speed(null))
252+
}
253+
254+
@JvmInline
255+
internal value class Speed1(val kmh: Number)
256+
257+
internal class PathSegment1(
258+
val id: String,
259+
val speedLimit: Speed1? = null,
260+
)
261+
262+
@Test
263+
fun valueClass() {
264+
val segments = listOf(PathSegment1("foo", Speed1(2.3)), PathSegment1("bar"))
265+
266+
val df = segments.toDataFrame()
267+
df["speedLimit"].values() shouldBe listOf(Speed1(2.3), null)
268+
}
269+
270+
@Test
271+
fun testKPropertyGet() {
272+
val segment = PathSegment("bar")
273+
val result = PathSegment::speedLimit.call(segment)
274+
result shouldBe null
275+
}
276+
277+
fun call(kProperty0: KProperty<*>, obj: Any) = kProperty0.call(obj)
278+
279+
@Test
280+
fun testKPropertyCallLibrary() {
281+
val segment = PathSegment1("bar")
282+
val result = call(PathSegment1::speedLimit, segment)
283+
// Sudden result! I cannot create this value, so toString.
284+
// In the test above you can see decompiled code that "fixes" this strange wrapping
285+
result.toString() shouldBe "Speed1(kmh=null)"
286+
}
227287
}

0 commit comments

Comments
 (0)