Skip to content

Commit 55419f8

Browse files
committed
Merge branch 'feat/uri-inet-jvm' into feat/uri-inet-roundtrip
2 parents 49324c4 + dd29057 commit 55419f8

4 files changed

Lines changed: 105 additions & 0 deletions

File tree

server/java/integration-tests-kotlin/src/test/kotlin/com/metaobjects/integration/kotlin/QueryScenarioRunner.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import org.jetbrains.exposed.sql.statements.InsertStatement
3434
import org.jetbrains.exposed.sql.transactions.transaction
3535
import com.metaobjects.integration.kotlin.tables.AllTypesTable
3636
import java.math.BigDecimal
37+
import java.net.URI
3738
import java.sql.DriverManager
3839
import java.sql.Timestamp
3940
import java.time.Instant
@@ -444,6 +445,13 @@ object QueryScenarioRunner {
444445
// both receive the String and write a real Postgres JSONB value (a bare String bind
445446
// would be rejected by jsonb). Read-back (#98) parses it back per-column.
446447
type.contains("jsonb") -> if (raw is String) raw else JSON.writeValueAsString(raw)
448+
// `field.uri` → a `Column<URI>` over `text`: construct a java.net.URI from the authoring
449+
// string so Exposed's MetaUriColumnType binds it (the read-back URI.toString() is verbatim).
450+
type.contains("text") -> if (raw is URI) raw else URI.create(raw.toString())
451+
// `field.inet` → a `Column<String>` over the native `inet` type: bind the bare address
452+
// string; the driver coerces it to inet, and the read-back is the native compressed wire
453+
// string. (`else` already returns the String — listed explicitly for intent.)
454+
type == "inet" -> raw.toString()
447455
else -> raw
448456
}
449457
}

server/java/integration-tests-kotlin/src/test/kotlin/com/metaobjects/integration/kotlin/tables/AllTypesTable.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,14 @@ object AllTypesTable : Table("all_types") {
6464
val moneyVal = long("moneyVal")
6565
val enumVal = varchar("enumVal", 64)
6666
val uuidVal = uuid("uuidVal")
67+
// `field.uri` → plain `text` column carrying the verbatim URI string (Postgres has no uri
68+
// type). See [MetaUriColumnType] — round-trips the URI unchanged.
69+
val uriVal = uriColumn("uriVal")
70+
// `field.inet` (IPv4 + IPv6) → Postgres-native `inet` columns. [MetaInetStringColumnType]
71+
// reads the driver's native inet wire string (bare host, NO `::text` cast → no /32|/128 mask;
72+
// canonical COMPRESSED IPv6) and binds the bare address on write.
73+
val inetVal = inetColumn("inetVal")
74+
val inet6Val = inetColumn("inet6Val")
6775
// `@storage:jsonb` typed owned-object column. Raw-JSON-String identity encode/decode keeps
6876
// the JSON text; the runner serializes the authoring map on write and parses it back to a
6977
// Map on read so it re-serializes with sorted keys per the normalization contract. Nullable
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package com.metaobjects.integration.kotlin.tables
2+
3+
import org.jetbrains.exposed.sql.Column
4+
import org.jetbrains.exposed.sql.ColumnType
5+
import org.jetbrains.exposed.sql.Table
6+
import org.jetbrains.exposed.sql.statements.api.PreparedStatementApi
7+
import org.jetbrains.exposed.sql.statements.jdbc.JdbcPreparedStatementImpl
8+
import org.jetbrains.exposed.sql.vendors.currentDialect
9+
import java.net.URI
10+
import java.sql.ResultSet
11+
import java.sql.Types
12+
13+
/**
14+
* Hand-written reference Exposed column types for `field.uri` / `field.inet` (ADR-0036/0037
15+
* Wave 3) — the test-harness analogue of the per-package `MetaInetUriColumnType.kt` the
16+
* [com.metaobjects.generator.kotlin.KotlinExposedTableGenerator] emits.
17+
*
18+
* `field.uri` mirrors the generated `MetaUriColumnType` 1:1 (a `Column<java.net.URI>` over plain
19+
* `text`; persists/round-trips the verbatim URI string).
20+
*
21+
* `field.inet` DEVIATES from the generated `MetaInetColumnType` on the READ side ON PURPOSE.
22+
* The generator's column is a `Column<java.net.InetAddress>` whose `valueFromDB` resolves the
23+
* JDBC value to an `InetAddress` — and `InetAddress.getHostAddress()` returns the UNcompressed
24+
* IPv6 form (`2001:db8:0:0:0:8a2e:370:7334`). The persistence-conformance `op:roundtrip`
25+
* expectation is the canonical COMPRESSED form (`2001:db8::8a2e:370:7334`) — which is exactly
26+
* what Postgres' native `inet` wire text already is. So the reference column here is a
27+
* `Column<String>` over the native `inet` type that READS the driver's native inet string
28+
* directly via `ResultSet.getString` (compressed, bare host — NO `::text` cast, so no `/32`|`/128`
29+
* mask is appended) and WRITES the bare address string via the driver's `inet` coercion. This is
30+
* the "read the inet column as the driver's string wire value" rule from the corpus
31+
* (`roundtrip-all-types.yaml`): it keeps the read-back byte-identical to the cross-port wire form
32+
* without re-deriving the host literal from an `InetAddress`.
33+
*/
34+
35+
/** Custom column type for `field.uri`: a `Column<java.net.URI>` over plain `text`. */
36+
internal class MetaUriColumnType : ColumnType<URI>() {
37+
override fun sqlType(): String = currentDialect.dataTypeProvider.textType()
38+
override fun valueFromDB(value: Any): URI = when (value) {
39+
is URI -> value
40+
else -> URI.create(value.toString())
41+
}
42+
override fun notNullValueToDB(value: URI): Any = value.toString()
43+
override fun nonNullValueToString(value: URI): String = "'$value'"
44+
}
45+
46+
/** Column builder for `field.uri`: a `Column<java.net.URI>` over a `text` column. */
47+
internal fun Table.uriColumn(name: String): Column<URI> =
48+
registerColumn(name, MetaUriColumnType())
49+
50+
/**
51+
* Custom column type for `field.inet`: a `Column<String>` over the Postgres-native `inet` type
52+
* that round-trips the bare host-literal string. The READ asks the driver for the native inet
53+
* wire text (`ResultSet.getString`), which Postgres returns WITHOUT a CIDR mask (host address,
54+
* no `/32`|`/128`) and in CANONICAL COMPRESSED form for IPv6 — so the read-back matches the
55+
* corpus expectation without going through `InetAddress.getHostAddress()` (which would emit the
56+
* uncompressed IPv6). The WRITE binds the address string and lets the JDBC driver coerce it to
57+
* the native `inet` column.
58+
*/
59+
internal class MetaInetStringColumnType : ColumnType<String>() {
60+
override fun sqlType(): String = "inet"
61+
override fun valueFromDB(value: Any): String = value.toString()
62+
override fun notNullValueToDB(value: String): Any = value
63+
override fun nonNullValueToString(value: String): String = "'$value'"
64+
/** Read the native inet wire string (compressed IPv6, bare host — no `::text` cast). */
65+
override fun readObject(rs: ResultSet, index: Int): Any? = rs.getString(index)
66+
override fun setParameter(stmt: PreparedStatementApi, index: Int, value: Any?) {
67+
// Bind the bare address string via setObject(.., Types.OTHER) so the Postgres JDBC driver
68+
// coerces it to the native `inet` column. Exposed's plain `stmt[index] = value` binds the
69+
// String as VARCHAR (default stringtype), which Postgres rejects ("column is of type inet
70+
// but expression is of type character varying"). Reach the raw java.sql.PreparedStatement
71+
// and bind with Types.OTHER — the exact pattern OMDB's InetCodec uses on the JVM.
72+
val raw = (stmt as JdbcPreparedStatementImpl).statement
73+
if (value == null) raw.setNull(index, Types.OTHER)
74+
else raw.setObject(index, value.toString(), Types.OTHER)
75+
}
76+
}
77+
78+
/** Column builder for `field.inet`: a `Column<String>` (native inet wire string) over an `inet` column. */
79+
internal fun Table.inetColumn(name: String): Column<String> =
80+
registerColumn(name, MetaInetStringColumnType())

server/java/integration-tests/src/test/java/com/metaobjects/integration/RoundtripWriter.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@
77
import com.metaobjects.field.DoubleField;
88
import com.metaobjects.field.EnumField;
99
import com.metaobjects.field.FloatField;
10+
import com.metaobjects.field.InetField;
1011
import com.metaobjects.field.IntegerField;
1112
import com.metaobjects.field.LongField;
1213
import com.metaobjects.field.MetaField;
1314
import com.metaobjects.field.ObjectField;
1415
import com.metaobjects.field.StringField;
1516
import com.metaobjects.field.TimeField;
1617
import com.metaobjects.field.TimestampField;
18+
import com.metaobjects.field.UriField;
1719
import com.metaobjects.field.UuidField;
1820
import com.metaobjects.integration.Scenarios.QuerySpec;
1921
import com.metaobjects.manager.ObjectConnection;
@@ -152,6 +154,13 @@ private static void setNative(ValueObject vo, MetaField<?> mf, Object raw) {
152154

153155
if (mf instanceof StringField || mf instanceof EnumField) {
154156
vo.setString(name, raw.toString());
157+
} else if (mf instanceof UriField || mf instanceof InetField) {
158+
// field.uri / field.inet are String-backed scalars (DataTypes.STRING). Author the URI /
159+
// IP string verbatim; OMDB's StringCodec (uri → text column) / InetCodec (inet →
160+
// setObject(.., Types.OTHER)) handles the bind, and InetCodec's rs.getString reads the
161+
// inet column's NATIVE wire form on read-back (bare address, no ::text cast → no /32|/128
162+
// mask, and Postgres returns the compressed canonical IPv6).
163+
vo.setString(name, raw.toString());
155164
} else if (mf instanceof UuidField) {
156165
// Author the upper-case UUID string; the write codec binds a java.util.UUID and the
157166
// read codec returns it lowercase-canonical (the cross-port contract).

0 commit comments

Comments
 (0)