What happened?
adbc_driver_postgresql.dbapi.Cursor.adbc_ingest() silently stores incorrect
PostgreSQL NUMERIC values when both of the following conditions are true:
- The absolute integer part is a non-zero exact multiple of
10^9.
- The fractional part is non-zero.
A minimal affected case using NUMERIC(18,6) is:
Input: 5000000000.000001
Stored: 5000000100.000000
Expected: 5000000000.000001
A nearby control value round-trips correctly:
Input: 4999999999.000001
Stored: 4999999999.000001
The write and commit both complete successfully without any exception or
warning.
The stored result is verified with a separate psycopg2 connection using
SELECT val::text. Therefore, this is not merely an ADBC result-decoding
problem: the incorrect value is stored in PostgreSQL.
Stack Trace
No response
How can we reproduce the bug?
The Arrow decimal value should be stored exactly in the PostgreSQL NUMERIC
column.
If the value cannot be serialized correctly, the driver should reject the
write. It should not report success while storing a different value.
This is silent data corruption:
adbc_ingest() succeeds;
commit() succeeds;
- PostgreSQL accepts the write;
- no warning or exception is emitted;
- a subsequent independent query returns a different numeric value.
This can affect financial, accounting, scientific, measurement, and other
pipelines that depend on exact decimal storage.
repro_adbc_numeric_bug_ready.py
Minimal reproduction
Install the dependencies:
python -m pip install \
"adbc-driver-postgresql==1.11.0" \
"pyarrow==24.0.0" \
"psycopg2-binary>=2.9,<3"
Start PostgreSQL 16, then set the connection URI:
export POSTGRES_URI="postgresql://postgres:postgres@localhost:5432/postgres"
On PowerShell:
$env:POSTGRES_URI = "postgresql://postgres:postgres@localhost:5432/postgres"
Run the attached script:
python repro_adbc_numeric_bug_ready.py
The script deliberately uses a regular table instead of a PostgreSQL temporary
table. The ADBC connection and the independent psycopg2 verification
connection are separate database sessions, while temporary tables are
session-local.
It exits with:
0 if every value round-trips correctly;
1 if corruption is detected;
2 if the test cannot complete because of an environment or runtime error.
Actual output
5000000000.000001 -> 5000000100.000000 [CORRUPTED]
1000000000.000001 -> 1000000100.000000 [CORRUPTED]
5000000000.100000 -> 5010000000.000000 [CORRUPTED]
4999999999.000001 -> 4999999999.000001 [OK]
5000000001.000001 -> 5000000001.000001 [OK]
5000000000.0000000001 -> 5000000000.0000010000 [CORRUPTED]
No stack trace is produced because no exception is raised.
Additional observed cases
Unless otherwise noted, these use NUMERIC(18,6).
Affected values
| Input |
Stored value |
Observation |
5000000000.000001 |
5000000100.000000 |
the 1e-6 fraction is displaced into the integer part |
5000000000.000010 |
5000001000.000000 |
the 1e-5 fraction is displaced into the integer part |
5000000000.100000 |
5010000000.000000 |
the 1e-1 fraction is displaced into the integer part |
1000000000.000001 |
1000000100.000000 |
triggers at exactly 10^9 |
2000000000.000001 |
2000000100.000000 |
triggers at another multiple of 10^9 |
10000000000.000001 |
10000000100.000000 |
triggers at 10 × 10^9 |
5000000000.0000000001 using NUMERIC(28,10) |
5000000000.0000010000 |
the same pattern is present at a different scale |
Values that round-trip correctly
| Input |
Stored value |
Observation |
4999999999.000001 |
4999999999.000001 |
integer part is not a multiple of 10^9 |
5000000001.000001 |
5000000001.000001 |
integer part is not a multiple of 10^9 |
9999999999.999999 |
9999999999.999999 |
integer part is not a multiple of 10^9 |
1234567890.123456 |
1234567890.123456 |
integer part is not a multiple of 10^9 |
5000000000.000000 |
5000000000.000000 |
fractional part is zero |
0.000001 |
0.000001 |
integer part is zero |
The output pattern is deterministic and direction-preserving: the fractional
digits appear to move upward by one base-10000 digit group.
This appears related to #3485, which also reported altered PostgreSQL
NUMERIC values during ingestion.
However, this report has a distinct and repeatable trigger:
|
#3485 |
This report |
| Trigger |
decimal conversion/scale cases |
non-zero integer part is an exact multiple of 10^9 and the fraction is non-zero |
| Corruption shape |
previously reported decimal alteration |
fractional groups move upward into the integer part |
| Current observation |
reported as resolved |
still reproducible in 1.11.0 |
The two problems may share the same general serialization area while being
different failure modes.
Suspected implementation area — hypothesis only
PostgreSQL binary NUMERIC values use metadata plus an array of base-10000
digit groups.
The observed displacement is consistent with a digit-group alignment or
weight calculation problem in the PostgreSQL binary COPY serializer. The
fractional digits appear to be serialized one base-10000 group too high.
This is only a hypothesis based on the output pattern. The exact source-level
cause has not been confirmed.
Suggested regression tests
At minimum, a regression test should include this affected/control pair:
5000000000.000001 -> must remain 5000000000.000001
4999999999.000001 -> must remain 4999999999.000001
Additional useful coverage would include:
- positive and negative multiples of
10^9;
- values immediately below and above the boundary;
- zero and non-zero fractional parts;
- scales
1, 4, 5, 6, and 10;
decimal128;
decimal256, where supported;
- multiple PostgreSQL versions;
- Windows and Linux.
Environment/Setup
The issue was reproduced with Python packages installed using pip.
| Component |
Tested value |
adbc-driver-postgresql |
1.11.0 |
adbc-driver-manager |
1.11.0 |
pyarrow |
24.0.0 |
psycopg2-binary |
2.9.x |
| Python |
3.11 |
| PostgreSQL |
16 |
| Platforms |
Windows amd64; Linux aarch64 |
The following adbc-driver-postgresql versions were separately tested and all
showed the corruption:
| Version |
Result |
1.2.0 |
corrupted |
1.5.0 |
corrupted |
1.8.0 |
corrupted |
1.10.0 |
corrupted |
1.11.0 |
corrupted |
This only claims coverage for the versions listed above; intermediate releases
were not tested.
What happened?
adbc_driver_postgresql.dbapi.Cursor.adbc_ingest()silently stores incorrectPostgreSQL
NUMERICvalues when both of the following conditions are true:10^9.A minimal affected case using
NUMERIC(18,6)is:A nearby control value round-trips correctly:
The write and commit both complete successfully without any exception or
warning.
The stored result is verified with a separate psycopg2 connection using
SELECT val::text. Therefore, this is not merely an ADBC result-decodingproblem: the incorrect value is stored in PostgreSQL.
Stack Trace
No response
How can we reproduce the bug?
The Arrow decimal value should be stored exactly in the PostgreSQL
NUMERICcolumn.
If the value cannot be serialized correctly, the driver should reject the
write. It should not report success while storing a different value.
This is silent data corruption:
adbc_ingest()succeeds;commit()succeeds;This can affect financial, accounting, scientific, measurement, and other
pipelines that depend on exact decimal storage.
repro_adbc_numeric_bug_ready.py
Minimal reproduction
Install the dependencies:
Start PostgreSQL 16, then set the connection URI:
On PowerShell:
Run the attached script:
The script deliberately uses a regular table instead of a PostgreSQL temporary
table. The ADBC connection and the independent psycopg2 verification
connection are separate database sessions, while temporary tables are
session-local.
It exits with:
0if every value round-trips correctly;1if corruption is detected;2if the test cannot complete because of an environment or runtime error.Actual output
No stack trace is produced because no exception is raised.
Additional observed cases
Unless otherwise noted, these use
NUMERIC(18,6).Affected values
5000000000.0000015000000100.0000001e-6fraction is displaced into the integer part5000000000.0000105000001000.0000001e-5fraction is displaced into the integer part5000000000.1000005010000000.0000001e-1fraction is displaced into the integer part1000000000.0000011000000100.00000010^92000000000.0000012000000100.00000010^910000000000.00000110000000100.00000010 × 10^95000000000.0000000001usingNUMERIC(28,10)5000000000.0000010000Values that round-trip correctly
4999999999.0000014999999999.00000110^95000000001.0000015000000001.00000110^99999999999.9999999999999999.99999910^91234567890.1234561234567890.12345610^95000000000.0000005000000000.0000000.0000010.000001The output pattern is deterministic and direction-preserving: the fractional
digits appear to move upward by one base-10000 digit group.
Relationship to #3485 / #3787
This appears related to #3485, which also reported altered PostgreSQL
NUMERICvalues during ingestion.However, this report has a distinct and repeatable trigger:
10^9and the fraction is non-zero1.11.0The two problems may share the same general serialization area while being
different failure modes.
Suspected implementation area — hypothesis only
PostgreSQL binary
NUMERICvalues use metadata plus an array of base-10000digit groups.
The observed displacement is consistent with a digit-group alignment or
weightcalculation problem in the PostgreSQL binary COPY serializer. Thefractional digits appear to be serialized one base-10000 group too high.
This is only a hypothesis based on the output pattern. The exact source-level
cause has not been confirmed.
Suggested regression tests
At minimum, a regression test should include this affected/control pair:
Additional useful coverage would include:
10^9;1,4,5,6, and10;decimal128;decimal256, where supported;Environment/Setup
The issue was reproduced with Python packages installed using
pip.adbc-driver-postgresql1.11.0adbc-driver-manager1.11.0pyarrow24.0.0psycopg2-binary2.9.x3.1116amd64; Linuxaarch64The following
adbc-driver-postgresqlversions were separately tested and allshowed the corruption:
1.2.01.5.01.8.01.10.01.11.0This only claims coverage for the versions listed above; intermediate releases
were not tested.