Skip to content

[Bug][Connect-V2][Jdbc][Sqlserver] Fix JDBC sink null binding for SQLServer binary-like columns (e.g. IMAGE)#10626

Open
siwen-yu wants to merge 2 commits intoapache:devfrom
siwen-yu:hotfix/fix-10613
Open

[Bug][Connect-V2][Jdbc][Sqlserver] Fix JDBC sink null binding for SQLServer binary-like columns (e.g. IMAGE)#10626
siwen-yu wants to merge 2 commits intoapache:devfrom
siwen-yu:hotfix/fix-10613

Conversation

@siwen-yu
Copy link
Copy Markdown

Purpose of this pull request

fix #10613

Previously, when a field value was null, AbstractJdbcRowConverter used:
statement.setObject(index, null)

On SQLServer, this may cause the driver to infer nvarchar type for null values,
which leads to errors like:
"Implicit conversion from data type nvarchar to image is not allowed"

Does this PR introduce any user-facing change?

No.

This is a bug fix that corrects null handling for SQLServer binary columns.
Existing jobs will continue to work as before, except that previously failing
cases (e.g. writing null to IMAGE column) will now succeed.

How was this patch tested?

  1. Added unit tests in SqlserverJdbcRowConverterTest

  2. Verified logic through code path`

  3. Manually verified with SQLServer

Check list

@siwen-yu siwen-yu reopened this Mar 20, 2026
@DanielCarter-stack
Copy link
Copy Markdown

Issue 1: Redundant setObject calls in AbstractJdbcRowConverter

Location: AbstractJdbcRowConverter.java:219-221

if (fieldValue == null) {
    setNullToStatementByDataType(
            statement, seaTunnelDataType, statementIndex, sourceType);
    statement.setObject(statementIndex, null);  // ← Redundant call!
    continue;
}

Related Context:

  • Parent class: AbstractJdbcRowConverter.java:240-247 (setNullToStatementByDataType method)
  • Subclass: SqlserverJdbcRowConverter.java:58-71 (override implementation)
  • Callers: SimpleBatchStatementExecutor.java:48, InsertOrUpdateBatchStatementExecutor.java:89,96,155

Problem Description:
In the null branch of the toExternal() method, setNullToStatementByDataType() is called first, followed by statement.setObject(statementIndex, null). This leads to:

  1. SQLServer Scenario: setNull(index, Types.LONGVARBINARY) is called first, then immediately followed by setObject(index, null) which overrides the previous setting, fix becomes ineffective
  2. Other Database Scenarios: The base class's setNullToStatementByDataType() implementation also calls setObject(index, null), resulting in setting the same parameter twice

Potential Risks:

  • Risk 1 (High Severity): SQLServer fix is completely ineffective because setObject() will override the type information from setNull()
  • Risk 2 (Medium Severity): All databases will execute redundant JDBC calls, which doesn't affect functionality but impacts performance
  • Risk 3 (Low Severity): May cause confusion for certain JDBC drivers (continuously setting the same parameter)

Scope of Impact:

  • Direct Impact:
    • AbstractJdbcRowConverter.toExternal() - Common logic for all JDBC Converters
    • SqlserverJdbcRowConverter.setNullToStatementByDataType() - SQLServer-specific implementation (fix ineffective)
  • Indirect Impact:
    • JDBC Connectors for all 22 databases (MySQL, PostgreSQL, Oracle, etc.)
    • All Sink scenarios using SimpleBatchStatementExecutor and InsertOrUpdateBatchStatementExecutor
  • Impact Area: Multiple Connectors / Core Framework

Severity: CRITICAL (Blocking issue, must be fixed before merging)

Improvement Suggestions:

// AbstractJdbcRowConverter.java:218-223
if (fieldValue == null) {
    setNullToStatementByDataType(
            statement, seaTunnelDataType, statementIndex, sourceType);
    // Delete this line: statement.setObject(statementIndex, null);
    continue;
}

Rationale:

  • setNullToStatementByDataType() is already responsible for setting null values, including type information
  • Redundant setObject() calls will override the correctly set type information
  • This is a core logic error in the PR that will cause the fix to be completely ineffective

Issue 2: Old toExternal method in SqlserverJdbcRowConverter not deleted

Location: SqlserverJdbcRowConverter.java:91-164

public PreparedStatement toExternal(
        SeaTunnelRowType rowType, SeaTunnelRow row, PreparedStatement statement)
        throws SQLException {
    // ... Large amount of logic duplicated with parent class
}

Related Context:

  • Parent class: AbstractJdbcRowConverter.java:194-238 (new version of toExternal)
  • Interface: JdbcRowConverter.java:43-56 (defines two overloaded versions)
  • Caller: SimpleBatchStatementExecutor.java:48 - calls the 4-parameter version

Problem Description:
SqlserverJdbcRowConverter retains an old version of the toExternal method (signature: SeaTunnelRowType, SeaTunnelRow, PreparedStatement). This method:

  1. Duplicates implementation logic from the parent class (approximately 70 lines of code)
  2. Will not be called because all callers use the 4-parameter version (with databaseTableSchema)
  3. Contains obsolete null handling logic (Lines 98-101), which is exactly the issue this PR aims to fix
  4. Creates confusion with the parent class method, making future maintainers unsure which one to modify

Potential Risks:

  • Risk 1 (Low Severity): Code redundancy increases maintenance costs
  • Risk 2 (Medium Severity): If future callers mistakenly use this method, they will bypass the new null handling logic
  • Risk 3 (Medium Severity): The old null handling logic in this method (Line 98: if (fieldValue == null && seaTunnelDataType.getSqlType() != SqlType.BYTES)) contradicts the fix claimed by the PR, indicating this method is deprecated

Scope of Impact:

  • Direct Impact: SqlserverJdbcRowConverter class itself
  • Indirect Impact: May cause code review confusion and future errors
  • Impact Area: Single Connector (SQLServer)

Severity: MAJOR (Does not block merging, but should be cleaned up)

Improvement Suggestions:

// Delete the entire method at SqlserverJdbcRowConverter.java:91-164
// Reason:
// 1. This method will not be called (all callers use the 4-parameter version)
// 2. Logic duplicated with parent class
// 3. Contains old, buggy null handling logic
// 4. Deletion does not affect any functionality (parent class method will take effect)

Rationale:


Issue 3: Other databases may have the same issue

Location: All other JDBC Connector subclasses

Related Context:

  • Other subclasses: OracleJdbcRowConverter, DB2JdbcRowConverter, PostgresJdbcRowConverter, etc. (21 in total)
  • Similar scenarios: Oracle's BLOB, CLOB types; DB2's BLOB type; MySQL's BLOB, BINARY types

Problem Description:
This PR fixes the binary type null binding issue for SQLServer, but:

  1. Oracle, DB2, MySQL and other databases also have similar binary types (BLOB, LONG RAW, etc.)
  2. These databases' JDBC drivers may have the same type inference issue
  3. Currently, all these subclasses use the base class's default implementation, which is setObject(index, null)
  4. If they have the same problem, users will report similar bugs

Potential Risks:

  • Risk 1 (Medium Severity): Future users may report the same issue for other databases
  • Risk 2 (Low Severity): Requires repetitive fix work (no unified handling)
  • Risk 3 (Low Severity): Fixes for different databases may be inconsistent

Scope of Impact:

  • Direct Impact: None (currently only affects SQLServer)
  • Indirect Impact: May need to apply similar fixes in other database subclasses
  • Impact Area: Multiple Connectors (potential)

Severity: MINOR (Does not block current PR, but worth considering)

Improvement Suggestions:

// Solution 1 (Recommended): Add similar overrides in other database subclasses
// For example OracleJdbcRowConverter.java:

@Override
protected void setNullToStatementByDataType(
        PreparedStatement statement,
        SeaTunnelDataType<?> seaTunnelDataType,
        int statementIndex,
        @Nullable String sourceType)
        throws SQLException {
    if (seaTunnelDataType.getSqlType() == SqlType.BYTES) {
        // Oracle BLOB type requires special handling
        statement.setNull(statementIndex, java.sql.Types.BLOB);
        return;
    }
    statement.setObject(statementIndex, null);
}

// Solution 2 (Long-term optimization): Provide generic mapping in AbstractJdbcRowConverter
// Use a Map<SqlType, Integer> to store default JDBC Types

Rationale:

  • This PR provides a good pattern that can be reused in other databases
  • Suggestion: Merge the current PR first, then create a follow-up issue to systematically check other databases
  • Can search GitHub Issues to see if there are similar bug reports for other databases

Issue 4: Handling of unknown sourceType in resolveSqlServerBytesNullType

Location: SqlserverJdbcRowConverter.java:73-89

private int resolveSqlServerBytesNullType(@Nullable String sourceType) {
    if (sourceType == null) {
        return java.sql.Types.VARBINARY;  // ← Is the default value reasonable?
    }
    
    if (SqlServerTypeConverter.SQLSERVER_IMAGE.equals(sourceType)) {
        return java.sql.Types.LONGVARBINARY;
    }
    if (sourceType.startsWith(SqlServerTypeConverter.SQLSERVER_VARBINARY)) {
        return java.sql.Types.VARBINARY;
    }
    if (sourceType.startsWith(SqlServerTypeConverter.SQLSERVER_BINARY)) {
        return java.sql.Types.BINARY;
    }
    
    return java.sql.Types.VARBINARY;  // ← Return VARBINARY for unknown types?
}

Related Context:

  • Type definitions: SqlServerTypeConverter.java:76-90 (defines SQLSERVER_IMAGE, SQLSERVER_VARBINARY, SQLSERVER_BINARY)
  • Possible unknown types: User-defined types, legacy SQLServer types (e.g., NTEXT), future new types

Problem Description:

  1. When sourceType == null (Lines 74-76), it defaults to returning VARBINARY, which may be reasonable, but:

    • If databaseTableSchema == null, we cannot obtain the actual source type
    • Using VARBINARY as a default value may not be applicable to certain scenarios
  2. When sourceType is an unknown value (Line 88), it also returns VARBINARY:

    • No log warning, users are unaware of the degradation
    • If it's a future new SQLServer type, it may lead to silent errors
    • If it's a configuration error, users cannot perceive it

Potential Risks:

  • Risk 1 (Medium Severity): Unknown types may need LONGVARBINARY rather than VARBINARY
  • Risk 2 (Low Severity): Silent degradation, difficult to debug
  • Risk 3 (Low Severity): Future new SQLServer types may not be handled correctly

Scope of Impact:

  • Direct Impact: SqlserverJdbcRowConverter.resolveSqlServerBytesNullType()
  • Indirect Impact: Writing BYTES type null values to SQLServer
  • Impact Area: Single Connector (SQLServer)

Severity: MAJOR (Recommended to improve before merging)

Improvement Suggestions:

private int resolveSqlServerBytesNullType(@Nullable String sourceType) {
    if (sourceType == null) {
        log.warn(
            "Cannot determine source type for BYTES field, defaulting to VARBINARY. " +
            "If this causes errors, please provide databaseTableSchema.");
        return java.sql.Types.VARBINARY;
    }
    
    if (SqlServerTypeConverter.SQLSERVER_IMAGE.equals(sourceType)) {
        return java.sql.Types.LONGVARBINARY;
    }
    if (sourceType.startsWith(SqlServerTypeConverter.SQLSERVER_VARBINARY)) {
        return java.sql.Types.VARBINARY;
    }
    if (sourceType.startsWith(SqlServerTypeConverter.SQLSERVER_BINARY)) {
        return java.sql.Types.BINARY;
    }
    
    // Unknown type, log warning and use conservative LONGVARBINARY
    log.warn(
        "Unknown SQLServer binary type: {}, defaulting to LONGVARBINARY. " +
        "This may cause issues if the target column is not a large binary type.",
        sourceType);
    return java.sql.Types.LONGVARBINARY;  // More conservative default value
}

Rationale:

  • For sourceType == null, using VARBINARY is reasonable (applicable to most scenarios)
  • For unknown types, using LONGVARBINARY is more conservative (can accommodate larger data)
  • Adding logs can help users diagnose issues
  • Follows the "Robustness Principle" (Be Liberal in What You Accept)

Alternatively, if you don't want to add logs in production:

// Minimize changes: only modify return value for unknown types
private int resolveSqlServerBytesNullType(@Nullable String sourceType) {
    if (sourceType == null) {
        return java.sql.Types.VARBINARY;
    }
    
    if (SqlServerTypeConverter.SQLSERVER_IMAGE.equals(sourceType)) {
        return java.sql.Types.LONGVARBINARY;
    }
    if (sourceType.startsWith(SqlServerTypeConverter.SQLSERVER_VARBINARY)) {
        return java.sql.Types.VARBINARY;
    }
    if (sourceType.startsWith(SqlServerTypeConverter.SQLSERVER_BINARY)) {
        return java.sql.Types.BINARY;
    }
    
    // Use LONGVARBINARY for unknown types (more conservative)
    return java.sql.Types.LONGVARBINARY;
}

Issue 5: Missing JavaDoc and comments

Location:

  • AbstractJdbcRowConverter.java:240-247 (setNullToStatementByDataType)
  • SqlserverJdbcRowConverter.java:58-71 (override implementation)
  • SqlserverJdbcRowConverter.java:73-89 (resolveSqlServerBytesNullType)

Related Context:

  • Similar methods: AbstractJdbcRowConverter.java:249-336 (setValueToStatementByDataType - has complete switch-case comments)

Problem Description:
The two newly added methods have no JavaDoc or comments:

  1. setNullToStatementByDataType() - Does not explain:

    • Why this method is needed (instead of directly using setObject(index, null))
    • Why subclasses need to override
    • The purpose of parameter sourceType
  2. resolveSqlServerBytesNullType() - Does not explain:

    • Why SQLServer needs special handling
    • Why each type maps to the corresponding JDBC Type
    • The reason for IMAGELONGVARBINARY

Potential Risks:

  • Risk 1 (Low Severity): Future maintainers may not understand why this method is needed
  • Risk 2 (Low Severity): May be mistakenly considered redundant code and deleted
  • Risk 3 (Medium Severity): Other database developers won't know how to apply the same pattern

Scope of Impact:

  • Direct Impact: Code maintainability
  • Indirect Impact: Knowledge transfer, future extensions
  • Impact Area: Single Connector + potential impact on other Connectors

Severity: MINOR (Does not block merging, but strongly recommended to add)

Improvement Suggestions:

// AbstractJdbcRowConverter.java:240-247
/**
 * Bind a null value to the PreparedStatement parameter.
 * 
 * <p>Default implementation uses {@link PreparedStatement#setObject(int, Object)},
 * which works for most databases. However, some databases (e.g., SQL Server) require
 * explicit type information for null values in certain column types (e.g., IMAGE, BLOB).
 * 
 * <p>Subclasses can override this method to provide database-specific null handling.
 * For example, SQL Server uses {@link PreparedStatement#setNull(int, int)} with
 * the appropriate JDBC type (e.g., {@link java.sql.Types#LONGVARBINARY} for IMAGE columns).
 * 
 * @param statement the PreparedStatement to bind the parameter to
 * @param seaTunnelDataType the SeaTunnel data type of the field
 * @param statementIndex the 1-based parameter index in the PreparedStatement
 * @param sourceType the source database column type (if available), used to determine
 *                   the appropriate JDBC type for null values
 * @throws SQLException if a database access error occurs
 */
protected void setNullToStatementByDataType(
        PreparedStatement statement,
        SeaTunnelDataType<?> seaTunnelDataType,
        int statementIndex,
        @Nullable String sourceType)
        throws SQLException {
    statement.setObject(statementIndex, null);
}

// SqlserverJdbcRowConverter.java:58-89
/**
 * {@inheritDoc}
 * 
 * <p>SQL Server requires explicit type information for null values in binary columns
 * (IMAGE, VARBINARY, BINARY). Using {@code setObject(index, null)} causes the driver to
 * infer the type as NVARCHAR, which leads to conversion errors:
 * <pre>
 * Implicit conversion from data type nvarchar to image is not allowed
 * </pre>
 * 
 * <p>This method uses {@code setNull(index, sqlType)} with the appropriate JDBC type
 * based on the source column type.
 */
@Override
protected void setNullToStatementByDataType(
        PreparedStatement statement,
        SeaTunnelDataType<?> seaTunnelDataType,
        int statementIndex,
        @Nullable String sourceType)
        throws SQLException {
    
    if (seaTunnelDataType.getSqlType() == SqlType.BYTES) {
        statement.setNull(statementIndex, resolveSqlServerBytesNullType(sourceType));
        return;
    }
    
    statement.setObject(statementIndex, null);
}

/**
 * Resolves the appropriate JDBC type for SQL Server binary columns when the value is null.
 * 
 * <p>SQL Server has multiple binary types, each mapping to a different JDBC type:
 * <ul>
 *   <li>IMAGE → LONGVARBINARY (legacy large binary type)</li>
 *   <li>VARBINARY(*) → VARBINARY</li>
 *   <li>VARBINARY(MAX) → VARBINARY</li>
 *   <li>BINARY(*) → BINARY (fixed-length)</li>
 * </ul>
 * 
 * @param sourceType the SQL Server column type (e.g., "IMAGE", "VARBINARY", "BINARY")
 * @return the JDBC type constant from {@link java.sql.Types}
 */
private int resolveSqlServerBytesNullType(@Nullable String sourceType) {
    // ... implementation
}

Rationale:

  • Follows Apache project documentation standards
  • Helps future maintainers understand design intent
  • Provides reference for other database developers
  • Aligns with "code as documentation" best practices

Issue 6: Incomplete unit test coverage

Location: SqlserverJdbcRowConverterTest.java

Related Context:

  • Test methods: testSetNullToStatementByDataTypeForImage, testSetNullToStatementByDataTypeForVarbinary, testSetNullToStatementByDataTypeForBinary, testSetNullToStatementByDataTypeForMaxVarbinary
  • Missing tests: sourceType == null, non-BYTES types, exception scenarios

Problem Description:
Current tests only cover 4 known sourceTypes, but are missing the following scenarios:

  1. sourceType == null scenario:

    • Code handles this case at Lines 74-76
    • But no test validation
    • This is an actually possible scenario (when databaseTableSchema == null)
  2. Null values for non-BYTES types:

    • Code calls setObject(index, null) at Line 70
    • But no test validation for null handling of non-BYTES types (such as STRING, INT)
    • Ensure no regression issues are introduced
  3. Unknown sourceType:

    • If an unrecognized type is passed (e.g., "UNKNOWN_BINARY")
    • Should return VARBINARY (or improved LONGVARBINARY)
    • But no test validates this fallback logic
  4. Compatibility with other databases:

    • No test validates that other databases (such as MySQL) are not affected
    • Although this is a unit test, integration tests can be added

Potential Risks:

  • Risk 1 (Medium Severity): sourceType == null path is untested, may have boundary issues
  • Risk 2 (Low Severity): Null handling for non-BYTES types has no regression tests
  • Risk 3 (Low Severity): Fallback logic for unknown types is unverified

Scope of Impact:

  • Direct Impact: Test coverage
  • Indirect Impact: Future refactoring may break these untested paths
  • Impact Area: Single Connector (SQLServer)

Severity: MAJOR (Recommended to supplement key tests before merging)

Improvement Suggestions:

// SqlserverJdbcRowConverterTest.java

@Test
void testSetNullToStatementByDataTypeForNullSourceType() throws Exception {
    // Test when sourceType is null, use default VARBINARY
    PreparedStatement statement = mock(PreparedStatement.class);
    SqlserverJdbcRowConverter converter = new SqlserverJdbcRowConverter();
    SeaTunnelDataType<?> bytesType = PrimitiveByteArrayType.INSTANCE;
    
    converter.setNullToStatementByDataType(
            statement, bytesType, 1, null);
    
    verify(statement).setNull(1, Types.VARBINARY);
}

@Test
void testSetNullToStatementByDataTypeForNonBytesType() throws Exception {
    // Test null values of non-BYTES type should use setObject
    PreparedStatement statement = mock(PreparedStatement.class);
    SqlserverJdbcRowConverter converter = new SqlserverJdbcRowConverter();
    SeaTunnelDataType<?> stringType = BasicType.STRING_TYPE;
    
    converter.setNullToStatementByDataType(
            statement, stringType, 1, null);
    
    verify(statement).setObject(1, null);
}

@Test
void testSetNullToStatementByDataTypeForUnknownSourceType() throws Exception {
    // Test unknown sourceType should use default type
    PreparedStatement statement = mock(PreparedStatement.class);
    SqlserverJdbcRowConverter converter = new SqlserverJdbcRowConverter();
    SeaTunnelDataType<?> bytesType = PrimitiveByteArrayType.INSTANCE;
    
    converter.setNullToStatementByDataType(
            statement, bytesType, 1, "UNKNOWN_BINARY_TYPE");
    
    // According to improvement suggestions, should return LONGVARBINARY or VARBINARY
    verify(statement).setNull(1, Types.LONGVARBINARY); // Or Types.VARBINARY
}

Rationale:

  • These are critical path tests that should be covered
  • Test cost is not high, but can significantly improve code quality
  • Meets Apache project testing standards
  • Can prevent future regression issues

Issue 7: Missing integration tests or E2E tests

Location: PR's test coverage

Related Context:

Problem Description:

  1. Missing end-to-end tests:

    • No tests for real SQLServer database connections
    • No tests for actual data write scenarios
    • Cannot verify the effectiveness of the fix in a real environment
  2. Missing cross-database tests:

    • No tests to verify other databases are not affected
    • No tests to verify databaseTableSchema parameter passing

Potential Risks:

  • Risk 1 (Medium Severity): Unit tests pass, but real environment may have issues
  • Risk 2 (Medium Severity): Cannot capture environment-related issues (such as driver version)
  • Risk 3 (Low Severity): Manual testing cannot continuously validate

Scope of Impact:

  • Direct Impact: Test coverage and confidence
  • Indirect Impact: Future refactoring may break functionality
  • Impact Area: Single Connector (SQLServer)

Severity: MAJOR (Strongly recommended to add, but does not block merging)

Improvement Suggestions:

// Add tests in seatunnel-e2e/seatunnel-connector-v2-e2e/connector-sqlserver-e2e

@Test
public void testWriteNullToBinaryColumns() throws Exception {
    // Create table with IMAGE, VARBINARY, BINARY columns
    String createTableSql = 
        "CREATE TABLE test_binary_null (\n" +
        "    id INT,\n" +
        "    image_col IMAGE NULL,\n" +
        "    varbinary_col VARBINARY(100) NULL,\n" +
        "    binary_col BINARY(10) NULL\n" +
        ")";
    
    // Insert data containing null values
    // Verify data is successfully written
    // Verify null values are correctly saved
}

@Test
public void testWriteMixedNullAndNonNull() throws Exception {
    // Test scenarios containing both null and non-null values
}

Rationale:

  • E2E tests can catch issues that unit tests cannot discover
  • Meets Apache project testing standards
  • Can prevent future regressions
  • Recommended to add in a follow-up PR (does not block current PR)

@siwen-yu
Copy link
Copy Markdown
Author

@DanielCarter-stack Thanks for the detailed and thorough review — I’ve gone through all the points carefully.

Changes to be addressed in this PR

Issue 1
Good catch — this is an oversight on my side.

Issue 2
This appears to be a legacy method and was not introduced in this PR.
I plan to remove it, but I’ll double-check all call sites before doing so.

Issue 4
I’ll improve the fallback handling for null or unknown sourceType.

Issue 5
I’ll add JavaDoc/comments to clarify

Issue 6
I’ll extend unit test coverage to include:

  • sourceType == null
  • non-BYTES null handling
  • unknown sourceType

Follow-ups

Issue 3
I agree that similar null-binding issues may exist in other JDBC dialects (Oracle, DB2, MySQL, etc.).
To keep this PR focused on the reported SQLServer IMAGE issue, I’ll address that in a separate follow-up issue/PR.

Issue 7
I agree that E2E coverage would be valuable.
I’ll add E2E tests in a follow-up PR to keep the current changeset focused and easier to review.


Thanks again for the valuable feedback — I’ll update the PR soon.

…ary types and clean up legacy logic.

- Fix null binding override issue in AbstractJdbcRowConverter
- Remove legacy unused toExternal(...) method
- Improve fallback handling for unknown sourceType with warning
- Add JavaDoc for SQLServer null binding behavior
- Extend unit test coverage
@siwen-yu
Copy link
Copy Markdown
Author

Addressed review comments in the latest update:

  • Fixed null binding override issue
  • Removed legacy toExternal(...) method
  • Improved fallback handling for unknown sourceType
  • Added JavaDoc and extended unit tests

Please take another look. Thanks!

@davidzollo davidzollo removed the jdbc label Mar 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] [Connector-v2][Jdbc][SqlServer] SQLServer to SQLServer sync fails with IMAGE type: nvarchar incompatible with image

3 participants