Skip to content

Commit cce57cc

Browse files
Add SQL type mapping documentation and SQLite metadata probe for testing
1 parent 71ee483 commit cce57cc

11 files changed

Lines changed: 1673 additions & 78 deletions

File tree

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
package org.jetbrains.kotlinx.dataframe.io.db
2+
3+
import org.junit.Ignore
4+
import org.junit.Test
5+
import java.io.File
6+
import java.nio.file.Files
7+
import java.sql.DriverManager
8+
import java.sql.Types
9+
10+
/**
11+
* Probe used to figure out what the Xerial SQLite JDBC driver reports in the metadata
12+
* for common declared column types (SQLite type affinity is applied at storage level, but
13+
* the driver preserves the declared type name in `sqlTypeName`).
14+
*
15+
* Not part of the regular suite; enable ad-hoc if you need to refresh the mapping.
16+
*/
17+
@Ignore("Investigation probe — not a real test")
18+
class SqliteMetadataProbe {
19+
20+
@Test
21+
fun `dump getObject class vs metadata jdbcType for problematic types`() {
22+
val file = Files.createTempFile("dataframe_sqlite_probe2_", ".db").toFile()
23+
file.deleteOnExit()
24+
val url = "jdbc:sqlite:${file.absolutePath}"
25+
26+
// (declaredType, sql-literal, description)
27+
val cases = listOf(
28+
Triple("BOOLEAN", "1", "boolean-as-int"),
29+
Triple("BOOLEAN", "0", "boolean-as-int-false"),
30+
Triple("DATE", "'2020-01-15'", "date-as-iso-text"),
31+
Triple("DATE", "1579046400", "date-as-unix-int"),
32+
Triple("DATETIME", "'2020-01-15 10:30:00'", "datetime-as-iso-text"),
33+
Triple("TIMESTAMP", "'2020-01-15T10:30:00Z'", "timestamp-as-iso-text"),
34+
Triple("TIMESTAMP", "1579083000", "timestamp-as-unix-int"),
35+
Triple("NUMERIC", "1.5", "numeric-as-real"),
36+
Triple("NUMERIC", "42", "numeric-as-int"),
37+
Triple("DECIMAL(10,5)", "1.5", "decimal-as-real"),
38+
Triple("DATE", "2460146.5", "date-as-julian-real"),
39+
Triple("TIMESTAMP", "2460146.5", "timestamp-as-julian-real"),
40+
)
41+
42+
DriverManager.getConnection(url).use { conn ->
43+
cases.forEachIndexed { i, (decl, literal, _) ->
44+
conn.createStatement().execute("CREATE TABLE t$i (col $decl)")
45+
conn.createStatement().execute("INSERT INTO t$i (col) VALUES ($literal)")
46+
}
47+
for ((i, case) in cases.withIndex()) {
48+
val (decl, literal, desc) = case
49+
conn.prepareStatement("SELECT col FROM t$i").executeQuery().use { rs ->
50+
val md = rs.metaData
51+
val jdbc = md.getColumnType(1)
52+
val jdbcName = jdbcName(jdbc)
53+
val cls = md.getColumnClassName(1)
54+
rs.next()
55+
val got = rs.getObject(1)
56+
val gotClass = got?.javaClass?.name
57+
println(
58+
"%-25s declared=%-14s literal=%-24s meta.jdbc=%-9s meta.class=%-22s getObject.class=%s value=%s".format(
59+
desc, decl, literal, jdbcName, cls, gotClass, got,
60+
),
61+
)
62+
}
63+
}
64+
}
65+
file.delete()
66+
}
67+
68+
@Test
69+
fun `dump metadata for common declared types`() {
70+
val file = Files.createTempFile("dataframe_sqlite_probe_", ".db").toFile()
71+
file.deleteOnExit()
72+
val url = "jdbc:sqlite:${file.absolutePath}"
73+
74+
val declaredTypes = listOf(
75+
"INTEGER", "INT", "TINYINT", "SMALLINT", "MEDIUMINT", "BIGINT", "UNSIGNED BIG INT",
76+
"INT2", "INT8",
77+
"REAL", "DOUBLE", "DOUBLE PRECISION", "FLOAT",
78+
"NUMERIC", "DECIMAL(10,5)",
79+
"BOOLEAN",
80+
"DATE", "DATETIME", "TIMESTAMP",
81+
"TEXT", "VARCHAR(255)", "CHAR(10)", "CLOB", "NVARCHAR(10)", "NCHAR(10)",
82+
"BLOB",
83+
"", // no declared type
84+
"CUSTOM_TYPE", // unknown → NUMERIC affinity
85+
)
86+
87+
DriverManager.getConnection(url).use { conn ->
88+
declaredTypes.forEachIndexed { i, decl ->
89+
val col = "col$i ${if (decl.isEmpty()) "" else decl}".trim()
90+
conn.createStatement().execute("CREATE TABLE t$i ($col)")
91+
// Insert one representative value so the driver can infer the actual storage class.
92+
conn.createStatement().execute("INSERT INTO t$i (col$i) VALUES (${sampleValueFor(decl)})")
93+
}
94+
for ((i, decl) in declaredTypes.withIndex()) {
95+
conn.prepareStatement("SELECT col$i FROM t$i").executeQuery().use { rs ->
96+
val md = rs.metaData
97+
val jdbc = md.getColumnType(1)
98+
val jdbcName = jdbcName(jdbc)
99+
val typeName = md.getColumnTypeName(1)
100+
val cls = md.getColumnClassName(1)
101+
println(
102+
"declared=%-24s -> sqlTypeName=%-16s jdbcType=%-8s (%s) classNm=%s".format(
103+
"\"$decl\"", typeName, jdbc.toString(), jdbcName, cls,
104+
),
105+
)
106+
}
107+
}
108+
}
109+
file.delete()
110+
}
111+
112+
private fun jdbcName(t: Int): String =
113+
Types::class.java.fields.firstOrNull { it.getInt(null) == t }?.name ?: "?"
114+
115+
private fun sampleValueFor(decl: String): String {
116+
val u = decl.uppercase()
117+
return when {
118+
u.contains("BLOB") -> "x'00'"
119+
u.contains("BOOLEAN") -> "1"
120+
u.contains("DATE") || u.contains("TIMESTAMP") || u.contains("TIME") -> "'2020-01-01'"
121+
u.contains("CHAR") || u.contains("TEXT") || u.contains("CLOB") -> "'x'"
122+
u.contains("REAL") || u.contains("DOUB") || u.contains("FLOA") ||
123+
u.contains("NUMERIC") || u.contains("DECIMAL") -> "1.5"
124+
u.contains("INT") -> "1"
125+
u.isEmpty() -> "1"
126+
else -> "'x'"
127+
}
128+
}
129+
}

0 commit comments

Comments
 (0)