From d18c874ad092fe85f8fbf04e3f4026b2f08403e2 Mon Sep 17 00:00:00 2001 From: Ajay Kumar <206070437+ajaykr0905@users.noreply.github.com> Date: Fri, 23 Oct 2026 12:19:19 +0530 Subject: [PATCH] Report decimal precision and scale metadata Assisted-by: OpenAI Codex Signed-off-by: Ajay Kumar <206070437+ajaykr0905@users.noreply.github.com> --- internal/arrowscan/coltype.go | 13 ++++++++- internal/arrowscan/coltype_test.go | 14 +++++++++ internal/backend/kernel/rows.go | 11 +++++++ internal/rows/rows.go | 24 ++++++++++++++++ internal/rows/rows_test.go | 46 ++++++++++++++++++++++++++++++ 5 files changed, 107 insertions(+), 1 deletion(-) diff --git a/internal/arrowscan/coltype.go b/internal/arrowscan/coltype.go index d1e8a750..846fa2db 100644 --- a/internal/arrowscan/coltype.go +++ b/internal/arrowscan/coltype.go @@ -27,6 +27,10 @@ type ColumnTypeInfo struct { // (math.MaxInt64), matching Thrift; fixed-width types report (0, false). Length int64 HasLength bool + // Precision / Scale report a DECIMAL column's declared qualifiers. + Precision int64 + Scale int64 + HasPrecisionScale bool } // Scan types, matching the Thrift path's vars in internal/rows/rows.go so both @@ -85,7 +89,14 @@ func ColumnTypeInfoFor(dt arrow.DataType) ColumnTypeInfo { // Under WithKernelDecimalAsFloat the value is a float64 instead of a string; // the scan type stays RawBytes, a deliberate Thrift-matching exception to the // scan-type/value lockstep (Thrift does the same under UseArrowNativeDecimal). - return ColumnTypeInfo{DatabaseTypeName: "DECIMAL", ScanType: scanTypeRawBytes} + decimalType := dt.(*arrow.Decimal128Type) + return ColumnTypeInfo{ + DatabaseTypeName: "DECIMAL", + ScanType: scanTypeRawBytes, + Precision: int64(decimalType.Precision), + Scale: int64(decimalType.Scale), + HasPrecisionScale: true, + } case arrow.LIST, arrow.LARGE_LIST, arrow.FIXED_SIZE_LIST: return varLen("ARRAY", scanTypeRawBytes) case arrow.MAP: diff --git a/internal/arrowscan/coltype_test.go b/internal/arrowscan/coltype_test.go index dfb643be..a0af90d8 100644 --- a/internal/arrowscan/coltype_test.go +++ b/internal/arrowscan/coltype_test.go @@ -70,6 +70,20 @@ func TestColumnTypeInfoFor(t *testing.T) { } } +func TestColumnTypeInfoForDecimalSize(t *testing.T) { + decimal := ColumnTypeInfoFor(&arrow.Decimal128Type{Precision: 38, Scale: 18}) + if decimal.Precision != 38 || decimal.Scale != 18 || !decimal.HasPrecisionScale { + t.Errorf("decimal size = (%d,%d,%v), want (38,18,true)", + decimal.Precision, decimal.Scale, decimal.HasPrecisionScale) + } + + integer := ColumnTypeInfoFor(arrow.PrimitiveTypes.Int64) + if integer.Precision != 0 || integer.Scale != 0 || integer.HasPrecisionScale { + t.Errorf("integer decimal size = (%d,%d,%v), want (0,0,false)", + integer.Precision, integer.Scale, integer.HasPrecisionScale) + } +} + // TestColumnTypeInfoScanTypeCoversScanner is the lockstep guard: every Arrow type // ScanCellCached handles must have a non-fallback ColumnTypeInfoFor entry, so a new // scanner type without matching metadata is caught here, not at runtime. diff --git a/internal/backend/kernel/rows.go b/internal/backend/kernel/rows.go index d55f77ea..94250477 100644 --- a/internal/backend/kernel/rows.go +++ b/internal/backend/kernel/rows.go @@ -40,6 +40,7 @@ var ( _ driver.RowsColumnTypeDatabaseTypeName = (*kernelRows)(nil) _ driver.RowsColumnTypeNullable = (*kernelRows)(nil) _ driver.RowsColumnTypeLength = (*kernelRows)(nil) + _ driver.RowsColumnTypePrecisionScale = (*kernelRows)(nil) // Public batch API, matching the Thrift path (rows.rows). GetArrowBatches is // real; GetArrowIPCStreams rejects (kernel C ABI exports C Data, not IPC bytes). _ dbrows.Rows = (*kernelRows)(nil) @@ -174,6 +175,16 @@ func (r *kernelRows) ColumnTypeLength(index int) (length int64, ok bool) { return ct.Length, ct.HasLength } +// ColumnTypePrecisionScale returns the declared precision and scale for a +// DECIMAL column, matching the Thrift path. +func (r *kernelRows) ColumnTypePrecisionScale(index int) (precision, scale int64, ok bool) { + if index < 0 || index >= len(r.colTypes) { + return 0, 0, false + } + ct := r.colTypes[index] + return ct.Precision, ct.Scale, ct.HasPrecisionScale +} + // Close releases the current batch, the kernel result stream, and (query-path // ownership) the server operation. Idempotent. func (r *kernelRows) Close() error { diff --git a/internal/rows/rows.go b/internal/rows/rows.go index 1ba3bb7c..8780dbe1 100644 --- a/internal/rows/rows.go +++ b/internal/rows/rows.go @@ -29,6 +29,7 @@ import ( // RowsColumnTypeDatabaseTypeName // RowsColumnTypeNullable // RowsColumnTypeLength +// RowsColumnTypePrecisionScale type rows struct { // The RowScanner is responsible for handling the different // formats in which the query results can be returned @@ -92,6 +93,7 @@ var _ driver.RowsColumnTypeScanType = (*rows)(nil) var _ driver.RowsColumnTypeDatabaseTypeName = (*rows)(nil) var _ driver.RowsColumnTypeNullable = (*rows)(nil) var _ driver.RowsColumnTypeLength = (*rows)(nil) +var _ driver.RowsColumnTypePrecisionScale = (*rows)(nil) var _ dbsqlrows.Rows = (*rows)(nil) // TelemetryCallbacks bundles the optional telemetry hooks passed into NewRows. @@ -384,6 +386,28 @@ func (r *rows) ColumnTypeLength(index int) (length int64, ok bool) { return columnTypeLengthForID(rowscanner.GetDBTypeID(columnInfo)) } +// ColumnTypePrecisionScale returns the declared precision and scale for a +// DECIMAL column. Non-decimal columns and incomplete metadata return ok=false. +func (r *rows) ColumnTypePrecisionScale(index int) (precision, scale int64, ok bool) { + columnInfo, err := r.getColumnMetadataByIndex(index) + if err != nil || rowscanner.GetDBTypeID(columnInfo) != cli_service.TTypeId_DECIMAL_TYPE { + return 0, 0, false + } + + qualifiers := rowscanner.GetDBTypeQualifiers(columnInfo) + if qualifiers == nil { + return 0, 0, false + } + precisionQualifier := qualifiers.Qualifiers["precision"] + scaleQualifier := qualifiers.Qualifiers["scale"] + if precisionQualifier == nil || !precisionQualifier.IsSetI32Value() || + scaleQualifier == nil || !scaleQualifier.IsSetI32Value() { + return 0, 0, false + } + + return int64(precisionQualifier.GetI32Value()), int64(scaleQualifier.GetI32Value()), true +} + // columnTypeLengthForID is the variable-length classification behind // ColumnTypeLength, factored out of the method (which needs a live *rows and a // server round-trip to reach it) so the exact rule can be reused verbatim — in diff --git a/internal/rows/rows_test.go b/internal/rows/rows_test.go index e04f9ef5..b69805fd 100644 --- a/internal/rows/rows_test.go +++ b/internal/rows/rows_test.go @@ -667,6 +667,52 @@ func TestColumnTypeLength(t *testing.T) { } } +func TestColumnTypePrecisionScale(t *testing.T) { + precision := int32(38) + scale := int32(18) + missingScalePrecision := int32(10) + + column := func(typeID cli_service.TTypeId, qualifiers map[string]*cli_service.TTypeQualifierValue) *cli_service.TColumnDesc { + return &cli_service.TColumnDesc{ + TypeDesc: &cli_service.TTypeDesc{Types: []*cli_service.TTypeEntry{{ + PrimitiveEntry: &cli_service.TPrimitiveTypeEntry{ + Type: typeID, + TypeQualifiers: &cli_service.TTypeQualifiers{Qualifiers: qualifiers}, + }, + }}}, + } + } + + rowSet := &rows{ + client: &client.TestClient{}, + schema: &cli_service.TTableSchema{Columns: []*cli_service.TColumnDesc{ + column(cli_service.TTypeId_DECIMAL_TYPE, map[string]*cli_service.TTypeQualifierValue{ + "precision": {I32Value: &precision}, + "scale": {I32Value: &scale}, + }), + column(cli_service.TTypeId_INT_TYPE, map[string]*cli_service.TTypeQualifierValue{ + "precision": {I32Value: &precision}, + "scale": {I32Value: &scale}, + }), + column(cli_service.TTypeId_DECIMAL_TYPE, map[string]*cli_service.TTypeQualifierValue{ + "precision": {I32Value: &missingScalePrecision}, + }), + }}, + } + + gotPrecision, gotScale, ok := rowSet.ColumnTypePrecisionScale(0) + assert.Equal(t, int64(38), gotPrecision) + assert.Equal(t, int64(18), gotScale) + assert.True(t, ok) + + for _, index := range []int{-1, 1, 2, 3} { + gotPrecision, gotScale, ok = rowSet.ColumnTypePrecisionScale(index) + assert.Zero(t, gotPrecision) + assert.Zero(t, gotScale) + assert.False(t, ok) + } +} + func TestColumnTypeDatabaseTypeName(t *testing.T) { var getMetadataCount, fetchResultsCount int