@@ -77,14 +77,32 @@ fun main(args: Array<String>) {
7777 var errorCount = 0
7878
7979 // ── Wallets ────────────────────────────────────────────────────────────────
80-
81- val walletIds = sourceConn.query(" SELECT id FROM wallets" ) { rs ->
82- rs.getString(" id" )
80+ //
81+ // IMPORTANT: waltid-wallet-api stores wallet and account UUIDs as 16-byte binary
82+ // blobs (not text strings). rs.getString() on a blob returns an unreliable binary
83+ // string that does NOT match when used as a setString() parameter in child queries.
84+ // We must read blobs with getBytes() and convert to proper UUID strings.
85+ //
86+ // Child queries (keys / credentials / DIDs) use a queryWithBytes() helper that
87+ // passes the raw 16-byte blob as setBytes() — SQLite matches blobs by bytes, not
88+ // by text representation.
89+
90+ val walletEntries = sourceConn.query(" SELECT id FROM wallets" ) { rs ->
91+ val bytes = rs.getBytes(" id" )
92+ if (bytes != null && bytes.size == 16 ) {
93+ // waltid-wallet-api stores UUIDs as 16-byte binary blobs (big-endian)
94+ val uuid = bytesToUuid(bytes)
95+ uuid to WalletIdParam .Blob (bytes)
96+ } else {
97+ // PostgreSQL / text-based SQLite — ID is already a string
98+ val text = rs.getString(" id" ) ? : " "
99+ text to WalletIdParam .Text (text)
100+ }
83101 }
84102
85- log.info { " Found ${walletIds .size} wallets to migrate" }
103+ log.info { " Found ${walletEntries .size} wallets to migrate" }
86104
87- for (walletUuid in walletIds ) {
105+ for (( walletUuid, walletParam) in walletEntries ) {
88106 val keyStoreId = " wallet-$walletUuid -keys"
89107 val credStoreId = " wallet-$walletUuid -creds"
90108 val didStoreId = " wallet-$walletUuid -dids"
@@ -115,8 +133,8 @@ fun main(args: Array<String>) {
115133
116134 // ── Keys ──────────────────────────────────────────────────────────────
117135
118- val keys = sourceConn.queryWith (
119- " SELECT kid, document FROM wallet_keys WHERE wallet = ?" , walletUuid
136+ val keys = sourceConn.queryWithWalletParam (
137+ " SELECT kid, document FROM wallet_keys WHERE wallet = ?" , walletParam
120138 ) { rs -> rs.getString(" kid" ) to rs.getString(" document" ) }
121139
122140 for ((keyId, serializedKey) in keys) {
@@ -142,8 +160,8 @@ fun main(args: Array<String>) {
142160 // ── Credentials ───────────────────────────────────────────────────────
143161
144162 data class OldCred (val id : String , val document : String , val disclosures : String? )
145- val credentials = sourceConn.queryWith (
146- " SELECT id, document, disclosures FROM credentials WHERE wallet = ?" , walletUuid
163+ val credentials = sourceConn.queryWithWalletParam (
164+ " SELECT id, document, disclosures FROM credentials WHERE wallet = ?" , walletParam
147165 ) { rs -> OldCred (rs.getString(" id" ), rs.getString(" document" ), rs.getString(" disclosures" )) }
148166
149167 for (cred in credentials) {
@@ -171,19 +189,32 @@ fun main(args: Array<String>) {
171189
172190 // ── DIDs ──────────────────────────────────────────────────────────────
173191
174- val dids = sourceConn.queryWith (
175- " SELECT did, document FROM wallet_dids WHERE wallet = ?" , walletUuid
192+ val dids = sourceConn.queryWithWalletParam (
193+ " SELECT did, document FROM wallet_dids WHERE wallet = ?" , walletParam
176194 ) { rs -> rs.getString(" did" ) to rs.getString(" document" ) }
177195
178196 for ((did, documentStr) in dids) {
179197 runCatching {
180- Json .parseToJsonElement(documentStr).jsonObject // validate JSON
198+ // The document column normally holds the full DID document JSON.
199+ // However some deployments (e.g. NDID) store only the DID URI string
200+ // for DID types where only the identifier matters, not the document.
201+ // In that case we synthesise a minimal {"id":"<did>"} document so the
202+ // DID is preserved in v2 rather than silently dropped.
203+ val documentJson = runCatching {
204+ Json .parseToJsonElement(documentStr).jsonObject
205+ }.getOrElse {
206+ log.warn {
207+ " DID $did has non-JSON document ('$documentStr ') — " +
208+ " storing minimal {\" id\" :\" $did \" } placeholder document."
209+ }
210+ Json .parseToJsonElement(""" {"id":"$did "}""" ).jsonObject
211+ }
181212 if (! dryRun) {
182213 suspendTransaction(targetDb) {
183214 Wallet2Tables .Dids .upsert {
184215 it[Wallet2Tables .Dids .storeId] = didStoreId
185216 it[Wallet2Tables .Dids .did] = did
186- it[Wallet2Tables .Dids .document] = documentStr
217+ it[Wallet2Tables .Dids .document] = documentJson.toString()
187218 }
188219 }
189220 }
@@ -199,9 +230,10 @@ fun main(args: Array<String>) {
199230
200231 // Old: account_wallet_mapping(tenant, id UUID, wallet UUID)
201232 // New: wallet2_account_wallets(account_id TEXT, wallet_id TEXT)
233+ // IDs may be 16-byte binary blobs (waltid-wallet-api SQLite) or plain text (PostgreSQL).
202234 val accountMappings = sourceConn.query(
203- " SELECT CAST(id AS TEXT) AS account_id, CAST( wallet AS TEXT) AS wallet_id FROM account_wallet_mapping"
204- ) { rs -> rs.getString( " account_id " ) to rs.getString( " wallet_id " ) }
235+ " SELECT id, wallet FROM account_wallet_mapping"
236+ ) { rs -> readWalletId(rs, " id " ) to readWalletId(rs, " wallet " ) }
205237
206238 for ((accountId, walletId) in accountMappings) {
207239 runCatching {
@@ -235,7 +267,27 @@ fun main(args: Array<String>) {
235267 }
236268}
237269
238- // ── JDBC helpers ───────────────────────────────────────────────────────────────
270+ /* *
271+ * Represents a wallet/account UUID parameter for use in JDBC child queries.
272+ *
273+ * waltid-wallet-api (SQLite) stores UUIDs as 16-byte binary blobs → [Blob].
274+ * PostgreSQL and non-blob SQLite store them as text strings → [Text].
275+ */
276+ private sealed class WalletIdParam {
277+ data class Blob (val bytes : ByteArray ) : WalletIdParam()
278+ data class Text (val value : String ) : WalletIdParam()
279+ }
280+
281+ /* *
282+ * Read a UUID column from [rs] by name, detecting whether it is stored as a 16-byte
283+ * binary blob (waltid-wallet-api SQLite) or as a plain text/UUID string (PostgreSQL).
284+ * Returns the UUID as a hyphenated string in both cases.
285+ */
286+ private fun readWalletId (rs : ResultSet , columnName : String ): String {
287+ val bytes = rs.getBytes(columnName)
288+ return if (bytes != null && bytes.size == 16 ) bytesToUuid(bytes)
289+ else rs.getString(columnName) ? : " "
290+ }
239291
240292/* * Execute a SELECT with no parameters and map each row. */
241293private fun <T > java.sql.Connection.query (sql : String , mapper : (ResultSet ) -> T ): List <T > =
@@ -247,28 +299,54 @@ private fun <T> java.sql.Connection.query(sql: String, mapper: (ResultSet) -> T)
247299 }
248300 }
249301
250- /* * Execute a SELECT with one String parameter (wallet UUID as TEXT) and map each row. */
251- private fun <T > java.sql.Connection.queryWith (sql : String , param : String , mapper : (ResultSet ) -> T ): List <T > =
302+ /* *
303+ * Execute a SELECT with one wallet-UUID parameter, handling both blob (waltid-wallet-api
304+ * SQLite) and text (PostgreSQL / plain SQLite) storage formats automatically.
305+ */
306+ private fun <T > java.sql.Connection.queryWithWalletParam (
307+ sql : String ,
308+ param : WalletIdParam ,
309+ mapper : (ResultSet ) -> T ,
310+ ): List <T > =
252311 prepareStatement(sql).use { stmt ->
253- stmt.setString(1 , param)
312+ when (param) {
313+ is WalletIdParam .Blob -> stmt.setBytes(1 , param.bytes)
314+ is WalletIdParam .Text -> stmt.setString(1 , param.value)
315+ }
254316 stmt.executeQuery().use { rs ->
255317 val out = mutableListOf<T >()
256318 while (rs.next()) out + = mapper(rs)
257319 out
258320 }
259321 }
260322
323+ /* *
324+ * Convert a 16-byte binary UUID blob to a standard hyphenated UUID string.
325+ * The old wallet stores UUIDs as 16-byte binary (most-significant bytes first).
326+ */
327+ private fun bytesToUuid (bytes : ByteArray? ): String {
328+ if (bytes == null || bytes.size != 16 ) return bytes?.toString(Charsets .ISO_8859_1 ) ? : " "
329+ val bb = java.nio.ByteBuffer .wrap(bytes)
330+ return java.util.UUID (bb.long, bb.long).toString()
331+ }
332+
261333/* * Infer the wallet2 key type string from a KeySerialization JSON document. */
262- private fun inferKeyType (serializedKey : String ): String = runCatching {
334+ internal fun inferKeyType (serializedKey : String ): String = runCatching {
263335 val json = Json .parseToJsonElement(serializedKey).jsonObject
264- val crv = json[" jwk" ]?.jsonObject?.get(" crv" )?.toString()?.trim(' "' )
336+ val jwk = json[" jwk" ]?.jsonObject
337+ val crv = jwk?.get(" crv" )?.toString()?.trim(' "' )
338+ val kty = jwk?.get(" kty" )?.toString()?.trim(' "' )
265339 when (crv) {
266340 " P-256" -> " secp256r1"
267341 " P-384" -> " secp384r1"
268342 " P-521" -> " secp521r1"
269343 " Ed25519" -> " Ed25519"
270344 " secp256k1" -> " secp256k1"
271- else -> json[" type" ]?.toString()?.trim(' "' ) ? : " unknown"
345+ else -> when (kty) {
346+ " RSA" -> " RSA"
347+ " OKP" -> " Ed25519" // Ed25519/X25519 without explicit crv — treat as Ed25519
348+ else -> kty ? : " unknown"
349+ }
272350 }
273351}.getOrDefault(" unknown" )
274352
0 commit comments