Skip to content
Merged
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
105 changes: 105 additions & 0 deletions docker/thirdparties/docker-compose/sqlserver/init/03-create-table.sql
Original file line number Diff line number Diff line change
Expand Up @@ -284,3 +284,108 @@ CREATE TABLE dbo.test_date_filter (
datetime_value datetime NULL,
datetime2_value datetime2 NULL
);

-- User-defined alias types (CREATE TYPE ... FROM base_type). DatabaseMetaData.getColumns()
-- reports such columns with TYPE_NAME set to the alias name, see #67793.
-- They must exist before the tables below are created, hence the batch separator.
CREATE TYPE dbo.doris_alias_varchar FROM varchar(50) NOT NULL;
CREATE TYPE dbo.doris_alias_varcharmax FROM varchar(max) NULL;
CREATE TYPE dbo.doris_alias_nvarchar FROM nvarchar(20) NULL;
CREATE TYPE dbo.doris_alias_nvarcharmax FROM nvarchar(max) NULL;
CREATE TYPE dbo.doris_alias_char FROM char(10) NULL;
CREATE TYPE dbo.doris_alias_nchar FROM nchar(10) NULL;
CREATE TYPE dbo.doris_alias_text FROM text NULL;
CREATE TYPE dbo.doris_alias_ntext FROM ntext NULL;
CREATE TYPE dbo.doris_alias_bit FROM bit NULL;
CREATE TYPE dbo.doris_alias_tinyint FROM tinyint NULL;
CREATE TYPE dbo.doris_alias_smallint FROM smallint NULL;
CREATE TYPE dbo.doris_alias_int FROM int NULL;
CREATE TYPE dbo.doris_alias_bigint FROM bigint NULL;
CREATE TYPE dbo.doris_alias_real FROM real NULL;
CREATE TYPE dbo.doris_alias_float FROM float NULL;
CREATE TYPE dbo.doris_alias_decimal FROM decimal(10, 2) NULL;
CREATE TYPE dbo.doris_alias_numeric FROM numeric(38, 10) NULL;
CREATE TYPE dbo.doris_alias_money FROM money NULL;
CREATE TYPE dbo.doris_alias_smallmoney FROM smallmoney NULL;
CREATE TYPE dbo.doris_alias_date FROM date NULL;
CREATE TYPE dbo.doris_alias_time FROM time NULL;
CREATE TYPE dbo.doris_alias_datetime FROM datetime NULL;
CREATE TYPE dbo.doris_alias_datetime2 FROM datetime2(3) NULL;
CREATE TYPE dbo.doris_alias_datetime2_default FROM datetime2 NULL;
CREATE TYPE dbo.doris_alias_smalldatetime FROM smalldatetime NULL;
CREATE TYPE dbo.doris_alias_guid FROM uniqueidentifier NULL;
CREATE TYPE dbo.doris_alias_identity FROM int NOT NULL;
-- Aliases over types that the JDBC catalog can not resolve by type code. They must stay UNSUPPORTED.
CREATE TYPE dbo.doris_alias_binary FROM binary(20) NULL;
CREATE TYPE dbo.doris_alias_varbinary FROM varbinary(20) NULL;
CREATE TYPE dbo.doris_alias_image FROM image NULL;
CREATE TYPE dbo.doris_alias_datetimeoffset FROM datetimeoffset NULL;
CREATE TYPE dbo.doris_alias_variant FROM sql_variant NULL;
-- Alias names that start with a system type name: they are reported as they are and must not be
-- mistaken for that system type.
CREATE TYPE dbo.[int alias] FROM varchar(50) NULL;
CREATE TYPE dbo.[decimal(18,0) identity] FROM nvarchar(20) NULL;
CREATE TYPE dbo.[int identity] FROM varchar(10) NULL;
GO

-- Every supported base type family behind an alias, plus sysname (a built-in alias over nvarchar(128)).
CREATE TABLE dbo.test_alias_type (
id int PRIMARY KEY NOT NULL,
plain_col varchar(50) NULL,
alias_varchar_col dbo.doris_alias_varchar NULL,
alias_varcharmax_col dbo.doris_alias_varcharmax NULL,
alias_nvarchar_col dbo.doris_alias_nvarchar NULL,
alias_nvarcharmax_col dbo.doris_alias_nvarcharmax NULL,
alias_char_col dbo.doris_alias_char NULL,
alias_nchar_col dbo.doris_alias_nchar NULL,
alias_text_col dbo.doris_alias_text NULL,
alias_ntext_col dbo.doris_alias_ntext NULL,
alias_bit_col dbo.doris_alias_bit NULL,
alias_tinyint_col dbo.doris_alias_tinyint NULL,
alias_smallint_col dbo.doris_alias_smallint NULL,
alias_int_col dbo.doris_alias_int NULL,
alias_bigint_col dbo.doris_alias_bigint NULL,
alias_real_col dbo.doris_alias_real NULL,
alias_float_col dbo.doris_alias_float NULL,
alias_decimal_col dbo.doris_alias_decimal NULL,
alias_numeric_col dbo.doris_alias_numeric NULL,
alias_money_col dbo.doris_alias_money NULL,
alias_smallmoney_col dbo.doris_alias_smallmoney NULL,
alias_date_col dbo.doris_alias_date NULL,
alias_time_col dbo.doris_alias_time NULL,
alias_datetime_col dbo.doris_alias_datetime NULL,
alias_datetime2_col dbo.doris_alias_datetime2 NULL,
alias_datetime2_default_col dbo.doris_alias_datetime2_default NULL,
alias_smalldatetime_col dbo.doris_alias_smalldatetime NULL,
alias_guid_col dbo.doris_alias_guid NULL,
sysname_col sysname NULL
);

-- IDENTITY on an alias typed column: the driver reports the plain alias name as TYPE_NAME.
CREATE TABLE dbo.test_alias_identity (
id dbo.doris_alias_identity IDENTITY(1,1) PRIMARY KEY,
val dbo.doris_alias_varchar NULL
);

-- Alias names that start with a system type name, next to a real IDENTITY column.
CREATE TABLE dbo.test_alias_name (
id int IDENTITY(1,1) PRIMARY KEY,
alias_named_int_col dbo.[int alias] NULL,
alias_named_decimal_identity_col dbo.[decimal(18,0) identity] NULL,
alias_named_int_identity_col dbo.[int identity] NULL
);

-- Negative cases: aliases over binary types, datetimeoffset and sql_variant, and the xml / CLR system
-- types. All of them must be reported as UNSUPPORTED while the other columns stay readable.
CREATE TABLE dbo.test_alias_unsupported (
id int PRIMARY KEY NOT NULL,
plain_col varchar(50) NULL,
alias_binary_col dbo.doris_alias_binary NULL,
alias_varbinary_col dbo.doris_alias_varbinary NULL,
alias_image_col dbo.doris_alias_image NULL,
alias_datetimeoffset_col dbo.doris_alias_datetimeoffset NULL,
alias_variant_col dbo.doris_alias_variant NULL,
xml_col xml NULL,
geometry_col geometry NULL,
hierarchyid_col hierarchyid NULL
);
20 changes: 20 additions & 0 deletions docker/thirdparties/docker-compose/sqlserver/init/04-insert.sql
Original file line number Diff line number Diff line change
Expand Up @@ -132,3 +132,23 @@ Insert into dbo.test_date_filter values
(3, '2024-12-31', '2024-12-31 23:59:59', '2024-12-31 23:59:59.999'),
(4, '2023-01-17', '2023-01-17 08:00:00', '2023-01-17 08:00:00'),
(5, '2025-03-15', '2025-03-15 12:00:00', '2025-03-15 12:00:00.500');

-- Alias typed columns, see #67793
Insert into dbo.test_alias_type values
(1, 'plain', 'alias', 'alias varchar max', 'alias nvarchar', 'alias nvarchar max', 'Doris', 'Doris', 'alias text', 'alias ntext',
1, 255, 32767, 1, 9223372036854775807, 123.123, 1.5, 12345.67, 1234567890123456789012345678.0123456789, 123.4567, 214748.3647,
'2023-01-17', '16:49:05.1234567', '2023-01-17 16:49:05', '2023-01-17 10:30:45.123', '2023-01-17 16:49:05.1234567', '2023-01-17 16:49:05',
'FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF', 'sysname value'),
(2, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL, NULL, NULL,
NULL, NULL);

Insert into dbo.test_alias_identity (val) values ('first'), ('second');

Insert into dbo.test_alias_name (alias_named_int_col, alias_named_decimal_identity_col, alias_named_int_identity_col)
values ('not an int', 'not a decimal', 'not an id');

Insert into dbo.test_alias_unsupported values
(1, 'plain', 0x01, 0x0102, 0x03, '2023-01-17 16:49:05 +08:00', 1, '<a/>', geometry::STGeomFromText('POINT (1 2)', 0), hierarchyid::GetRoot()),
(2, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.Statement;
import java.sql.Types;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
* SQL Server-specific JDBC connector client.
Expand All @@ -36,6 +40,12 @@ public class JdbcSQLServerConnectorClient extends JdbcConnectorClient {

private static final Logger LOG = LogManager.getLogger(JdbcSQLServerConnectorClient.class);

// TYPE_NAME of an IDENTITY column decorates the base type: "int identity", "decimal() identity",
// "numeric(18, 0) identity", "decimal(18,0) IDENTITY(1,1)". IDENTITY is only allowed on these base types.
private static final Pattern IDENTITY_TYPE_NAME = Pattern.compile(
"^(tinyint|smallint|int|bigint|decimal|numeric)\\s*(\\([^)]*\\))?\\s+identity(\\s*\\([^)]*\\))?$",
Pattern.CASE_INSENSITIVE);

public JdbcSQLServerConnectorClient(
String catalogName, JdbcDbType dbType, String jdbcUrl,
boolean onlySpecifiedDatabase,
Expand All @@ -48,13 +58,48 @@ public JdbcSQLServerConnectorClient(
enableMappingVarbinary, enableMappingTimestampTz);
}

/**
* The base type of an IDENTITY column's TYPE_NAME, or the name unchanged.
* <p>
* The decoration is trusted only when {@code DATA_TYPE} is the code of the named base type: an alias type
* may legally be named like that ({@code CREATE TYPE dbo.[int identity] FROM varchar(10)}, or
* {@code dbo.[int alias]}), and it then has to be resolved by its code, not by the words of its name.
*/
static String identityBaseType(String typeName, int dataType) {
Matcher matcher = IDENTITY_TYPE_NAME.matcher(typeName);
if (!matcher.matches()) {
return typeName;
}
String baseType = matcher.group(1).toLowerCase(Locale.ROOT);
boolean codeMatches;
switch (baseType) {
case "tinyint":
codeMatches = dataType == Types.TINYINT;
break;
case "smallint":
codeMatches = dataType == Types.SMALLINT;
break;
case "int":
codeMatches = dataType == Types.INTEGER;
break;
case "bigint":
codeMatches = dataType == Types.BIGINT;
break;
default:
codeMatches = dataType == Types.DECIMAL || dataType == Types.NUMERIC;
break;
}
return codeMatches ? baseType : typeName;
}

@Override
public ConnectorType jdbcTypeToConnectorType(JdbcFieldInfo fieldInfo) {
String rawType = fieldInfo.getDataTypeName().orElse("unknown").toLowerCase();
// SQL Server JDBC driver decorates type names for IDENTITY columns,
// e.g., "int identity", "decimal() identity". Strip parenthesized parts
// and suffixes to get the base type name.
String ssType = rawType.replaceAll("[\\s(].*", "");
// An IDENTITY column is reported as "int identity" or "decimal(18,0) identity": only the base type
// is matched below. Any other name is matched as it is: system type names are single words, and a
// user-defined alias type may be named with spaces or parentheses ("int alias") and must not be
// mistaken for the system type its name starts with.
String ssType = identityBaseType(rawType, fieldInfo.getDataType());
switch (ssType) {
case "bit":
return ConnectorType.of("BOOLEAN");
Expand Down Expand Up @@ -105,6 +150,74 @@ public ConnectorType jdbcTypeToConnectorType(JdbcFieldInfo fieldInfo) {
return enableMappingVarbinary
? ConnectorType.of("VARBINARY", fieldInfo.requiredColumnSize(), -1)
: ConnectorType.of("STRING");
case "xml":
case "sql_variant":
case "geometry":
case "geography":
case "hierarchyid":
case "json":
case "vector":
// SQL Server system types that Doris does not support. They are listed explicitly
// so that they never reach the JDBC type code fallback below.
return ConnectorType.of("UNSUPPORTED");
default:
return jdbcTypeCodeToConnectorType(fieldInfo);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P1] Route legal alias identifiers through the base-code fallback

SQL Server permits alias type names to be delimited identifiers containing spaces or parentheses (CREATE TYPE, identifier rules). For CREATE TYPE dbo.[int alias] FROM varchar(50), getColumns() supplies TYPE_NAME = int alias and the base DATA_TYPE = VARCHAR; the normalization at line 58 reduces the name to int, so this fallback is never reached and FE exposes the VARCHAR column as INT. The legacy client has the same preemption. Please restrict IDENTITY normalization to a code-compatible IDENTITY form (or otherwise prefer unambiguous base codes before name dispatch) and cover legal alias names and system-name collisions in both paths.

}
}

/**
* Fallback for type names that are not SQL Server system types.
* <p>
* User-defined alias types ({@code CREATE TYPE dbo.my_type FROM varchar(50)}) are reported by
* {@code DatabaseMetaData.getColumns()} with {@code TYPE_NAME} set to the alias name, so they can not be
* matched by name. {@code DATA_TYPE}, {@code COLUMN_SIZE} and {@code DECIMAL_DIGITS} still describe the
* base type, so the standard {@link Types} code is used to resolve the Doris type. The mapping mirrors
* the name based one above.
* <p>
* Binary codes are deliberately not mapped: mssql-jdbc also reports CLR user-defined types
* (geometry, geography, hierarchyid, ...) as {@link Types#VARBINARY}, so they can not be told apart from
* an alias over a binary type by the type code alone. Vendor specific codes stay unsupported as well.
*/
private ConnectorType jdbcTypeCodeToConnectorType(JdbcFieldInfo fieldInfo) {
switch (fieldInfo.getDataType()) {
case Types.BIT:
case Types.BOOLEAN:
return ConnectorType.of("BOOLEAN");
// SQL Server tinyint is unsigned (0 to 255), so it needs SMALLINT
case Types.TINYINT:
case Types.SMALLINT:
return ConnectorType.of("SMALLINT");
case Types.INTEGER:
return ConnectorType.of("INT");
case Types.BIGINT:
return ConnectorType.of("BIGINT");
case Types.REAL:
return ConnectorType.of("FLOAT");
case Types.FLOAT:
case Types.DOUBLE:
return ConnectorType.of("DOUBLE");
case Types.DECIMAL:
case Types.NUMERIC: {
// money and smallmoney are reported as DECIMAL(19,4) and DECIMAL(10,4)
int precision = fieldInfo.requiredColumnSize();
int scale = fieldInfo.requiredDecimalDigits();
return createDecimalOrString(precision, scale);
}
case Types.DATE:
return ConnectorType.of("DATEV2");
case Types.TIMESTAMP: {
int scale = fieldInfo.getDecimalDigits().orElse(0);
scale = Math.min(scale, JDBC_DATETIME_SCALE);
return ConnectorType.of("DATETIMEV2", scale, -1);
}
case Types.CHAR:
case Types.NCHAR:
case Types.VARCHAR:
case Types.NVARCHAR:
case Types.LONGVARCHAR:
case Types.LONGNVARCHAR:
case Types.TIME:
return ConnectorType.of("STRING");
default:
return ConnectorType.of("UNSUPPORTED");
}
Expand Down
Loading
Loading