@@ -15,7 +15,7 @@ import {
1515 RecordBatchReader ,
1616 util as arrowUtils ,
1717} from 'apache-arrow' ;
18- import { TTableSchema , TColumnDesc } from '../../thrift/TCLIService_types' ;
18+ import { TTableSchema , TColumnDesc , TTypeId } from '../../thrift/TCLIService_types' ;
1919import IClientContext from '../contracts/IClientContext' ;
2020import HiveDriverError from '../errors/HiveDriverError' ;
2121import IResultsProvider , { ResultsProviderFetchNextOptions } from './IResultsProvider' ;
@@ -169,13 +169,41 @@ function formatDayTimeFromTotal(totalNanos: bigint): string {
169169 return `${ sign } ${ days . toString ( ) } ${ pad2 ( hours ) } :${ pad2 ( minutes ) } :${ pad2 ( seconds ) } ${ fraction } ` ;
170170}
171171
172+ /**
173+ * Render an Arrow `Decimal` value — supplied as its unscaled integer (from
174+ * `bigNumToBigInt`) plus the column `scale` — as an exact decimal string,
175+ * e.g. unscaled `1234567890` / scale `5` → `"12345.67890"`. Used by the
176+ * precision-preserving path so high-precision DECIMALs survive the round-trip
177+ * instead of being flattened to an IEEE-754 double.
178+ */
179+ export function bigNumDecimalToString ( unscaled : bigint , scale : number ) : string {
180+ if ( scale <= 0 ) {
181+ return unscaled . toString ( ) ;
182+ }
183+ const negative = unscaled < ZERO_BIGINT ;
184+ // `padStart(scale + 1)` guarantees at least one digit before the point
185+ // (e.g. unscaled `5` / scale `2` → `"005"` → `"0.05"`).
186+ const digits = ( negative ? - unscaled : unscaled ) . toString ( ) . padStart ( scale + 1 , '0' ) ;
187+ const cut = digits . length - scale ;
188+ return `${ negative ? '-' : '' } ${ digits . slice ( 0 , cut ) } .${ digits . slice ( cut ) } ` ;
189+ }
190+
172191export default class ArrowResultConverter implements IResultsProvider < Array < any > > {
173192 private readonly context : IClientContext ;
174193
175194 private readonly source : IResultsProvider < ArrowBatch > ;
176195
177196 private readonly schema : Array < TColumnDesc > ;
178197
198+ // When true, DECIMAL and 64-bit integer values keep full precision —
199+ // DECIMAL as an exact string and BIGINT as a JS `bigint` — instead of being
200+ // coerced to a lossy `number`. Enabled by the SEA backend, which always
201+ // receives native Arrow `Decimal128` / `Int64` from the kernel and has no
202+ // server-side "send as string" escape hatch (the Thrift backend gets the
203+ // string form via `useArrowNativeTypes=false`). Off by default so the Thrift
204+ // path keeps its long-standing `number` representation unchanged.
205+ private readonly preserveBigNumericPrecision : boolean ;
206+
179207 private recordBatchReader ?: IterableIterator < RecordBatch < TypeMap > > ;
180208
181209 // Remaining rows in current Arrow batch (not the record batch!)
@@ -193,10 +221,16 @@ export default class ArrowResultConverter implements IResultsProvider<Array<any>
193221 // operation backend and the SEA backend's neutral `ResultMetadata` —
194222 // which both carry `schema?: TTableSchema` — can construct the converter
195223 // without an adapter at the call site.
196- constructor ( context : IClientContext , source : IResultsProvider < ArrowBatch > , { schema } : { schema ?: TTableSchema } ) {
224+ constructor (
225+ context : IClientContext ,
226+ source : IResultsProvider < ArrowBatch > ,
227+ { schema } : { schema ?: TTableSchema } ,
228+ { preserveBigNumericPrecision = false } : { preserveBigNumericPrecision ?: boolean } = { } ,
229+ ) {
197230 this . context = context ;
198231 this . source = source ;
199232 this . schema = getSchemaColumns ( schema ) ;
233+ this . preserveBigNumericPrecision = preserveBigNumericPrecision ;
200234 }
201235
202236 public async hasMore ( ) {
@@ -374,6 +408,11 @@ export default class ArrowResultConverter implements IResultsProvider<Array<any>
374408 if ( value instanceof Object && value [ isArrowBigNumSymbol ] ) {
375409 const result = bigNumToBigInt ( value ) ;
376410 if ( DataType . isDecimal ( valueType ) ) {
411+ // Preserve full precision as an exact string when requested (SEA);
412+ // otherwise keep the historical lossy `number` form.
413+ if ( this . preserveBigNumericPrecision ) {
414+ return bigNumDecimalToString ( result , valueType . scale ) ;
415+ }
377416 return Number ( result ) / 10 ** valueType . scale ;
378417 }
379418 // A rewritten Duration Int64 surfaces as a raw `bigint`, not a BigNum
@@ -397,6 +436,12 @@ export default class ArrowResultConverter implements IResultsProvider<Array<any>
397436 if ( durationUnit ) {
398437 return formatDurationToIntervalDayTime ( value , durationUnit ) ;
399438 }
439+ // Keep the exact `bigint` when precision must be preserved (SEA); the
440+ // default path narrows to `number` for backward compatibility (the
441+ // Thrift backend has always returned BIGINT as a JS `number`).
442+ if ( this . preserveBigNumericPrecision ) {
443+ return value ;
444+ }
400445 return Number ( value ) ;
401446 }
402447
@@ -411,7 +456,23 @@ export default class ArrowResultConverter implements IResultsProvider<Array<any>
411456 const typeDescriptor = column . typeDesc . types [ 0 ] ?. primitiveEntry ;
412457 const field = column . columnName ;
413458 const value = record [ field ] ;
414- result [ field ] = value === null ? null : convertThriftValue ( typeDescriptor , value ) ;
459+ if ( value === null ) {
460+ result [ field ] = null ;
461+ return ;
462+ }
463+ // When preserving precision, DECIMAL and BIGINT values were already
464+ // produced in their exact form by `convertArrowTypes` (string / bigint).
465+ // `convertThriftValue` would narrow both back to a lossy `number`
466+ // (DECIMAL_TYPE → `Number(value)`, BIGINT_TYPE → `convertBigInt`), so
467+ // pass them through untouched on this path.
468+ if (
469+ this . preserveBigNumericPrecision &&
470+ ( typeDescriptor ?. type === TTypeId . DECIMAL_TYPE || typeDescriptor ?. type === TTypeId . BIGINT_TYPE )
471+ ) {
472+ result [ field ] = value ;
473+ return ;
474+ }
475+ result [ field ] = convertThriftValue ( typeDescriptor , value ) ;
415476 } ) ;
416477
417478 return result ;
0 commit comments