|
1 | 1 | /* Copyright (c) 2026 Airbyte, Inc., all rights reserved. */ |
2 | 2 | package io.airbyte.integrations.source.postgres.operations.types |
3 | 3 |
|
4 | | -import io.airbyte.cdk.data.DoubleCodec |
5 | | -import io.airbyte.cdk.data.FloatCodec |
| 4 | +import com.fasterxml.jackson.databind.JsonNode |
| 5 | +import io.airbyte.cdk.data.JsonCodec |
6 | 6 | import io.airbyte.cdk.data.LeafAirbyteSchemaType |
7 | 7 | import io.airbyte.cdk.jdbc.JdbcAccessor |
8 | 8 | import io.airbyte.cdk.jdbc.SymmetricJdbcFieldType |
| 9 | +import io.airbyte.cdk.util.Jsons |
9 | 10 | import java.sql.PreparedStatement |
10 | 11 | import java.sql.ResultSet |
11 | 12 |
|
12 | | -// Postgres real/double precision columns permit Infinity, -Infinity, and NaN. Downstream |
13 | | -// destinations expect JSON numbers, but Jackson serializes IEEE-754 non-finite values as |
14 | | -// the string tokens "Infinity"/"-Infinity"/"NaN" — which then fail to parse as numbers |
15 | | -// (NumberFormatException: Character I is neither a decimal digit...). Match the legacy |
16 | | -// connector's behavior and the existing numeric/decimal handling by emitting null for |
17 | | -// non-finite values. |
| 13 | +// Postgres real/double precision permit Infinity, -Infinity, and NaN. Downstream destinations |
| 14 | +// expect JSON numbers, but Jackson serializes IEEE-754 non-finite values as the string tokens |
| 15 | +// "Infinity"/"-Infinity"/"NaN" — which then fail to parse as numbers (NumberFormatException: |
| 16 | +// Character I is neither a decimal digit...). The accessor throws on non-finite during JDBC |
| 17 | +// reads (recording RETRIEVAL_FAILURE_TOTAL in record metadata), and the codec throws during |
| 18 | +// CDC decode (recording DESERIALIZATION_FAILURE_TOTAL). The value is then nulled in the |
| 19 | +// emitted record, but the change is noted — mirroring how PgTimestampFieldType handles |
| 20 | +// infinity dates. |
18 | 21 | object PostgresFiniteFloatFieldType : |
19 | 22 | SymmetricJdbcFieldType<Float>( |
20 | 23 | LeafAirbyteSchemaType.NUMBER, |
21 | 24 | PgFiniteFloatAccessor, |
22 | | - FloatCodec, |
| 25 | + FiniteFloatCodec, |
23 | 26 | ) |
24 | 27 |
|
25 | 28 | object PostgresFiniteDoubleFieldType : |
26 | 29 | SymmetricJdbcFieldType<Double>( |
27 | 30 | LeafAirbyteSchemaType.NUMBER, |
28 | 31 | PgFiniteDoubleAccessor, |
29 | | - DoubleCodec, |
| 32 | + FiniteDoubleCodec, |
30 | 33 | ) |
31 | 34 |
|
| 35 | +object FiniteFloatCodec : JsonCodec<Float> { |
| 36 | + override fun encode(decoded: Float): JsonNode = Jsons.numberNode(decoded) |
| 37 | + |
| 38 | + override fun decode(encoded: JsonNode): Float { |
| 39 | + if (!encoded.isNumber) { |
| 40 | + throw IllegalArgumentException("non-numeric value $encoded is unsupported") |
| 41 | + } |
| 42 | + val decoded: Float = encoded.floatValue() |
| 43 | + if (!decoded.isFinite()) { |
| 44 | + throw IllegalArgumentException("non-finite value $decoded is unsupported") |
| 45 | + } |
| 46 | + if (encode(decoded).doubleValue().compareTo(encoded.doubleValue()) != 0) { |
| 47 | + throw IllegalArgumentException( |
| 48 | + "invalid IEEE-754 32-bit floating point value $encoded (type ${encoded.javaClass.canonicalName})" |
| 49 | + ) |
| 50 | + } |
| 51 | + return decoded |
| 52 | + } |
| 53 | +} |
| 54 | + |
| 55 | +object FiniteDoubleCodec : JsonCodec<Double> { |
| 56 | + override fun encode(decoded: Double): JsonNode = Jsons.numberNode(decoded) |
| 57 | + |
| 58 | + override fun decode(encoded: JsonNode): Double { |
| 59 | + if (!encoded.isNumber) { |
| 60 | + throw IllegalArgumentException("non-numeric value $encoded is unsupported") |
| 61 | + } |
| 62 | + val decoded: Double = encoded.doubleValue() |
| 63 | + if (!decoded.isFinite()) { |
| 64 | + throw IllegalArgumentException("non-finite value $decoded is unsupported") |
| 65 | + } |
| 66 | + if (encode(decoded).decimalValue().compareTo(encoded.decimalValue()) != 0) { |
| 67 | + throw IllegalArgumentException("invalid IEEE-754 64-bit floating point value $encoded") |
| 68 | + } |
| 69 | + return decoded |
| 70 | + } |
| 71 | +} |
| 72 | + |
32 | 73 | private object PgFiniteFloatAccessor : JdbcAccessor<Float> { |
33 | | - override fun get(rs: ResultSet, colIdx: Int): Float? = |
34 | | - rs.getFloat(colIdx).takeUnless { rs.wasNull() }?.takeIf { it.isFinite() } |
| 74 | + override fun get(rs: ResultSet, colIdx: Int): Float? { |
| 75 | + val value = rs.getFloat(colIdx) |
| 76 | + if (rs.wasNull()) return null |
| 77 | + if (!value.isFinite()) { |
| 78 | + throw IllegalStateException("non-finite value $value is unsupported") |
| 79 | + } |
| 80 | + return value |
| 81 | + } |
35 | 82 |
|
36 | 83 | override fun set(stmt: PreparedStatement, paramIdx: Int, value: Float) { |
37 | 84 | stmt.setFloat(paramIdx, value) |
38 | 85 | } |
39 | 86 | } |
40 | 87 |
|
41 | 88 | private object PgFiniteDoubleAccessor : JdbcAccessor<Double> { |
42 | | - override fun get(rs: ResultSet, colIdx: Int): Double? = |
43 | | - rs.getDouble(colIdx).takeUnless { rs.wasNull() }?.takeIf { it.isFinite() } |
| 89 | + override fun get(rs: ResultSet, colIdx: Int): Double? { |
| 90 | + val value = rs.getDouble(colIdx) |
| 91 | + if (rs.wasNull()) return null |
| 92 | + if (!value.isFinite()) { |
| 93 | + throw IllegalStateException("non-finite value $value is unsupported") |
| 94 | + } |
| 95 | + return value |
| 96 | + } |
44 | 97 |
|
45 | 98 | override fun set(stmt: PreparedStatement, paramIdx: Int, value: Double) { |
46 | 99 | stmt.setDouble(paramIdx, value) |
|
0 commit comments