-
Notifications
You must be signed in to change notification settings - Fork 47
Add DECIMAL Type Support to C# Language Extension #83
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
0dbaa30
0c3296f
fa9a35f
7c524be
a093694
154f3ec
96163ff
7f3211e
93e85e1
732adb3
b421eac
8158979
1a6404b
0dd9ac0
95ba1d1
56e23fa
79610ed
8dc09dd
77f8c5a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,12 +9,14 @@ | |
| // | ||
| //********************************************************************* | ||
| using System; | ||
| using System.Data.SqlTypes; | ||
| using System.Linq; | ||
| using System.Text; | ||
| using System.Runtime.InteropServices; | ||
| using System.Collections.Generic; | ||
| using Microsoft.Data.Analysis; | ||
| using static Microsoft.SqlServer.CSharpExtension.Sql; | ||
| using static Microsoft.SqlServer.CSharpExtension.SqlNumericHelper; | ||
|
|
||
| namespace Microsoft.SqlServer.CSharpExtension | ||
| { | ||
|
|
@@ -174,6 +176,9 @@ DataFrameColumn column | |
| case SqlDataType.DotNetDouble: | ||
| SetDataPtrs<double>(columnNumber, GetArray<double>(column)); | ||
| break; | ||
| case SqlDataType.DotNetNumeric: | ||
| ExtractNumericColumn(columnNumber, column); | ||
| break; | ||
| case SqlDataType.DotNetChar: | ||
| // Calculate column size from actual data. | ||
| // columnSize = max UTF-8 byte length across all rows. | ||
|
|
@@ -203,7 +208,7 @@ DataFrameColumn column | |
| /// <summary> | ||
| /// This method sets data pointer for the column and append the array to the handle list. | ||
| /// </summary> | ||
| private unsafe void SetDataPtrs<T>( | ||
| private void SetDataPtrs<T>( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The change from
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it's intentional; I've been trying to get rid of unsafe as much as possible. I may revert it though to keep the change focused. |
||
| ushort columnNumber, | ||
| T[] array | ||
| ) where T : unmanaged | ||
|
|
@@ -213,6 +218,68 @@ T[] array | |
| _handleList.Add(handle); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Extracts NUMERIC/DECIMAL column data by converting SqlDecimal values to ODBC-compatible SQL_NUMERIC_STRUCT array. | ||
| /// </summary> | ||
| /// <param name="columnNumber">The column index.</param> | ||
| /// <param name="column">The DataFrameColumn containing SqlDecimal values.</param> | ||
| private unsafe void ExtractNumericColumn( | ||
| ushort columnNumber, | ||
| DataFrameColumn column) | ||
| { | ||
| if (column == null) | ||
| { | ||
| SetDataPtrs<SqlNumericStruct>(columnNumber, Array.Empty<SqlNumericStruct>()); | ||
| return; | ||
| } | ||
|
|
||
| // Determine target precision/scale from max values across all rows | ||
| // | ||
| byte precision = SqlNumericHelper.SQL_MIN_PRECISION; | ||
| byte scale = (byte)_columns[columnNumber].DecimalDigits; | ||
|
|
||
| for (int rowNumber = 0; rowNumber < column.Length; ++rowNumber) | ||
| { | ||
| if (column[rowNumber] != null) | ||
| { | ||
| SqlDecimal value = (SqlDecimal)column[rowNumber]; | ||
| if (!value.IsNull) | ||
| { | ||
| scale = Math.Max(scale, value.Scale); | ||
| precision = Math.Max(precision, value.Precision); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Enforce T-SQL DECIMAL(p,s) constraints: 1 <= p <= 38, 0 <= s <= p | ||
| // | ||
| precision = Math.Max(precision, SqlNumericHelper.SQL_MIN_PRECISION); | ||
| precision = Math.Min(precision, SqlNumericHelper.SQL_MAX_PRECISION); | ||
| if (scale > precision) | ||
| { | ||
| precision = scale; | ||
| } | ||
|
Comment on lines
+236
to
+261
|
||
|
|
||
| // Update metadata: Size = precision (total digits), DecimalDigits = scale (fractional digits) | ||
| // | ||
| _columns[columnNumber].Size = precision; | ||
| _columns[columnNumber].DecimalDigits = scale; | ||
|
|
||
| Logging.Trace($"ExtractNumericColumn: Column {columnNumber}, T-SQL type=DECIMAL({precision},{scale}), RowCount={column.Length}"); | ||
|
|
||
| // Convert all values (including NULLs) to SQL_NUMERIC_STRUCT using FromSqlDecimal | ||
| // | ||
| SqlNumericStruct[] numericArray = new SqlNumericStruct[column.Length]; | ||
| for (int rowNumber = 0; rowNumber < column.Length; ++rowNumber) | ||
| { | ||
| SqlDecimal value = (column[rowNumber] != null) ? (SqlDecimal)column[rowNumber] : SqlDecimal.Null; | ||
| numericArray[rowNumber] = FromSqlDecimal(value, precision, scale); | ||
| Logging.Trace($"ExtractNumericColumn: Row {rowNumber}, Value={value}"); | ||
| } | ||
|
|
||
| SetDataPtrs<SqlNumericStruct>(columnNumber, numericArray); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// This method gets the array from a DataFrameColumn Column for numeric types. | ||
| /// </summary> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,11 +9,13 @@ | |
| // | ||
| //************************************************************************************************* | ||
| using System; | ||
| using System.Data.SqlTypes; | ||
| using System.Runtime; | ||
| using System.Text; | ||
| using System.Collections.Generic; | ||
| using System.Runtime.InteropServices; | ||
| using static Microsoft.SqlServer.CSharpExtension.Sql; | ||
| using static Microsoft.SqlServer.CSharpExtension.SqlNumericHelper; | ||
|
|
||
| namespace Microsoft.SqlServer.CSharpExtension | ||
| { | ||
|
|
@@ -132,6 +134,11 @@ public unsafe void AddParam( | |
| case SqlDataType.DotNetBit: | ||
| _params[paramNumber].Value = *(bool*)paramValue; | ||
| break; | ||
| case SqlDataType.DotNetNumeric: | ||
| // Convert SQL_NUMERIC_STRUCT to SqlDecimal, handling OUTPUT parameter sentinel (precision=0) | ||
| // | ||
| _params[paramNumber].Value = ToSqlDecimalFromPointer((SqlNumericStruct*)paramValue); | ||
| break; | ||
| case SqlDataType.DotNetChar: | ||
| _params[paramNumber].Value = Interop.UTF8PtrToStr((char*)paramValue, (ulong)strLenOrNullMap); | ||
| break; | ||
|
|
@@ -168,7 +175,19 @@ public unsafe void ReplaceParam( | |
|
|
||
| _params[paramNumber].Value = paramValue_; | ||
| CSharpParam param = _params[paramNumber]; | ||
| if(param.Value == null) | ||
|
|
||
| // Use null-coalescing pattern for safer null checking with value types | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The comment says "Use null-coalescing pattern for safer null checking with value types" but this isn't a null-coalescing pattern — it's a split null check. The split is needed because Please update the comment to explain this specific reason rather than using inaccurate terminology. Something like:
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The comment says "Use null-coalescing pattern for safer null checking with value types" but this isn't a null-coalescing pattern — it's a split null check. The split is needed because Please update the comment to explain this specific reason rather than using inaccurate terminology. Something like: |
||
| // SqlDecimal is a struct, so we need to check both object null and SqlDecimal.IsNull | ||
| // | ||
| if(ReferenceEquals(param.Value, null)) | ||
| { | ||
| *strLenOrNullMap = SQL_NULL_DATA; | ||
| return; | ||
| } | ||
|
|
||
| // Special handling for SqlDecimal.Null (SqlDecimal is a struct, not a class) | ||
| // | ||
| if(param.DataType == SqlDataType.DotNetNumeric && param.Value is SqlDecimal sqlDecVal && sqlDecVal.IsNull) | ||
| { | ||
| *strLenOrNullMap = SQL_NULL_DATA; | ||
| return; | ||
|
|
@@ -214,6 +233,21 @@ public unsafe void ReplaceParam( | |
| bool boolValue = Convert.ToBoolean(param.Value); | ||
| ReplaceNumericParam<bool>(boolValue, paramValue); | ||
| break; | ||
| case SqlDataType.DotNetNumeric: | ||
| // Use declared precision/scale from T-SQL parameter definition, not SqlDecimal's intrinsic values | ||
| // | ||
| if (param.Value is SqlDecimal sqlDecimalValue) | ||
| { | ||
| byte precision = (byte)param.Size; | ||
| byte scale = (byte)param.DecimalDigits; | ||
| *strLenOrNullMap = Sql.SqlNumericStructSize; | ||
| ReplaceNumericStructParam(sqlDecimalValue, precision, scale, paramValue); | ||
| } | ||
| else | ||
| { | ||
| throw new InvalidCastException($"Expected SqlDecimal for NUMERIC parameter, got {param.Value?.GetType().Name ?? "null"}"); | ||
| } | ||
| break; | ||
| case SqlDataType.DotNetChar: | ||
| // For CHAR/VARCHAR, strLenOrNullMap is in bytes (1 byte per character for ANSI). | ||
| // param.Size is the declared parameter size in characters (from SQL Server's CHAR(n)/VARCHAR(n)). | ||
|
|
@@ -229,7 +263,6 @@ public unsafe void ReplaceParam( | |
| // In C#, sizeof(char) is always 2 bytes (UTF-16), regardless of platform. | ||
| // Note: C++ wchar_t is 2 bytes on Windows but 4 bytes on Linux - this extension only supports Windows. | ||
| // param.Size is the declared parameter size in characters (from SQL Server's NCHAR(n)/NVARCHAR(n)), | ||
| // so we multiply by sizeof(char) to convert to bytes. | ||
| // | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The line
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The line |
||
| int wcharByteLen = param.Value.Length * sizeof(char); | ||
| int wcharMaxByteLen = (int)param.Size * sizeof(char); | ||
|
|
@@ -275,6 +308,26 @@ private unsafe void ReplaceNumericParam<T>( | |
| *paramValue = (void*)handle.AddrOfPinnedObject(); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Replaces NUMERIC/DECIMAL parameter value by converting SqlDecimal to SQL_NUMERIC_STRUCT and pinning it. | ||
| /// </summary> | ||
| private unsafe void ReplaceNumericStructParam( | ||
| SqlDecimal value, | ||
| byte precision, | ||
| byte scale, | ||
| void **paramValue) | ||
| { | ||
| SqlNumericStruct numericStruct = FromSqlDecimal(value, precision, scale); | ||
|
|
||
| // Box into array for heap allocation (stack-allocated struct destroyed at method exit) | ||
| // Pin to prevent GC from moving memory while native code holds the pointer | ||
| // | ||
| SqlNumericStruct[] valueArray = new SqlNumericStruct[1] { numericStruct }; | ||
| GCHandle handle = GCHandle.Alloc(valueArray, GCHandleType.Pinned); | ||
| _handleList.Add(handle); | ||
| *paramValue = (void*)handle.AddrOfPinnedObject(); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// This method replaces parameter value for string data types. | ||
| /// If the string is not empty, the address of underlying bytes will be assigned to paramValue. | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The change from
private unsafe void SetDataPtrs<T>toprivate void SetDataPtrs<T>is technically correct (the method body usesGCHandlewhich doesn't needunsafe), but it's unrelated to the DECIMAL feature. If it's intentional cleanup, call it out in the PR description. Otherwise, revert to keep the diff focused.