|
| 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