Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions language-extensions/dotnet-core-CSharp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,59 @@ Not Supported.
After downloading or building the dotnet-core-CSharp-lang-extension.zip, use [CREATE EXTERNAL LANGUAGE](https://docs.microsoft.com/en-us/sql/t-sql/statements/create-external-language-transact-sql?view=sql-server-ver15) to register the language with SQL Server 2019 CU3+.

This [tutorial](./sample/regex/README.md) will walk you through an end to end sample using the .NET Core C# language extension.

## Output Schema Support

By default, output column types are inferred from the .NET DataFrame column types. For string columns, you can explicitly specify the SQL data type using the `OutputColumnDataTypes` property.

### Specifying Output Column Types

Use `OutputColumnDataTypes` to specify the SQL data type for output columns by name:

```csharp
using Microsoft.SqlServer.CSharpExtension.SDK;
using Microsoft.Data.Analysis;
using static Microsoft.SqlServer.CSharpExtension.Sql;

public class MyExecutor : AbstractSqlServerExtensionExecutor
{
public override DataFrame Execute(DataFrame input, Dictionary<string, dynamic> sqlParams)
{
// Specify NVARCHAR (UTF-16) output for a string column
OutputColumnDataTypes["unicode_column"] = SqlDataType.DotNetWChar;

// Process and return data
return resultDataFrame;
Comment on lines +74 to +75
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code sample returns resultDataFrame, but that variable isn’t defined in the example, so the snippet won’t compile if copied as-is. Consider returning input (as in other examples) or declare/build resultDataFrame in the snippet to keep the documentation self-contained.

Suggested change
// Process and return data
return resultDataFrame;
// Process data if needed and return the DataFrame
return input;

Copilot uses AI. Check for mistakes.
}
}
```

### Supported String Types

| SqlDataType | SQL Type | Encoding | Description |
|-------------|----------|----------|-------------|
| `SqlDataType.DotNetChar` | VARCHAR | UTF-8 | Default for string columns |
| `SqlDataType.DotNetWChar` | NVARCHAR | UTF-16 | Use for Unicode data |

Comment on lines +82 to +86
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The “Supported String Types” table rows start with ||, which creates an extra empty column in Markdown and renders incorrectly on GitHub. Use a single leading | for the header/separator/data rows so the table has the intended 4 columns.

Copilot uses AI. Check for mistakes.
### Example: Mixed VARCHAR and NVARCHAR Output

```csharp
public class MixedOutputExecutor : AbstractSqlServerExtensionExecutor
{
public override DataFrame Execute(DataFrame input, Dictionary<string, dynamic> sqlParams)
{
// "ascii_col" will default to VARCHAR (no configuration needed)

// "unicode_col" should be NVARCHAR
OutputColumnDataTypes["unicode_col"] = SqlDataType.DotNetWChar;

return input;
}
}
```

### Default Behavior

If no explicit type is specified for a string column:
- String columns default to `DotNetChar` (VARCHAR/UTF-8)
- Numeric and other types are automatically mapped from their .NET types
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
using Microsoft.Data.Analysis;
using Microsoft.SqlServer.CSharpExtension.SDK;
using System.Text.RegularExpressions;
using static Microsoft.SqlServer.CSharpExtension.Sql;

namespace UserExecutor
{
Expand Down Expand Up @@ -60,6 +61,11 @@ public override DataFrame Execute(DataFrame input, Dictionary<string, dynamic> s
sqlParams["@rowsCount"] = output.Rows.Count;
sqlParams["@regexExpr"] = "Success!";

// Optionally, specify that "text" column should be output as NVARCHAR (UTF-16)
// instead of the default VARCHAR (UTF-8). Uncomment the line below to enable:
//
// OutputColumnDataTypes["text"] = SqlDataType.DotNetWChar;

// Return output dataset as a DataFrame
//
return output;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,19 +45,42 @@ public class CSharpOutputDataSet: CSharpDataSet
/// by extracting data and information from every DataFrameColumn.
/// </summary>
/// <param name="dataFrame">The DataFrame containing the output data.</param>
public unsafe void ExtractColumns(DataFrame dataFrame)
/// <param name="outputColumnDataTypes">Optional user-specified column data types by name.</param>
public unsafe void ExtractColumns(
DataFrame dataFrame,
Dictionary<string, SqlDataType> outputColumnDataTypes = null)
{
Logging.Trace("CSharpOutputDataSet::ExtractColumns");
_strLenOrNullMapPtrs = new IntPtr[ColumnsNumber];
_dataPtrs = new IntPtr[ColumnsNumber];

for(ushort columnNumber = 0; columnNumber < ColumnsNumber; ++columnNumber)
{
DataFrameColumn column = dataFrame.Columns[columnNumber];

// Determine the SQL data type for this column.
// All .NET strings are output as DotNetChar (varchar/UTF-8).
// Default behavior: map .NET types to SQL types (strings -> DotNetChar/varchar).
//
SqlDataType dataType = DataTypeMap[column.DataType];

// For string columns, check for user-specified type override
//
if (column.DataType == typeof(string) && outputColumnDataTypes != null)
{
if (outputColumnDataTypes.TryGetValue(column.Name, out var userType))
{
if (userType != SqlDataType.DotNetChar && userType != SqlDataType.DotNetWChar)
{
throw new ArgumentException(
$"Invalid type override '{userType}' for string column '{column.Name}'. " +
$"Only DotNetChar and DotNetWChar are supported for string columns.");
}

dataType = userType;
Logging.Trace($"ExtractColumns: Column '{column.Name}' using user-specified type: {dataType}");
}
Comment on lines +68 to +81
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These 'if' statements can be combined.

Suggested change
if (column.DataType == typeof(string) && outputColumnDataTypes != null)
{
if (outputColumnDataTypes.TryGetValue(column.Name, out var userType))
{
if (userType != SqlDataType.DotNetChar && userType != SqlDataType.DotNetWChar)
{
throw new ArgumentException(
$"Invalid type override '{userType}' for string column '{column.Name}'. " +
$"Only DotNetChar and DotNetWChar are supported for string columns.");
}
dataType = userType;
Logging.Trace($"ExtractColumns: Column '{column.Name}' using user-specified type: {dataType}");
}
if (column.DataType == typeof(string)
&& outputColumnDataTypes != null
&& outputColumnDataTypes.TryGetValue(column.Name, out var userType))
{
if (userType != SqlDataType.DotNetChar && userType != SqlDataType.DotNetWChar)
{
throw new ArgumentException(
$"Invalid type override '{userType}' for string column '{column.Name}'. " +
$"Only DotNetChar and DotNetWChar are supported for string columns.");
}
dataType = userType;
Logging.Trace($"ExtractColumns: Column '{column.Name}' using user-specified type: {dataType}");

Copilot uses AI. Check for mistakes.
}

ulong columnSize = (ulong)DataTypeSize[dataType];

// Add column metadata to a CSharpColumn dictionary
Expand Down Expand Up @@ -186,12 +209,11 @@ DataFrameColumn column
break;
case SqlDataType.DotNetWChar:
// Calculate column size from actual data.
// columnSize = max character count (UTF-16 byte length / 2).
// Minimum size is 1 character (nchar(0) is illegal in SQL).
// For WCHAR types, column size should be the max byte length.
// Minimum size is 2 bytes (1 UTF-16 character).
//
int maxUnicodeByteLen = colMap.Length > 0 ? colMap.Where(x => x > 0).DefaultIfEmpty(0).Max() : 0;
int maxCharCount = maxUnicodeByteLen / sizeof(char);
_columns[columnNumber].Size = (ulong)Math.Max(maxCharCount, MinUtf16CharSize);
_columns[columnNumber].Size = (ulong)Math.Max(maxUnicodeByteLen, MinUtf16CharSize);

SetDataPtrs<char>(columnNumber, GetUnicodeStringArray(column));
break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,12 @@ public void Execute(
{
_outputDataSet.ColumnsNumber = (ushort)_outputDataSet.CSharpDataFrame.Columns.Count;

_outputDataSet.ExtractColumns(_outputDataSet.CSharpDataFrame);
// Pass user-specified output column types
//
_outputDataSet.ExtractColumns(
_outputDataSet.CSharpDataFrame,
_userDll.UserExecutor.OutputColumnDataTypes);

*outputSchemaColumnsNumber = _outputDataSet.ColumnsNumber;
}
else
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
//*********************************************************************
using System.Collections.Generic;
using Microsoft.Data.Analysis;
using static Microsoft.SqlServer.CSharpExtension.Sql;

namespace Microsoft.SqlServer.CSharpExtension.SDK
{
Expand All @@ -23,6 +24,13 @@ public abstract class AbstractSqlServerExtensionExecutor
/// </summary>
public readonly int SQLSERVER_DOTNET_LANG_EXTENSION_V1 = 1;

/// <summary>
/// Optional: Specify SQL data types for output columns by name.
/// Use this to output string columns as NVARCHAR (DotNetWChar) instead of the default VARCHAR (DotNetChar).
/// Example: OutputColumnDataTypes["text"] = SqlDataType.DotNetWChar;
/// </summary>
public Dictionary<string, SqlDataType> OutputColumnDataTypes { get; } = new Dictionary<string, SqlDataType>();

/// <summary>
/// Default constructor for AbstractSqlServerExtensionExecutor
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
using System.Collections.Generic;
using Microsoft.Data.Analysis;
using Microsoft.SqlServer.CSharpExtension.SDK;
using static Microsoft.SqlServer.CSharpExtension.Sql;

namespace Microsoft.SqlServer.CSharpExtensionTest
{
Expand Down Expand Up @@ -187,4 +188,49 @@ public override DataFrame Execute(DataFrame input, Dictionary<string, dynamic> s
return null;
}
}

/// <summary>
/// Test executor demonstrating NVARCHAR output support for DataFrame columns.
/// Uses OutputColumnDataTypes to specify that string columns should be NVARCHAR.
/// </summary>
public class CSharpTestExecutorNVarcharOutput: AbstractSqlServerExtensionExecutor
{
public override DataFrame Execute(DataFrame input, Dictionary<string, dynamic> sqlParams){
Console.WriteLine("Hello .NET Core CSharpExtension!");
// Specify that output column "text" should be NVARCHAR (UTF-16)
OutputColumnDataTypes["text"] = SqlDataType.DotNetWChar;

// Return input unchanged - the column type will be NVARCHAR instead of VARCHAR
return input;
}
}

/// <summary>
/// Test executor demonstrating mixed VARCHAR and NVARCHAR output columns.
/// </summary>
public class CSharpTestExecutorMixedStringOutput: AbstractSqlServerExtensionExecutor
{
public override DataFrame Execute(DataFrame input, Dictionary<string, dynamic> sqlParams){
Console.WriteLine("Hello .NET Core CSharpExtension!");
// Column "ascii_col" stays VARCHAR (default, no need to specify)

// Column "unicode_col" should be NVARCHAR (by name)
OutputColumnDataTypes["unicode_col"] = SqlDataType.DotNetWChar;

return input;
}
}

/// <summary>
/// Test executor for basic pass-through (no NVARCHAR configuration).
/// </summary>
public class CSharpTestExecutorPreserveInputTypes: AbstractSqlServerExtensionExecutor
{
public override DataFrame Execute(DataFrame input, Dictionary<string, dynamic> sqlParams){
Console.WriteLine("Hello .NET Core CSharpExtension!");
// No explicit OutputColumnDataTypes configuration.
// All string columns will be VARCHAR (default).
return input;
}
}
}
Loading
Loading