From 8faa3888851a3406dc5d6fb0f761920b484c1282 Mon Sep 17 00:00:00 2001 From: Artem Ermoshkin Date: Sat, 12 Sep 2026 15:59:14 +0300 Subject: [PATCH 1/5] YDBAPPTEAM-1843 implement ODBC catalog metadata --- odbc/src/odbc_driver.cpp | 35 +- odbc/src/statement.h | 48 ++- odbc/src/statement_metadata.cpp | 384 ++++++++++++++++---- odbc/src/utils/sql_like.h | 37 +- odbc/tests/integration/core_api_it.cpp | 7 +- odbc/tests/integration/metadata_api_it.cpp | 400 ++++++++++++++++++++- odbc/tests/unit/sql_like_ut.cpp | 13 + 7 files changed, 804 insertions(+), 120 deletions(-) diff --git a/odbc/src/odbc_driver.cpp b/odbc/src/odbc_driver.cpp index 526e58d622..4ee38f5872 100644 --- a/odbc/src/odbc_driver.cpp +++ b/odbc/src/odbc_driver.cpp @@ -9,6 +9,8 @@ #include "odbc_compat.h" +#include + namespace { namespace Odbc = NYdb::NOdbc; using Odbc::TConnection; @@ -30,6 +32,18 @@ namespace { } }; + template + struct TLazyOptionalText { + Char* Value; + SQLINTEGER Length; + std::optional Resolve() const { + if (!Value) { + return std::nullopt; + } + return Odbc::GetString(Value, Length); + } + }; + template decltype(auto) Resolve(T& value) { if constexpr (requires { value.Resolve(); }) { @@ -44,6 +58,11 @@ namespace { return {value, length}; } + template + TLazyOptionalText OptionalText(Char* value, SQLINTEGER length) { + return {value, length}; + } + template SQLRETURN Forward(SQLHANDLE handle, Args&&... args) { return Call(handle, [&](Handle* value) { @@ -287,15 +306,15 @@ ODBC_FORWARD(SQLColumns, TStatement, TStatement::Columns, (SQLHSTMT statementHandle, SQLCHAR* catalogName, SQLSMALLINT nameLength1, SQLCHAR* schemaName, SQLSMALLINT nameLength2, SQLCHAR* tableName, SQLSMALLINT nameLength3, SQLCHAR* columnName, SQLSMALLINT nameLength4), - (statementHandle, Text(catalogName, nameLength1), Text(schemaName, nameLength2), - Text(tableName, nameLength3), Text(columnName, nameLength4))) + (statementHandle, OptionalText(catalogName, nameLength1), OptionalText(schemaName, nameLength2), + OptionalText(tableName, nameLength3), OptionalText(columnName, nameLength4))) ODBC_FORWARD(SQLTables, TStatement, TStatement::Tables, (SQLHSTMT statementHandle, SQLCHAR* catalogName, SQLSMALLINT nameLength1, SQLCHAR* schemaName, SQLSMALLINT nameLength2, SQLCHAR* tableName, SQLSMALLINT nameLength3, SQLCHAR* tableType, SQLSMALLINT nameLength4), - (statementHandle, Text(catalogName, nameLength1), Text(schemaName, nameLength2), - Text(tableName, nameLength3), Text(tableType, nameLength4))) + (statementHandle, OptionalText(catalogName, nameLength1), OptionalText(schemaName, nameLength2), + OptionalText(tableName, nameLength3), OptionalText(tableType, nameLength4))) ODBC_FORWARD(SQLCloseCursor, TStatement, TStatement::Close, (SQLHSTMT statementHandle), (statementHandle, false)) @@ -371,8 +390,8 @@ ODBC_FORWARD(SQLStatistics, TStatement, TStatement::Statistics, (SQLHSTMT statementHandle, SQLCHAR* catalogName, SQLSMALLINT nameLength1, SQLCHAR* schemaName, SQLSMALLINT nameLength2, SQLCHAR* tableName, SQLSMALLINT nameLength3, SQLUSMALLINT unique, SQLUSMALLINT reserved), - (statementHandle, Text(catalogName, nameLength1), Text(schemaName, nameLength2), - Text(tableName, nameLength3), unique, reserved)) + (statementHandle, OptionalText(catalogName, nameLength1), OptionalText(schemaName, nameLength2), + OptionalText(tableName, nameLength3), unique, reserved)) ODBC_FORWARD(SQLSpecialColumns, TStatement, TStatement::SpecialColumns, (SQLHSTMT statementHandle, SQLUSMALLINT identifierType, SQLCHAR* catalogName, @@ -424,8 +443,8 @@ ODBC_FORWARD(SQLPrimaryKeys, TStatement, TStatement::PrimaryKeys, (SQLHSTMT statementHandle, SQLCHAR* catalogName, SQLSMALLINT nameLength1, SQLCHAR* schemaName, SQLSMALLINT nameLength2, SQLCHAR* tableName, SQLSMALLINT nameLength3), - (statementHandle, Text(catalogName, nameLength1), Text(schemaName, nameLength2), - Text(tableName, nameLength3))) + (statementHandle, OptionalText(catalogName, nameLength1), OptionalText(schemaName, nameLength2), + OptionalText(tableName, nameLength3))) ODBC_FORWARD(SQLForeignKeys, TStatement, TStatement::ForeignKeys, (SQLHSTMT statementHandle, SQLCHAR* pkCatalogName, SQLSMALLINT nameLength1, diff --git a/odbc/src/statement.h b/odbc/src/statement.h index c469e638d1..a4e2848c21 100644 --- a/odbc/src/statement.h +++ b/odbc/src/statement.h @@ -39,20 +39,20 @@ class TStatement : public TErrorManager { SQLRETURN BindCol(SQLUSMALLINT columnNumber, SQLSMALLINT targetType, SQLPOINTER targetValue, SQLLEN bufferLength, SQLLEN* strLenOrInd); SQLRETURN BindParameter(SQLUSMALLINT paramNumber, SQLSMALLINT inputOutputType, SQLSMALLINT valueType, SQLSMALLINT parameterType, SQLULEN columnSize, SQLSMALLINT decimalDigits, SQLPOINTER parameterValuePtr, SQLLEN bufferLength, SQLLEN* strLenOrIndPtr); - SQLRETURN Columns(const std::string& catalogName, - const std::string& schemaName, - const std::string& tableName, - const std::string& columnName); + SQLRETURN Columns(const std::optional& catalogName, + const std::optional& schemaName, + const std::optional& tableName, + const std::optional& columnName); - SQLRETURN Tables(const std::string& catalogName, - const std::string& schemaName, - const std::string& tableName, - const std::string& tableType); + SQLRETURN Tables(const std::optional& catalogName, + const std::optional& schemaName, + const std::optional& tableName, + const std::optional& tableType); SQLRETURN GetTypeInfo(SQLSMALLINT dataType); - SQLRETURN Statistics(const std::string& catalogName, - const std::string& schemaName, - const std::string& tableName, + SQLRETURN Statistics(const std::optional& catalogName, + const std::optional& schemaName, + const std::optional& tableName, SQLUSMALLINT unique, SQLUSMALLINT accuracy); SQLRETURN SpecialColumns(const std::string& catalogName, @@ -60,9 +60,9 @@ class TStatement : public TErrorManager { const std::string& tableName, SQLUSMALLINT identifierType, SQLUSMALLINT scope); - SQLRETURN PrimaryKeys(const std::string& catalogName, - const std::string& schemaName, - const std::string& tableName); + SQLRETURN PrimaryKeys(const std::optional& catalogName, + const std::optional& schemaName, + const std::optional& tableName); SQLRETURN ForeignKeys(const std::string& pkCatalogName, const std::string& pkSchemaName, const std::string& pkTableName, @@ -165,9 +165,12 @@ class TStatement : public TErrorManager { std::optional ResolveDescriptorAttribute(SQLINTEGER attr); SQLUSMALLINT FindNextNeedDataParam() const; - std::string GetTraversalRoot(const std::string& pattern) const; + std::string QualifyMetadataTableName(const std::string& tableName) const; std::string GetMetadataTableName(const std::string& path) const; - bool MetadataNamespaceMatches(const std::string& catalog, const std::string& schema) const; + bool MetadataNamespaceMatches(const std::optional& catalog, + const std::optional& schema, + bool catalogPatternsAllowed, + bool schemaPatternsAllowed) const; NQuery::TExecuteQueryResult ExecuteQuery( NQuery::TSession& session, @@ -175,9 +178,16 @@ class TStatement : public TErrorManager { SQLULEN paramSet); NYdb::NRetry::TRetryOperationSettings MakeAutocommitRetrySettings(); - std::vector GetPatternEntries(const std::string& pattern); - SQLRETURN VisitEntry(const std::string& path, const std::string& pattern, std::vector& resultEntries); - bool IsPatternMatch(const std::string& path, const std::string& pattern); + std::vector GetMetadataEntries( + const std::optional& tableName, + bool patternsAllowed); + SQLRETURN VisitEntry(const std::string& path, + const std::string& tableName, + bool patternsAllowed, + std::vector& resultEntries); + bool IsMetadataTableMatch(const std::string& path, + const std::string& tableName, + bool patternsAllowed); std::optional GetTableType(NScheme::ESchemeEntryType type); }; diff --git a/odbc/src/statement_metadata.cpp b/odbc/src/statement_metadata.cpp index 6e6050e077..7aa1ed7c07 100644 --- a/odbc/src/statement_metadata.cpp +++ b/odbc/src/statement_metadata.cpp @@ -6,9 +6,12 @@ #include "utils/util.h" #include +#include #include +#include #include #include +#include namespace NYdb::NOdbc { namespace { @@ -83,7 +86,7 @@ const TColumnMeta kStatisticsSchema[] = { V("TABLE_CAT"), V("TABLE_SCHEM"), V("TABLE_NAME", 128, SQL_NO_NULLS), - N("NON_UNIQUE", SQL_SMALLINT, SQL_NO_NULLS), + N("NON_UNIQUE", SQL_SMALLINT), V("INDEX_QUALIFIER"), V("INDEX_NAME"), N("TYPE", SQL_SMALLINT, SQL_NO_NULLS), @@ -95,6 +98,28 @@ const TColumnMeta kStatisticsSchema[] = { V("FILTER_CONDITION"), }; +using TSchemeEntryType = NScheme::ESchemeEntryType; + +constexpr std::array kTableTypes{ + std::pair{TSchemeEntryType::Table, std::string_view{"TABLE"}}, + std::pair{TSchemeEntryType::View, std::string_view{"VIEW"}}, + std::pair{TSchemeEntryType::ColumnStore, std::string_view{"COLUMN_STORE"}}, + std::pair{TSchemeEntryType::ColumnTable, std::string_view{"COLUMN_TABLE"}}, + std::pair{TSchemeEntryType::Sequence, std::string_view{"SEQUENCE"}}, + std::pair{TSchemeEntryType::Replication, std::string_view{"REPLICATION"}}, + std::pair{TSchemeEntryType::Topic, std::string_view{"TOPIC"}}, + std::pair{TSchemeEntryType::ExternalTable, std::string_view{"EXTERNAL_TABLE"}}, + std::pair{TSchemeEntryType::ExternalDataSource, std::string_view{"EXTERNAL_DATA_SOURCE"}}, + std::pair{TSchemeEntryType::ResourcePool, std::string_view{"RESOURCE_POOL"}}, + std::pair{TSchemeEntryType::PqGroup, std::string_view{"PQ_GROUP"}}, + std::pair{TSchemeEntryType::RtmrVolume, std::string_view{"RTMR_VOLUME"}}, + std::pair{TSchemeEntryType::BlockStoreVolume, std::string_view{"BLOCK_STORE_VOLUME"}}, + std::pair{TSchemeEntryType::CoordinationNode, std::string_view{"COORDINATION_NODE"}}, + std::pair{TSchemeEntryType::Unknown, std::string_view{"UNKNOWN"}}, + std::pair{TSchemeEntryType::SysView, std::string_view{"SYSTEM VIEW"}}, + std::pair{TSchemeEntryType::Transfer, std::string_view{"TRANSFER"}}, +}; + const TColumnMeta kSpecialColumnsSchema[] = { N("SCOPE", SQL_SMALLINT), V("COLUMN_NAME", 128, SQL_NO_NULLS), @@ -157,6 +182,72 @@ TOdbcScalar Maybe(const std::optional& value) { return value ? I(*value) : Null(); } +TOdbcScalar Int32OrNull(uint64_t value) { + return value <= static_cast(std::numeric_limits::max()) + ? I(static_cast(value)) + : Null(); +} + +bool IsExplicitEmpty(const std::optional& value) { + return value && value->empty(); +} + +bool IsSpecialValue(const std::optional& value, std::string_view special) { + return value && *value == special; +} + +bool IsDateTimeType(SQLSMALLINT type) { + return type == SQL_TYPE_DATE || type == SQL_TYPE_TIME || type == SQL_TYPE_TIMESTAMP; +} + +TOdbcScalar GetSqlDataType(SQLSMALLINT type) { + return I(IsDateTimeType(type) ? SQL_DATETIME : type); +} + +TOdbcScalar GetDateTimeSub(SQLSMALLINT type) { + switch (type) { + case SQL_TYPE_DATE: return I(SQL_CODE_DATE); + case SQL_TYPE_TIME: return I(SQL_CODE_TIME); + case SQL_TYPE_TIMESTAMP: return I(SQL_CODE_TIMESTAMP); + default: return Null(); + } +} + +TOdbcScalar GetBufferLength(const TYdbTypeInfo& type) { + switch (type.SqlType) { + case SQL_BIT: + case SQL_TINYINT: return I(sizeof(SQLCHAR)); + case SQL_SMALLINT: return I(sizeof(SQLSMALLINT)); + case SQL_INTEGER: + case SQL_REAL: return I(sizeof(SQLINTEGER)); + case SQL_BIGINT: + case SQL_DOUBLE: return I(sizeof(SQLBIGINT)); + case SQL_TYPE_DATE: return I(sizeof(SQL_DATE_STRUCT)); + case SQL_TYPE_TIME: return I(sizeof(SQL_TIME_STRUCT)); + case SQL_TYPE_TIMESTAMP: return I(sizeof(SQL_TIMESTAMP_STRUCT)); + case SQL_GUID: return I(sizeof(SQLGUID)); + default: + return type.ColumnSize ? I(static_cast(type.ColumnSize)) : Null(); + } +} + +TOdbcScalar GetCharOctetLength(const TYdbTypeInfo& type) { + switch (type.SqlType) { + case SQL_CHAR: + case SQL_VARCHAR: + case SQL_LONGVARCHAR: + case SQL_WCHAR: + case SQL_WVARCHAR: + case SQL_WLONGVARCHAR: + case SQL_BINARY: + case SQL_VARBINARY: + case SQL_LONGVARBINARY: + return type.ColumnSize ? I(static_cast(type.ColumnSize)) : Null(); + default: + return Null(); + } +} + std::string GetMetadataCatalogName(TConnection* connection) { std::string catalog = connection->GetCatalogBinding().Catalog; // TABLE_CAT is an identifier. The leading slash belongs to YDB's absolute @@ -168,14 +259,17 @@ std::string GetMetadataCatalogName(TConnection* connection) { } template -void DescribeTable(TConnection* connection, const std::string& path, Visitor&& visitor) { +void DescribeTable(TConnection* connection, const std::string& path, + bool withTableStatistics, Visitor&& visitor) { auto client = connection->GetTableClient(); if (!client) { throw TOdbcException("HY000", 0, "No client connection"); } auto status = client->RetryOperationSync( - [path, &visitor](NTable::TSession session) -> TStatus { - auto result = session.DescribeTable(path).ExtractValueSync(); + [path, withTableStatistics, &visitor](NTable::TSession session) -> TStatus { + auto settings = NTable::TDescribeTableSettings() + .WithTableStatistics(withTableStatistics); + auto result = session.DescribeTable(path, settings).ExtractValueSync(); NStatusHelpers::ThrowOnError(result); visitor(result.GetTableDescription()); return TStatus(EStatus::SUCCESS, {}); @@ -183,6 +277,11 @@ void DescribeTable(TConnection* connection, const std::string& path, Visitor&& v NStatusHelpers::ThrowOnError(status); } +template +void DescribeTable(TConnection* connection, const std::string& path, Visitor&& visitor) { + DescribeTable(connection, path, false, std::forward(visitor)); +} + bool MatchesTableTypeFilter(std::string_view filter, std::string_view entryType) { if (filter.empty()) { return true; @@ -238,17 +337,24 @@ TTable BuildTypeInfoRows(SQLSMALLINT dataType) { } // namespace -SQLRETURN TStatement::Columns(const std::string& catalogName, const std::string& schemaName, - const std::string& tableName, const std::string& columnName) { +SQLRETURN TStatement::Columns(const std::optional& catalogName, + const std::optional& schemaName, + const std::optional& tableName, + const std::optional& columnName) { + if (Attributes_.GetMetadataId() == SQL_TRUE + && (!catalogName || !schemaName || !tableName || !columnName)) { + return AddError("HY009", 0, "Identifier arguments must not be null"); + } ResetForMetadata(); - if (!MetadataNamespaceMatches(catalogName, schemaName)) { + const bool patternsAllowed = Attributes_.GetMetadataId() != SQL_TRUE; + if (!MetadataNamespaceMatches(catalogName, schemaName, false, patternsAllowed)) { SetCursor(CreateVirtualCursor(kColumnsSchema)); return SQL_SUCCESS; } const std::string catalog = GetMetadataCatalogName(Conn_); TTable table; - for (const auto& entry : GetPatternEntries(tableName)) { + for (const auto& entry : GetMetadataEntries(tableName, patternsAllowed)) { if (entry.Type != NScheme::ESchemeEntryType::Table && entry.Type != NScheme::ESchemeEntryType::ColumnTable) { continue; @@ -258,9 +364,9 @@ SQLRETURN TStatement::Columns(const std::string& catalogName, const std::string& const auto& primaryKeyColumns = description.GetPrimaryKeyColumns(); for (size_t index = 0; index < columns.size(); ++index) { const auto& column = columns[index]; - const bool matches = columnName.empty() - || (Attributes_.GetMetadataId() == SQL_TRUE ? column.Name == columnName - : SqlLikeMatch(column.Name, columnName)); + const bool matches = !columnName + || (patternsAllowed ? SqlLikeMatch(column.Name, *columnName) + : column.Name == *columnName); if (!matches) { continue; } @@ -272,9 +378,11 @@ SQLRETURN TStatement::Columns(const std::string& catalogName, const std::string& || std::ranges::find(primaryKeyColumns, column.Name) != primaryKeyColumns.end(); table.push_back({ catalog, Null(), GetMetadataTableName(entry.Name), column.Name, I(type.SqlType), - type.TypeName, size, size, Maybe(type.DecimalDigits), Maybe(type.Radix), - I(notNull ? SQL_NO_NULLS : SQL_NULLABLE), Null(), Null(), I(type.SqlType), Null(), - size, I(static_cast(index + 1)), std::string(notNull ? "NO" : "YES"), + type.TypeName, size, GetBufferLength(type), Maybe(type.DecimalDigits), + Maybe(type.Radix), I(notNull ? SQL_NO_NULLS : SQL_NULLABLE), Null(), Null(), + GetSqlDataType(type.SqlType), GetDateTimeSub(type.SqlType), + GetCharOctetLength(type), I(static_cast(index + 1)), + std::string(notNull ? "NO" : "YES"), }); } }); @@ -283,19 +391,67 @@ SQLRETURN TStatement::Columns(const std::string& catalogName, const std::string& return SQL_SUCCESS; } -SQLRETURN TStatement::Tables(const std::string& catalogName, const std::string& schemaName, - const std::string& tableName, const std::string& tableType) { +SQLRETURN TStatement::Tables(const std::optional& catalogName, + const std::optional& schemaName, + const std::optional& tableName, + const std::optional& tableType) { + if (Attributes_.GetMetadataId() == SQL_TRUE + && (!catalogName || !schemaName || !tableName)) { + return AddError("HY009", 0, "Identifier arguments must not be null"); + } ResetForMetadata(); - if (!MetadataNamespaceMatches(catalogName, schemaName)) { + + const bool emptySchema = IsExplicitEmpty(schemaName); + const bool emptyTable = IsExplicitEmpty(tableName); + const bool emptyType = IsExplicitEmpty(tableType); + if (IsSpecialValue(catalogName, SQL_ALL_CATALOGS) + && emptySchema && emptyTable && emptyType) { + SetCursor(CreateVirtualCursor(kTablesSchema, {{ + GetMetadataCatalogName(Conn_), Null(), Null(), Null(), Null(), + }})); + return SQL_SUCCESS; + } + if (IsSpecialValue(schemaName, SQL_ALL_SCHEMAS) + && IsExplicitEmpty(catalogName) && emptyTable && emptyType) { + SetCursor(CreateVirtualCursor(kTablesSchema)); + return SQL_SUCCESS; + } + if (IsSpecialValue(tableType, SQL_ALL_TABLE_TYPES) + && IsExplicitEmpty(catalogName) && emptySchema && emptyTable) { + std::vector types; + types.reserve(kTableTypes.size()); + for (const auto& [_, type] : kTableTypes) { + types.push_back(type); + } + std::ranges::sort(types); + types.erase(std::unique(types.begin(), types.end()), types.end()); + TTable table; + table.reserve(types.size()); + for (const std::string_view type : types) { + table.push_back({Null(), Null(), Null(), std::string(type), Null()}); + } + SetCursor(CreateVirtualCursor(kTablesSchema, std::move(table))); + return SQL_SUCCESS; + } + + const bool patternsAllowed = Attributes_.GetMetadataId() != SQL_TRUE; + if (!MetadataNamespaceMatches(catalogName, schemaName, + patternsAllowed, patternsAllowed)) { SetCursor(CreateVirtualCursor(kTablesSchema)); return SQL_SUCCESS; } const std::string catalog = GetMetadataCatalogName(Conn_); TTable table; - for (const auto& entry : GetPatternEntries(tableName)) { + auto entries = GetMetadataEntries(tableName, patternsAllowed); + std::ranges::sort(entries, [this](const auto& lhs, const auto& rhs) { + return std::pair{GetTableType(lhs.Type).value_or(""), GetMetadataTableName(lhs.Name)} + < std::pair{GetTableType(rhs.Type).value_or(""), GetMetadataTableName(rhs.Name)}; + }); + const std::string_view typeFilter = tableType ? std::string_view(*tableType) : std::string_view{}; + for (const auto& entry : entries) { const auto type = GetTableType(entry.Type); - if (type && MatchesTableTypeFilter(tableType, *type)) { + if (type && MatchesTableTypeFilter(typeFilter, *type)) { table.push_back({catalog, Null(), GetMetadataTableName(entry.Name), *type, Null()}); } } @@ -309,10 +465,70 @@ SQLRETURN TStatement::GetTypeInfo(SQLSMALLINT dataType) { return SQL_SUCCESS; } -SQLRETURN TStatement::Statistics(const std::string&, const std::string&, const std::string&, - SQLUSMALLINT, SQLUSMALLINT) { +SQLRETURN TStatement::Statistics(const std::optional& catalogName, + const std::optional& schemaName, + const std::optional& tableName, + SQLUSMALLINT unique, SQLUSMALLINT accuracy) { + if (unique != SQL_INDEX_UNIQUE && unique != SQL_INDEX_ALL) { + return AddError("HY100", 0, "Invalid index uniqueness option"); + } + if (accuracy != SQL_QUICK && accuracy != SQL_ENSURE) { + return AddError("HY101", 0, "Invalid statistics accuracy option"); + } + if (!tableName) { + return AddError("HY009", 0, "TableName must not be null"); + } + if (Attributes_.GetMetadataId() == SQL_TRUE && (!catalogName || !schemaName)) { + return AddError("HY009", 0, "Identifier arguments must not be null"); + } ResetForMetadata(); - SetCursor(CreateVirtualCursor(kStatisticsSchema)); + if (!MetadataNamespaceMatches(catalogName, schemaName, false, false) + || tableName->empty()) { + SetCursor(CreateVirtualCursor(kStatisticsSchema)); + return SQL_SUCCESS; + } + + auto entries = GetMetadataEntries(tableName, false); + TTable table; + if (entries.size() == 1 + && (entries.front().Type == NScheme::ESchemeEntryType::Table + || entries.front().Type == NScheme::ESchemeEntryType::ColumnTable)) { + const auto& entry = entries.front(); + const std::string catalog = GetMetadataCatalogName(Conn_); + const std::string tableNameResult = GetMetadataTableName(entry.Name); + const bool withTableStatistics = accuracy == SQL_ENSURE; + DescribeTable(Conn_, entry.Name, withTableStatistics, [&](const auto& description) { + table.push_back({ + catalog, Null(), tableNameResult, Null(), Null(), Null(), I(SQL_TABLE_STAT), + Null(), Null(), Null(), + withTableStatistics ? Int32OrNull(description.GetTableRows()) : Null(), + Null(), Null(), + }); + + auto indexes = description.GetIndexDescriptions(); + std::ranges::sort(indexes, [](const auto& lhs, const auto& rhs) { + const bool lhsUnique = lhs.GetIndexType() == NTable::EIndexType::GlobalUnique; + const bool rhsUnique = rhs.GetIndexType() == NTable::EIndexType::GlobalUnique; + return std::pair{!lhsUnique, lhs.GetIndexName()} + < std::pair{!rhsUnique, rhs.GetIndexName()}; + }); + for (const auto& index : indexes) { + const bool isUnique = index.GetIndexType() == NTable::EIndexType::GlobalUnique; + if (unique == SQL_INDEX_UNIQUE && !isUnique) { + continue; + } + SQLSMALLINT ordinal = 1; + for (const auto& column : index.GetIndexColumns()) { + table.push_back({ + catalog, Null(), tableNameResult, I(isUnique ? SQL_FALSE : SQL_TRUE), + Null(), index.GetIndexName(), I(SQL_INDEX_OTHER), I(ordinal++), column, + Null(), Null(), Null(), Null(), + }); + } + } + }); + } + SetCursor(CreateVirtualCursor(kStatisticsSchema, std::move(table))); return SQL_SUCCESS; } @@ -323,12 +539,16 @@ SQLRETURN TStatement::SpecialColumns(const std::string& catalogName, const std:: return AddError("HYC00", 0, "Optional feature not implemented"); } ResetForMetadata(); - if (!MetadataNamespaceMatches(catalogName, schemaName)) { + const std::optional catalog = catalogName.empty() + ? std::nullopt : std::optional{catalogName}; + const std::optional schema = schemaName.empty() + ? std::nullopt : std::optional{schemaName}; + if (!MetadataNamespaceMatches(catalog, schema, false, false)) { SetCursor(CreateVirtualCursor(kSpecialColumnsSchema)); return SQL_SUCCESS; } - auto entries = GetPatternEntries(tableName); + auto entries = GetMetadataEntries(std::optional{tableName}, false); if (entries.size() > 1) { throw TOdbcException("HY000", 0, "Ambiguous table name"); } @@ -354,18 +574,23 @@ SQLRETURN TStatement::SpecialColumns(const std::string& catalogName, const std:: return SQL_SUCCESS; } -SQLRETURN TStatement::PrimaryKeys(const std::string& catalogName, const std::string& schemaName, - const std::string& tableName) { +SQLRETURN TStatement::PrimaryKeys(const std::optional& catalogName, + const std::optional& schemaName, + const std::optional& tableName) { + if (!tableName) { + return AddError("HY009", 0, "TableName must not be null"); + } + if (Attributes_.GetMetadataId() == SQL_TRUE && (!catalogName || !schemaName)) { + return AddError("HY009", 0, "Identifier arguments must not be null"); + } ResetForMetadata(); - if (!MetadataNamespaceMatches(catalogName, schemaName)) { + if (!MetadataNamespaceMatches(catalogName, schemaName, false, false) + || tableName->empty()) { SetCursor(CreateVirtualCursor(kPrimaryKeysSchema)); return SQL_SUCCESS; } - auto entries = GetPatternEntries(tableName); - if (entries.size() > 1) { - throw TOdbcException("HY000", 0, "Ambiguous table name"); - } + auto entries = GetMetadataEntries(tableName, false); TTable table; if (!entries.empty()) { const std::string catalog = GetMetadataCatalogName(Conn_); @@ -395,19 +620,39 @@ SQLRETURN TStatement::ColumnPrivileges(const std::string&, const std::string&, return SQL_SUCCESS; } -std::string TStatement::GetTraversalRoot(const std::string& pattern) const { - const size_t slash = pattern.rfind('/', pattern.find_first_of("%_")); - return slash == std::string::npos ? "" : pattern.substr(0, slash); +std::string TStatement::QualifyMetadataTableName(const std::string& tableName) const { + if (!tableName.empty() && tableName.front() == '/') { + return tableName; + } + const std::string catalog = Conn_->GetCatalogBinding().Catalog; + if (catalog.empty() || catalog == "/") { + return "/" + tableName; + } + return catalog + "/" + tableName; } -std::vector TStatement::GetPatternEntries(const std::string& pattern) { +std::vector TStatement::GetMetadataEntries( + const std::optional& tableName, + bool patternsAllowed) { + std::vector entries; + if (tableName && tableName->empty()) { + return entries; + } + const std::string catalog = Conn_->GetCatalogBinding().Catalog; - std::string searchPattern = pattern; - if (!pattern.empty() && pattern.front() != '/' && pattern.find('/') == std::string::npos) { - searchPattern = catalog + (catalog.empty() || catalog.back() != '/' ? "/" : "") + pattern; + const std::string qualifiedName = tableName ? QualifyMetadataTableName(*tableName) : std::string{}; + if (tableName) { + const bool insideCatalog = catalog == "/" + ? qualifiedName.starts_with('/') + : qualifiedName == catalog || qualifiedName.starts_with(catalog + "/"); + if (!insideCatalog) { + return entries; + } } - std::vector entries; - VisitEntry(pattern.empty() ? catalog : GetTraversalRoot(searchPattern), searchPattern, entries); + + VisitEntry(catalog.empty() ? "/" : catalog, qualifiedName, + tableName ? patternsAllowed : true, entries); + std::ranges::sort(entries, {}, &NScheme::TSchemeEntry::Name); return entries; } @@ -420,32 +665,44 @@ std::string TStatement::GetMetadataTableName(const std::string& path) const { return path.starts_with(prefix) ? path.substr(prefix.size()) : path; } -bool TStatement::MetadataNamespaceMatches(const std::string& catalog, const std::string& schema) const { - const auto matches = [&](const std::string& value, const std::string& pattern) { - return pattern.empty() || (Attributes_.GetMetadataId() == SQL_TRUE - ? value == pattern : SqlLikeMatch(value, pattern)); +bool TStatement::MetadataNamespaceMatches( + const std::optional& catalog, + const std::optional& schema, + bool catalogPatternsAllowed, + bool schemaPatternsAllowed) const { + const auto matches = [](std::string_view value, std::string_view argument, + bool patternsAllowed) { + return patternsAllowed ? SqlLikeMatch(value, argument) : value == argument; }; - std::string normalizedCatalog = catalog; - if (!normalizedCatalog.empty() && normalizedCatalog.front() != '/') { - normalizedCatalog.insert(normalizedCatalog.begin(), '/'); + + if (catalog) { + std::string catalogArgument = *catalog; + if (catalogArgument.starts_with('/')) { + catalogArgument.erase(0, 1); + } + if (!matches(GetMetadataCatalogName(Conn_), catalogArgument, + catalogPatternsAllowed)) { + return false; + } } - return matches(Conn_->GetCatalogBinding().Catalog, normalizedCatalog) && matches("", schema); + return !schema || matches("", *schema, schemaPatternsAllowed); } -SQLRETURN TStatement::VisitEntry(const std::string& path, const std::string& pattern, +SQLRETURN TStatement::VisitEntry(const std::string& path, const std::string& tableName, + bool patternsAllowed, std::vector& result) { auto client = Conn_->GetSchemeClient(); if (!client) { throw TOdbcException("HY000", 0, "No client connection"); } - auto listing = client->ListDirectory(path + "/").ExtractValueSync(); + auto listing = client->ListDirectory(path).ExtractValueSync(); NStatusHelpers::ThrowOnError(listing); for (const auto& entry : listing.GetChildren()) { - const std::string fullPath = path + "/" + entry.Name; + const std::string fullPath = path == "/" ? path + entry.Name : path + "/" + entry.Name; if (entry.Type == NScheme::ESchemeEntryType::Directory || entry.Type == NScheme::ESchemeEntryType::SubDomain) { - VisitEntry(fullPath, pattern, result); - } else if (IsPatternMatch(fullPath, pattern)) { + VisitEntry(fullPath, tableName, patternsAllowed, result); + } else if (IsMetadataTableMatch(fullPath, tableName, patternsAllowed)) { result.push_back(entry); result.back().Name = fullPath; } @@ -453,24 +710,15 @@ SQLRETURN TStatement::VisitEntry(const std::string& path, const std::string& pat return SQL_SUCCESS; } -bool TStatement::IsPatternMatch(const std::string& path, const std::string& pattern) { - return pattern.empty() || (Attributes_.GetMetadataId() == SQL_TRUE - ? path == pattern : SqlLikeMatch(path, pattern)); +bool TStatement::IsMetadataTableMatch(const std::string& path, + const std::string& tableName, + bool patternsAllowed) { + return tableName.empty() || (patternsAllowed ? SqlLikeMatch(path, tableName) + : path == tableName); } std::optional TStatement::GetTableType(NScheme::ESchemeEntryType type) { - using E = NScheme::ESchemeEntryType; - static constexpr std::pair types[] = { - {E::Table, "TABLE"}, {E::View, "VIEW"}, {E::ColumnStore, "COLUMN_STORE"}, - {E::ColumnTable, "COLUMN_TABLE"}, {E::Sequence, "SEQUENCE"}, - {E::Replication, "REPLICATION"}, {E::Topic, "TOPIC"}, - {E::ExternalTable, "EXTERNAL_TABLE"}, {E::ExternalDataSource, "EXTERNAL_DATA_SOURCE"}, - {E::ResourcePool, "RESOURCE_POOL"}, {E::PqGroup, "PQ_GROUP"}, - {E::RtmrVolume, "RTMR_VOLUME"}, {E::BlockStoreVolume, "BLOCK_STORE_VOLUME"}, - {E::CoordinationNode, "COORDINATION_NODE"}, {E::Unknown, "UNKNOWN"}, - {E::SysView, "SYSTEM VIEW"}, {E::Transfer, "TRANSFER"}, - }; - for (const auto& [entryType, name] : types) { + for (const auto& [entryType, name] : kTableTypes) { if (entryType == type) { return std::string(name); } diff --git a/odbc/src/utils/sql_like.h b/odbc/src/utils/sql_like.h index f51c10ca28..9bd98e1e2b 100644 --- a/odbc/src/utils/sql_like.h +++ b/odbc/src/utils/sql_like.h @@ -8,32 +8,37 @@ namespace NYdb::NOdbc { inline bool SqlLikeMatch(std::string_view text, std::string_view pattern) { size_t textPos = 0; size_t patPos = 0; - size_t lastPercentPat = std::string_view::npos; - size_t textStartAfterPercent = 0; + size_t retryPat = std::string_view::npos; + size_t retryText = 0; const size_t textLen = text.size(); const size_t patLen = pattern.size(); while (textPos < textLen) { - const bool morePat = patPos < patLen; - const char patCh = morePat ? pattern[patPos] : '\0'; - - if (morePat && patCh != '%' && (patCh == '_' || patCh == text[textPos])) { - ++textPos; - ++patPos; + if (patPos < patLen && pattern[patPos] == '%') { + retryPat = ++patPos; + retryText = textPos; continue; } - if (morePat && patCh == '%') { - lastPercentPat = patPos++; - textStartAfterPercent = textPos; - continue; + if (patPos < patLen) { + size_t nextPat = patPos + 1; + char expected = pattern[patPos]; + bool anyCharacter = expected == '_'; + if (expected == '\\' && nextPat < patLen) { + expected = pattern[nextPat++]; + anyCharacter = false; + } + if (anyCharacter || expected == text[textPos]) { + ++textPos; + patPos = nextPat; + continue; + } } - if (lastPercentPat != std::string_view::npos) { - patPos = lastPercentPat + 1; - ++textStartAfterPercent; - textPos = textStartAfterPercent; + if (retryPat != std::string_view::npos) { + patPos = retryPat; + textPos = ++retryText; continue; } diff --git a/odbc/tests/integration/core_api_it.cpp b/odbc/tests/integration/core_api_it.cpp index ff46824e72..d62692295f 100644 --- a/odbc/tests/integration/core_api_it.cpp +++ b/odbc/tests/integration/core_api_it.cpp @@ -314,13 +314,15 @@ TEST(CoreApi, SQLSetGetCursorName) { SQLFreeHandle(SQL_HANDLE_ENV, env); } -TEST(CoreApi, SQLStatisticsEmpty) { +TEST(CoreApi, SQLStatisticsSchemaForMissingTable) { SQLHENV env; SQLHDBC dbc; SQLHSTMT stmt; AllocEnvAndConnect(&env, &dbc); ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); - CHECK_ODBC_OK(SQLStatistics(stmt, nullptr, 0, nullptr, 0, (SQLCHAR*)"%", SQL_NTS, SQL_INDEX_ALL, SQL_ENSURE), + CHECK_ODBC_OK(SQLStatistics(stmt, nullptr, 0, nullptr, 0, + (SQLCHAR*)"missing_statistics_table", SQL_NTS, + SQL_INDEX_ALL, SQL_QUICK), stmt, SQL_HANDLE_STMT); SQLCHAR columnName[32] = {}; SQLSMALLINT nameLength = 0; @@ -333,6 +335,7 @@ TEST(CoreApi, SQLStatisticsEmpty) { stmt, SQL_HANDLE_STMT); EXPECT_STREQ(reinterpret_cast(columnName), "NON_UNIQUE"); EXPECT_EQ(dataType, SQL_SMALLINT); + EXPECT_EQ(nullable, SQL_NULLABLE); ASSERT_EQ(SQLFetch(stmt), SQL_NO_DATA); SQLFreeHandle(SQL_HANDLE_STMT, stmt); SQLDisconnect(dbc); diff --git a/odbc/tests/integration/metadata_api_it.cpp b/odbc/tests/integration/metadata_api_it.cpp index e82f5d284c..c0b9c8715f 100644 --- a/odbc/tests/integration/metadata_api_it.cpp +++ b/odbc/tests/integration/metadata_api_it.cpp @@ -1,9 +1,53 @@ #include "test_utils.h" +#include +#include +#include +#include + #ifndef SQL_ATTR_METADATA_ID #define SQL_ATTR_METADATA_ID 10029 #endif +namespace { + +void ExecuteAndClose(SQLHSTMT stmt, const char* sql) { + CHECK_ODBC_OK(SQLExecDirect( + stmt, reinterpret_cast(const_cast(sql)), SQL_NTS), + stmt, SQL_HANDLE_STMT); + ASSERT_EQ(SQLFreeStmt(stmt, SQL_CLOSE), SQL_SUCCESS); +} + +std::optional ReadText(SQLHSTMT stmt, SQLUSMALLINT column) { + char value[512] = {}; + SQLLEN indicator = 0; + const SQLRETURN rc = SQLGetData( + stmt, column, SQL_C_CHAR, value, sizeof(value), &indicator); + if (rc != SQL_SUCCESS && rc != SQL_SUCCESS_WITH_INFO) { + ADD_FAILURE() << GetOdbcError(stmt, SQL_HANDLE_STMT); + return std::nullopt; + } + if (indicator == SQL_NULL_DATA) { + return std::nullopt; + } + return std::string(value); +} + +template +std::optional ReadNumber(SQLHSTMT stmt, SQLUSMALLINT column, SQLSMALLINT targetType) { + T value = {}; + SQLLEN indicator = 0; + const SQLRETURN rc = SQLGetData( + stmt, column, targetType, &value, sizeof(value), &indicator); + if (rc != SQL_SUCCESS && rc != SQL_SUCCESS_WITH_INFO) { + ADD_FAILURE() << GetOdbcError(stmt, SQL_HANDLE_STMT); + return std::nullopt; + } + return indicator == SQL_NULL_DATA ? std::nullopt : std::optional{value}; +} + +} // namespace + TEST(MetadataApi, SQLTablesAll) { SQLHENV env; SQLHDBC dbc; @@ -41,7 +85,7 @@ TEST(MetadataApi, SQLTablesWithPattern) { stmt, SQL_HANDLE_STMT); SQLFreeStmt(stmt, SQL_CLOSE); CHECK_ODBC_OK(SQLTables(stmt, nullptr, 0, nullptr, 0, - (SQLCHAR*)"%/test_metadata_pattern_%", SQL_NTS, nullptr, 0), + (SQLCHAR*)"test\\_metadata\\_pattern\\_%", SQL_NTS, nullptr, 0), stmt, SQL_HANDLE_STMT); int tableCount = 0; while (SQLFetch(stmt) == SQL_SUCCESS) { @@ -69,7 +113,9 @@ TEST(MetadataApi, SQLTablesExactMatch) { CHECK_ODBC_OK(SQLSetStmtAttr(stmt, SQL_ATTR_METADATA_ID, (SQLPOINTER)(uintptr_t)SQL_TRUE, 0), stmt, SQL_HANDLE_STMT); const std::string exactPath = "/local/test_exact_table"; - CHECK_ODBC_OK(SQLTables(stmt, nullptr, 0, nullptr, 0, + SQLCHAR catalog[] = "local"; + SQLCHAR schema[] = ""; + CHECK_ODBC_OK(SQLTables(stmt, catalog, SQL_NTS, schema, 0, (SQLCHAR*)exactPath.c_str(), SQL_NTS, nullptr, 0), stmt, SQL_HANDLE_STMT); ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); @@ -213,7 +259,7 @@ TEST(MetadataApi, SQLTablesLikePatternWithMetadataId) { SQLULEN metadataId = SQL_FALSE; ASSERT_EQ(SQLGetStmtAttr(stmt, SQL_ATTR_METADATA_ID, &metadataId, 0, nullptr), SQL_SUCCESS); ASSERT_EQ(metadataId, SQL_FALSE); - const char* likePattern = "%/test_meta_table_%"; + const char* likePattern = "test_meta_table_%"; CHECK_ODBC_OK(SQLTables(stmt, nullptr, 0, nullptr, 0, (SQLCHAR*)likePattern, SQL_NTS, (SQLCHAR*)"TABLE", SQL_NTS), stmt, SQL_HANDLE_STMT); @@ -227,7 +273,9 @@ TEST(MetadataApi, SQLTablesLikePatternWithMetadataId) { stmt, SQL_HANDLE_STMT); ASSERT_EQ(SQLGetStmtAttr(stmt, SQL_ATTR_METADATA_ID, &metadataId, 0, nullptr), SQL_SUCCESS); ASSERT_EQ(metadataId, SQL_TRUE); - CHECK_ODBC_OK(SQLTables(stmt, nullptr, 0, nullptr, 0, + SQLCHAR catalog[] = "local"; + SQLCHAR schema[] = ""; + CHECK_ODBC_OK(SQLTables(stmt, catalog, SQL_NTS, schema, 0, (SQLCHAR*)likePattern, SQL_NTS, (SQLCHAR*)"TABLE", SQL_NTS), stmt, SQL_HANDLE_STMT); tableRows = 0; @@ -237,7 +285,7 @@ TEST(MetadataApi, SQLTablesLikePatternWithMetadataId) { ASSERT_EQ(tableRows, 0); SQLFreeStmt(stmt, SQL_CLOSE); const std::string exactPath = "/local/test_meta_table_1"; - CHECK_ODBC_OK(SQLTables(stmt, nullptr, 0, nullptr, 0, + CHECK_ODBC_OK(SQLTables(stmt, catalog, SQL_NTS, schema, 0, (SQLCHAR*)exactPath.c_str(), SQL_NTS, (SQLCHAR*)"TABLE", SQL_NTS), stmt, SQL_HANDLE_STMT); tableRows = 0; @@ -326,13 +374,15 @@ TEST(MetadataApi, SQLColumnsMetadataId) { const std::string exactTable = "/local/test_columns_metadata"; CHECK_ODBC_OK(SQLSetStmtAttr(stmt, SQL_ATTR_METADATA_ID, (SQLPOINTER)(uintptr_t)SQL_TRUE, 0), stmt, SQL_HANDLE_STMT); - ASSERT_EQ(SQLColumns(stmt, nullptr, 0, nullptr, 0, + SQLCHAR catalog[] = "local"; + SQLCHAR schema[] = ""; + ASSERT_EQ(SQLColumns(stmt, catalog, SQL_NTS, schema, 0, (SQLCHAR*)exactTable.c_str(), SQL_NTS, (SQLCHAR*)"val%", SQL_NTS), SQL_SUCCESS); ASSERT_EQ(SQLFetch(stmt), SQL_NO_DATA); SQLFreeStmt(stmt, SQL_CLOSE); - CHECK_ODBC_OK(SQLColumns(stmt, nullptr, 0, nullptr, 0, + CHECK_ODBC_OK(SQLColumns(stmt, catalog, SQL_NTS, schema, 0, (SQLCHAR*)exactTable.c_str(), SQL_NTS, (SQLCHAR*)"nonexistent_col%", SQL_NTS), stmt, SQL_HANDLE_STMT); @@ -391,3 +441,339 @@ TEST(MetadataApi, DdlWithComment) { SQLFreeHandle(SQL_HANDLE_DBC, dbc); SQLFreeHandle(SQL_HANDLE_ENV, env); } + +TEST(MetadataApi, SQLTablesSpecialEnumerationsAndEmptyArguments) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + + SQLCHAR empty[] = ""; + SQLCHAR all[] = "%"; + CHECK_ODBC_OK(SQLTables( + stmt, all, SQL_NTS, + empty, 0, empty, 0, empty, 0), stmt, SQL_HANDLE_STMT); + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + EXPECT_EQ(ReadText(stmt, 1), std::optional{"local"}); + EXPECT_FALSE(ReadText(stmt, 2)); + EXPECT_FALSE(ReadText(stmt, 3)); + EXPECT_FALSE(ReadText(stmt, 4)); + EXPECT_FALSE(ReadText(stmt, 5)); + EXPECT_EQ(SQLFetch(stmt), SQL_NO_DATA); + ASSERT_EQ(SQLFreeStmt(stmt, SQL_CLOSE), SQL_SUCCESS); + + CHECK_ODBC_OK(SQLTables( + stmt, empty, 0, all, SQL_NTS, + empty, 0, empty, 0), stmt, SQL_HANDLE_STMT); + EXPECT_EQ(SQLFetch(stmt), SQL_NO_DATA); + ASSERT_EQ(SQLFreeStmt(stmt, SQL_CLOSE), SQL_SUCCESS); + + CHECK_ODBC_OK(SQLTables( + stmt, empty, 0, empty, 0, empty, 0, + all, SQL_NTS), + stmt, SQL_HANDLE_STMT); + std::set tableTypes; + while (SQLFetch(stmt) == SQL_SUCCESS) { + EXPECT_FALSE(ReadText(stmt, 1)); + EXPECT_FALSE(ReadText(stmt, 2)); + EXPECT_FALSE(ReadText(stmt, 3)); + const auto tableType = ReadText(stmt, 4); + ASSERT_TRUE(tableType); + tableTypes.insert(*tableType); + EXPECT_FALSE(ReadText(stmt, 5)); + } + EXPECT_TRUE(tableTypes.contains("TABLE")); + EXPECT_TRUE(tableTypes.contains("VIEW")); + ASSERT_EQ(SQLFreeStmt(stmt, SQL_CLOSE), SQL_SUCCESS); + + CHECK_ODBC_OK(SQLTables( + stmt, empty, 0, nullptr, 0, nullptr, 0, nullptr, 0), + stmt, SQL_HANDLE_STMT); + EXPECT_EQ(SQLFetch(stmt), SQL_NO_DATA); + ASSERT_EQ(SQLFreeStmt(stmt, SQL_CLOSE), SQL_SUCCESS); + + CHECK_ODBC_OK(SQLTables( + stmt, nullptr, 0, nullptr, 0, empty, 0, nullptr, 0), + stmt, SQL_HANDLE_STMT); + EXPECT_EQ(SQLFetch(stmt), SQL_NO_DATA); + + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(MetadataApi, SQLColumnsFieldsAndCompositePrimaryKey) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + + ExecuteAndClose(stmt, "DROP TABLE IF EXISTS test_column_metadata_fields"); + ExecuteAndClose(stmt, + "CREATE TABLE test_column_metadata_fields (" + "pk_a Int32, pk_b Int64, text_value Utf8, payload String, created Timestamp, " + "PRIMARY KEY (pk_b, pk_a))"); + + struct TExpectedColumn { + const char* Name; + SQLSMALLINT DataType; + SQLSMALLINT Nullable; + SQLSMALLINT SqlDataType; + std::optional DateTimeSub; + std::optional CharOctetLength; + }; + const std::array expectedColumns{ + TExpectedColumn{"pk_a", SQL_INTEGER, SQL_NO_NULLS, SQL_INTEGER, std::nullopt, std::nullopt}, + TExpectedColumn{"pk_b", SQL_BIGINT, SQL_NO_NULLS, SQL_BIGINT, std::nullopt, std::nullopt}, + TExpectedColumn{"text_value", SQL_VARCHAR, SQL_NULLABLE, SQL_VARCHAR, std::nullopt, 255}, + TExpectedColumn{"payload", SQL_VARBINARY, SQL_NULLABLE, SQL_VARBINARY, std::nullopt, 4096}, + TExpectedColumn{"created", SQL_TYPE_TIMESTAMP, SQL_NULLABLE, SQL_DATETIME, + SQL_CODE_TIMESTAMP, std::nullopt}, + }; + + SQLCHAR tableName[] = "test_column_metadata_fields"; + CHECK_ODBC_OK(SQLColumns( + stmt, nullptr, 0, nullptr, 0, tableName, SQL_NTS, nullptr, 0), + stmt, SQL_HANDLE_STMT); + for (size_t i = 0; i < expectedColumns.size(); ++i) { + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + const auto& expected = expectedColumns[i]; + EXPECT_EQ(ReadText(stmt, 1), std::optional{"local"}); + EXPECT_FALSE(ReadText(stmt, 2)); + EXPECT_EQ(ReadText(stmt, 3), std::optional{"test_column_metadata_fields"}); + EXPECT_EQ(ReadText(stmt, 4), std::optional{expected.Name}); + EXPECT_EQ(ReadNumber(stmt, 5, SQL_C_SSHORT), expected.DataType); + EXPECT_TRUE(ReadNumber(stmt, 8, SQL_C_LONG)); + EXPECT_EQ(ReadNumber(stmt, 11, SQL_C_SSHORT), expected.Nullable); + EXPECT_EQ(ReadNumber(stmt, 14, SQL_C_SSHORT), expected.SqlDataType); + EXPECT_EQ(ReadNumber(stmt, 15, SQL_C_SSHORT), expected.DateTimeSub); + EXPECT_EQ(ReadNumber(stmt, 16, SQL_C_LONG), expected.CharOctetLength); + EXPECT_EQ(ReadNumber(stmt, 17, SQL_C_LONG), + std::optional{static_cast(i + 1)}); + EXPECT_EQ(ReadText(stmt, 18), + std::optional{expected.Nullable == SQL_NO_NULLS ? "NO" : "YES"}); + } + EXPECT_EQ(SQLFetch(stmt), SQL_NO_DATA); + ASSERT_EQ(SQLFreeStmt(stmt, SQL_CLOSE), SQL_SUCCESS); + + CHECK_ODBC_OK(SQLPrimaryKeys( + stmt, nullptr, 0, nullptr, 0, tableName, SQL_NTS), stmt, SQL_HANDLE_STMT); + const std::array expectedKeys{"pk_b", "pk_a"}; + for (size_t i = 0; i < expectedKeys.size(); ++i) { + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + EXPECT_EQ(ReadText(stmt, 1), std::optional{"local"}); + EXPECT_FALSE(ReadText(stmt, 2)); + EXPECT_EQ(ReadText(stmt, 3), std::optional{"test_column_metadata_fields"}); + EXPECT_EQ(ReadText(stmt, 4), std::optional{expectedKeys[i]}); + EXPECT_EQ(ReadNumber(stmt, 5, SQL_C_SSHORT), + std::optional{static_cast(i + 1)}); + EXPECT_FALSE(ReadText(stmt, 6)); + } + EXPECT_EQ(SQLFetch(stmt), SQL_NO_DATA); + + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(MetadataApi, MetadataUsesCurrentCatalogForSameNamedTables) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + + ExecuteAndClose(stmt, "DROP TABLE IF EXISTS `/local/cat_a/catalog_metadata_same`"); + ExecuteAndClose(stmt, "DROP TABLE IF EXISTS `/local/cat_b/catalog_metadata_same`"); + ExecuteAndClose(stmt, + "CREATE TABLE `/local/cat_a/catalog_metadata_same` (" + "id Int32, a_value Utf8, PRIMARY KEY (id))"); + ExecuteAndClose(stmt, + "CREATE TABLE `/local/cat_b/catalog_metadata_same` (" + "tenant_id Int32, id Int32, b_value Int64, PRIMARY KEY (tenant_id, id))"); + + SQLCHAR relativeTable[] = "catalog_metadata_same"; + SQLCHAR catalogA[] = "local/cat_a"; + CHECK_ODBC_OK(SQLSetConnectAttr( + dbc, SQL_ATTR_CURRENT_CATALOG, (SQLPOINTER)"/local/cat_a", SQL_NTS), + dbc, SQL_HANDLE_DBC); + CHECK_ODBC_OK(SQLTables( + stmt, catalogA, SQL_NTS, nullptr, 0, relativeTable, SQL_NTS, + (SQLCHAR*)"TABLE", SQL_NTS), stmt, SQL_HANDLE_STMT); + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + EXPECT_EQ(ReadText(stmt, 1), std::optional{"local/cat_a"}); + EXPECT_EQ(ReadText(stmt, 3), std::optional{"catalog_metadata_same"}); + EXPECT_EQ(SQLFetch(stmt), SQL_NO_DATA); + ASSERT_EQ(SQLFreeStmt(stmt, SQL_CLOSE), SQL_SUCCESS); + + CHECK_ODBC_OK(SQLColumns( + stmt, nullptr, 0, nullptr, 0, relativeTable, SQL_NTS, nullptr, 0), + stmt, SQL_HANDLE_STMT); + std::vector columns; + while (SQLFetch(stmt) == SQL_SUCCESS) { + const auto name = ReadText(stmt, 4); + ASSERT_TRUE(name); + columns.push_back(*name); + } + EXPECT_EQ(columns, (std::vector{"id", "a_value"})); + ASSERT_EQ(SQLFreeStmt(stmt, SQL_CLOSE), SQL_SUCCESS); + + CHECK_ODBC_OK(SQLSetConnectAttr( + dbc, SQL_ATTR_CURRENT_CATALOG, (SQLPOINTER)"/local/cat_b", SQL_NTS), + dbc, SQL_HANDLE_DBC); + CHECK_ODBC_OK(SQLPrimaryKeys( + stmt, nullptr, 0, nullptr, 0, relativeTable, SQL_NTS), + stmt, SQL_HANDLE_STMT); + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + EXPECT_EQ(ReadText(stmt, 1), std::optional{"local/cat_b"}); + EXPECT_EQ(ReadText(stmt, 4), std::optional{"tenant_id"}); + EXPECT_EQ(ReadNumber(stmt, 5, SQL_C_SSHORT), 1); + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + EXPECT_EQ(ReadText(stmt, 4), std::optional{"id"}); + EXPECT_EQ(ReadNumber(stmt, 5, SQL_C_SSHORT), 2); + EXPECT_EQ(SQLFetch(stmt), SQL_NO_DATA); + ASSERT_EQ(SQLFreeStmt(stmt, SQL_CLOSE), SQL_SUCCESS); + + CHECK_ODBC_OK(SQLSetConnectAttr( + dbc, SQL_ATTR_CURRENT_CATALOG, (SQLPOINTER)"/local", SQL_NTS), + dbc, SQL_HANDLE_DBC); + SQLCHAR nestedTable[] = "cat_a/catalog_metadata_same"; + CHECK_ODBC_OK(SQLTables( + stmt, nullptr, 0, nullptr, 0, nestedTable, SQL_NTS, + (SQLCHAR*)"TABLE", SQL_NTS), stmt, SQL_HANDLE_STMT); + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + EXPECT_EQ(ReadText(stmt, 3), + std::optional{"cat_a/catalog_metadata_same"}); + EXPECT_EQ(SQLFetch(stmt), SQL_NO_DATA); + ASSERT_EQ(SQLFreeStmt(stmt, SQL_CLOSE), SQL_SUCCESS); + + SQLCHAR absoluteTable[] = "/local/cat_b/catalog_metadata_same"; + CHECK_ODBC_OK(SQLColumns( + stmt, nullptr, 0, nullptr, 0, absoluteTable, SQL_NTS, + (SQLCHAR*)"b_value", SQL_NTS), stmt, SQL_HANDLE_STMT); + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + EXPECT_EQ(ReadText(stmt, 3), + std::optional{"cat_b/catalog_metadata_same"}); + EXPECT_EQ(ReadText(stmt, 4), std::optional{"b_value"}); + EXPECT_EQ(SQLFetch(stmt), SQL_NO_DATA); + + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(MetadataApi, SQLStatisticsReportsIndexesAndHonorsOptions) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + + ExecuteAndClose(stmt, "DROP TABLE IF EXISTS test_statistics_metadata"); + ExecuteAndClose(stmt, + "CREATE TABLE test_statistics_metadata (" + "id Int32, email Utf8 NOT NULL, city Utf8, created Timestamp, " + "INDEX idx_city_created GLOBAL SYNC ON (city, created), " + "INDEX idx_email GLOBAL UNIQUE SYNC ON (email), " + "PRIMARY KEY (id))"); + ExecuteAndClose(stmt, + "UPSERT INTO test_statistics_metadata (id, email, city) VALUES " + "(1, 'one@example.test', 'A'), (2, 'two@example.test', 'B')"); + + SQLCHAR tableName[] = "test_statistics_metadata"; + CHECK_ODBC_OK(SQLStatistics( + stmt, nullptr, 0, nullptr, 0, tableName, SQL_NTS, + SQL_INDEX_ALL, SQL_QUICK), stmt, SQL_HANDLE_STMT); + + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + EXPECT_EQ(ReadText(stmt, 3), std::optional{"test_statistics_metadata"}); + EXPECT_FALSE(ReadNumber(stmt, 4, SQL_C_SSHORT)); + EXPECT_FALSE(ReadText(stmt, 6)); + EXPECT_EQ(ReadNumber(stmt, 7, SQL_C_SSHORT), SQL_TABLE_STAT); + EXPECT_FALSE(ReadNumber(stmt, 8, SQL_C_SSHORT)); + EXPECT_FALSE(ReadText(stmt, 9)); + EXPECT_FALSE(ReadText(stmt, 10)); + EXPECT_FALSE(ReadNumber(stmt, 11, SQL_C_LONG)); + EXPECT_FALSE(ReadNumber(stmt, 12, SQL_C_LONG)); + EXPECT_FALSE(ReadText(stmt, 13)); + + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + EXPECT_EQ(ReadNumber(stmt, 4, SQL_C_SSHORT), SQL_FALSE); + EXPECT_EQ(ReadText(stmt, 6), std::optional{"idx_email"}); + EXPECT_EQ(ReadNumber(stmt, 7, SQL_C_SSHORT), SQL_INDEX_OTHER); + EXPECT_EQ(ReadNumber(stmt, 8, SQL_C_SSHORT), 1); + EXPECT_EQ(ReadText(stmt, 9), std::optional{"email"}); + EXPECT_FALSE(ReadNumber(stmt, 11, SQL_C_LONG)); + + const std::array regularIndexColumns{"city", "created"}; + for (size_t i = 0; i < regularIndexColumns.size(); ++i) { + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + EXPECT_EQ(ReadNumber(stmt, 4, SQL_C_SSHORT), SQL_TRUE); + EXPECT_EQ(ReadText(stmt, 6), std::optional{"idx_city_created"}); + EXPECT_EQ(ReadNumber(stmt, 7, SQL_C_SSHORT), SQL_INDEX_OTHER); + EXPECT_EQ(ReadNumber(stmt, 8, SQL_C_SSHORT), + std::optional{static_cast(i + 1)}); + EXPECT_EQ(ReadText(stmt, 9), + std::optional{regularIndexColumns[i]}); + EXPECT_FALSE(ReadNumber(stmt, 11, SQL_C_LONG)); + EXPECT_FALSE(ReadNumber(stmt, 12, SQL_C_LONG)); + } + EXPECT_EQ(SQLFetch(stmt), SQL_NO_DATA); + ASSERT_EQ(SQLFreeStmt(stmt, SQL_CLOSE), SQL_SUCCESS); + + CHECK_ODBC_OK(SQLStatistics( + stmt, nullptr, 0, nullptr, 0, tableName, SQL_NTS, + SQL_INDEX_UNIQUE, SQL_ENSURE), stmt, SQL_HANDLE_STMT); + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + EXPECT_EQ(ReadNumber(stmt, 7, SQL_C_SSHORT), SQL_TABLE_STAT); + EXPECT_TRUE(ReadNumber(stmt, 11, SQL_C_LONG)); + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + EXPECT_EQ(ReadNumber(stmt, 4, SQL_C_SSHORT), SQL_FALSE); + EXPECT_EQ(ReadText(stmt, 6), std::optional{"idx_email"}); + EXPECT_EQ(ReadText(stmt, 9), std::optional{"email"}); + EXPECT_EQ(SQLFetch(stmt), SQL_NO_DATA); + + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} + +TEST(MetadataApi, RequiredTableArgumentsAndStatisticsOptionsAreValidated) { + SQLHENV env; + SQLHDBC dbc; + SQLHSTMT stmt; + AllocEnvAndConnect(&env, &dbc); + ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + + EXPECT_EQ(SQLPrimaryKeys(stmt, nullptr, 0, nullptr, 0, nullptr, 0), SQL_ERROR); + EXPECT_TRUE(SqlStatePrefix(GetOdbcError(stmt, SQL_HANDLE_STMT), "HY009")); + EXPECT_EQ(SQLStatistics( + stmt, nullptr, 0, nullptr, 0, nullptr, 0, SQL_INDEX_ALL, SQL_QUICK), SQL_ERROR); + EXPECT_TRUE(SqlStatePrefix(GetOdbcError(stmt, SQL_HANDLE_STMT), "HY009")); + + SQLCHAR tableName[] = "not_used"; + EXPECT_EQ(SQLStatistics( + stmt, nullptr, 0, nullptr, 0, tableName, SQL_NTS, 99, SQL_QUICK), SQL_ERROR); + EXPECT_TRUE(SqlStatePrefix(GetOdbcError(stmt, SQL_HANDLE_STMT), "HY100")); + EXPECT_EQ(SQLStatistics( + stmt, nullptr, 0, nullptr, 0, tableName, SQL_NTS, SQL_INDEX_ALL, 99), SQL_ERROR); + EXPECT_TRUE(SqlStatePrefix(GetOdbcError(stmt, SQL_HANDLE_STMT), "HY101")); + + CHECK_ODBC_OK(SQLSetStmtAttr( + stmt, SQL_ATTR_METADATA_ID, (SQLPOINTER)(uintptr_t)SQL_TRUE, 0), + stmt, SQL_HANDLE_STMT); + EXPECT_EQ(SQLTables( + stmt, nullptr, 0, nullptr, 0, tableName, SQL_NTS, nullptr, 0), SQL_ERROR); + EXPECT_TRUE(SqlStatePrefix(GetOdbcError(stmt, SQL_HANDLE_STMT), "HY009")); + + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + SQLDisconnect(dbc); + SQLFreeHandle(SQL_HANDLE_DBC, dbc); + SQLFreeHandle(SQL_HANDLE_ENV, env); +} diff --git a/odbc/tests/unit/sql_like_ut.cpp b/odbc/tests/unit/sql_like_ut.cpp index e0b8d87ee0..34858652d4 100644 --- a/odbc/tests/unit/sql_like_ut.cpp +++ b/odbc/tests/unit/sql_like_ut.cpp @@ -26,3 +26,16 @@ TEST(SqlLikeMatch, PercentAtEnds) { EXPECT_TRUE(SqlLikeMatch("hello", "hel%")); EXPECT_TRUE(SqlLikeMatch("hello", "%llo")); } + +TEST(SqlLikeMatch, BackslashEscapesWildcards) { + EXPECT_TRUE(SqlLikeMatch("a_c", "a\\_c")); + EXPECT_FALSE(SqlLikeMatch("abc", "a\\_c")); + EXPECT_TRUE(SqlLikeMatch("rate%", "rate\\%")); + EXPECT_FALSE(SqlLikeMatch("rate123", "rate\\%")); + EXPECT_TRUE(SqlLikeMatch("a\\b", "a\\\\b")); +} + +TEST(SqlLikeMatch, PercentBacktrackingHonorsEscapes) { + EXPECT_TRUE(SqlLikeMatch("prefix_value_suffix", "%\\_value\\_%")); + EXPECT_FALSE(SqlLikeMatch("prefixXvalueYsuffix", "%\\_value\\_%")); +} From 5cfa6cb249a90bb3a27508d5250ada8940d8d42a Mon Sep 17 00:00:00 2001 From: Artem Ermoshkin Date: Sat, 12 Sep 2026 16:33:21 +0300 Subject: [PATCH 2/5] YDBAPPTEAM-1843 compact ODBC metadata implementation --- odbc/src/odbc_driver.cpp | 22 +- odbc/src/statement.h | 45 ++- odbc/src/statement_metadata.cpp | 208 ++++++-------- odbc/tests/integration/metadata_api_it.cpp | 310 ++++++++------------- odbc/tests/unit/sql_like_ut.cpp | 3 - 5 files changed, 228 insertions(+), 360 deletions(-) diff --git a/odbc/src/odbc_driver.cpp b/odbc/src/odbc_driver.cpp index 4ee38f5872..77ed9bd91c 100644 --- a/odbc/src/odbc_driver.cpp +++ b/odbc/src/odbc_driver.cpp @@ -23,24 +23,16 @@ namespace { return Odbc::CallOdbc(handle, std::forward(fn)); } - template + template struct TLazyText { Char* Value; SQLINTEGER Length; - std::string Resolve() const { - return Odbc::GetString(Value, Length); - } - }; - - template - struct TLazyOptionalText { - Char* Value; - SQLINTEGER Length; - std::optional Resolve() const { - if (!Value) { - return std::nullopt; + auto Resolve() const { + if constexpr (PreserveNull) { + return Value ? std::optional{Odbc::GetString(Value, Length)} : std::nullopt; + } else { + return Odbc::GetString(Value, Length); } - return Odbc::GetString(Value, Length); } }; @@ -59,7 +51,7 @@ namespace { } template - TLazyOptionalText OptionalText(Char* value, SQLINTEGER length) { + TLazyText OptionalText(Char* value, SQLINTEGER length) { return {value, length}; } diff --git a/odbc/src/statement.h b/odbc/src/statement.h index a4e2848c21..244a08210d 100644 --- a/odbc/src/statement.h +++ b/odbc/src/statement.h @@ -18,6 +18,8 @@ namespace NYdb::NOdbc { +using TMetadataArgument = std::optional; + class TStatement : public TErrorManager { public: TStatement(TConnection* conn); @@ -39,20 +41,16 @@ class TStatement : public TErrorManager { SQLRETURN BindCol(SQLUSMALLINT columnNumber, SQLSMALLINT targetType, SQLPOINTER targetValue, SQLLEN bufferLength, SQLLEN* strLenOrInd); SQLRETURN BindParameter(SQLUSMALLINT paramNumber, SQLSMALLINT inputOutputType, SQLSMALLINT valueType, SQLSMALLINT parameterType, SQLULEN columnSize, SQLSMALLINT decimalDigits, SQLPOINTER parameterValuePtr, SQLLEN bufferLength, SQLLEN* strLenOrIndPtr); - SQLRETURN Columns(const std::optional& catalogName, - const std::optional& schemaName, - const std::optional& tableName, - const std::optional& columnName); + SQLRETURN Columns(const TMetadataArgument& catalogName, const TMetadataArgument& schemaName, + const TMetadataArgument& tableName, const TMetadataArgument& columnName); - SQLRETURN Tables(const std::optional& catalogName, - const std::optional& schemaName, - const std::optional& tableName, - const std::optional& tableType); + SQLRETURN Tables(const TMetadataArgument& catalogName, const TMetadataArgument& schemaName, + const TMetadataArgument& tableName, const TMetadataArgument& tableType); SQLRETURN GetTypeInfo(SQLSMALLINT dataType); - SQLRETURN Statistics(const std::optional& catalogName, - const std::optional& schemaName, - const std::optional& tableName, + SQLRETURN Statistics(const TMetadataArgument& catalogName, + const TMetadataArgument& schemaName, + const TMetadataArgument& tableName, SQLUSMALLINT unique, SQLUSMALLINT accuracy); SQLRETURN SpecialColumns(const std::string& catalogName, @@ -60,9 +58,8 @@ class TStatement : public TErrorManager { const std::string& tableName, SQLUSMALLINT identifierType, SQLUSMALLINT scope); - SQLRETURN PrimaryKeys(const std::optional& catalogName, - const std::optional& schemaName, - const std::optional& tableName); + SQLRETURN PrimaryKeys(const TMetadataArgument& catalogName, const TMetadataArgument& schemaName, + const TMetadataArgument& tableName); SQLRETURN ForeignKeys(const std::string& pkCatalogName, const std::string& pkSchemaName, const std::string& pkTableName, @@ -165,10 +162,9 @@ class TStatement : public TErrorManager { std::optional ResolveDescriptorAttribute(SQLINTEGER attr); SQLUSMALLINT FindNextNeedDataParam() const; - std::string QualifyMetadataTableName(const std::string& tableName) const; std::string GetMetadataTableName(const std::string& path) const; - bool MetadataNamespaceMatches(const std::optional& catalog, - const std::optional& schema, + bool MetadataNamespaceMatches(const TMetadataArgument& catalog, + const TMetadataArgument& schema, bool catalogPatternsAllowed, bool schemaPatternsAllowed) const; @@ -178,16 +174,11 @@ class TStatement : public TErrorManager { SQLULEN paramSet); NYdb::NRetry::TRetryOperationSettings MakeAutocommitRetrySettings(); - std::vector GetMetadataEntries( - const std::optional& tableName, - bool patternsAllowed); - SQLRETURN VisitEntry(const std::string& path, - const std::string& tableName, - bool patternsAllowed, - std::vector& resultEntries); - bool IsMetadataTableMatch(const std::string& path, - const std::string& tableName, - bool patternsAllowed); + std::vector GetMetadataEntries(const TMetadataArgument& tableName, + bool patternsAllowed); + void VisitEntry(const std::string& path, const std::string& tableName, + bool patternsAllowed, + std::vector& resultEntries); std::optional GetTableType(NScheme::ESchemeEntryType type); }; diff --git a/odbc/src/statement_metadata.cpp b/odbc/src/statement_metadata.cpp index 7aa1ed7c07..279c974cc9 100644 --- a/odbc/src/statement_metadata.cpp +++ b/odbc/src/statement_metadata.cpp @@ -6,7 +6,6 @@ #include "utils/util.h" #include -#include #include #include #include @@ -98,28 +97,23 @@ const TColumnMeta kStatisticsSchema[] = { V("FILTER_CONDITION"), }; -using TSchemeEntryType = NScheme::ESchemeEntryType; - -constexpr std::array kTableTypes{ - std::pair{TSchemeEntryType::Table, std::string_view{"TABLE"}}, - std::pair{TSchemeEntryType::View, std::string_view{"VIEW"}}, - std::pair{TSchemeEntryType::ColumnStore, std::string_view{"COLUMN_STORE"}}, - std::pair{TSchemeEntryType::ColumnTable, std::string_view{"COLUMN_TABLE"}}, - std::pair{TSchemeEntryType::Sequence, std::string_view{"SEQUENCE"}}, - std::pair{TSchemeEntryType::Replication, std::string_view{"REPLICATION"}}, - std::pair{TSchemeEntryType::Topic, std::string_view{"TOPIC"}}, - std::pair{TSchemeEntryType::ExternalTable, std::string_view{"EXTERNAL_TABLE"}}, - std::pair{TSchemeEntryType::ExternalDataSource, std::string_view{"EXTERNAL_DATA_SOURCE"}}, - std::pair{TSchemeEntryType::ResourcePool, std::string_view{"RESOURCE_POOL"}}, - std::pair{TSchemeEntryType::PqGroup, std::string_view{"PQ_GROUP"}}, - std::pair{TSchemeEntryType::RtmrVolume, std::string_view{"RTMR_VOLUME"}}, - std::pair{TSchemeEntryType::BlockStoreVolume, std::string_view{"BLOCK_STORE_VOLUME"}}, - std::pair{TSchemeEntryType::CoordinationNode, std::string_view{"COORDINATION_NODE"}}, - std::pair{TSchemeEntryType::Unknown, std::string_view{"UNKNOWN"}}, - std::pair{TSchemeEntryType::SysView, std::string_view{"SYSTEM VIEW"}}, - std::pair{TSchemeEntryType::Transfer, std::string_view{"TRANSFER"}}, +using EEntry = NScheme::ESchemeEntryType; +constexpr std::pair kTableTypes[] = { + {EEntry::Table, "TABLE"}, {EEntry::View, "VIEW"}, {EEntry::ColumnStore, "COLUMN_STORE"}, + {EEntry::ColumnTable, "COLUMN_TABLE"}, {EEntry::Sequence, "SEQUENCE"}, + {EEntry::Replication, "REPLICATION"}, {EEntry::Topic, "TOPIC"}, + {EEntry::ExternalTable, "EXTERNAL_TABLE"}, + {EEntry::ExternalDataSource, "EXTERNAL_DATA_SOURCE"}, {EEntry::ResourcePool, "RESOURCE_POOL"}, + {EEntry::PqGroup, "PQ_GROUP"}, {EEntry::RtmrVolume, "RTMR_VOLUME"}, + {EEntry::BlockStoreVolume, "BLOCK_STORE_VOLUME"}, + {EEntry::CoordinationNode, "COORDINATION_NODE"}, {EEntry::Unknown, "UNKNOWN"}, + {EEntry::SysView, "SYSTEM VIEW"}, {EEntry::Transfer, "TRANSFER"}, }; +bool IsTable(EEntry type) { + return type == EEntry::Table || type == EEntry::ColumnTable; +} + const TColumnMeta kSpecialColumnsSchema[] = { N("SCOPE", SQL_SMALLINT), V("COLUMN_NAME", 128, SQL_NO_NULLS), @@ -188,11 +182,11 @@ TOdbcScalar Int32OrNull(uint64_t value) { : Null(); } -bool IsExplicitEmpty(const std::optional& value) { +bool IsExplicitEmpty(const TMetadataArgument& value) { return value && value->empty(); } -bool IsSpecialValue(const std::optional& value, std::string_view special) { +bool IsSpecialValue(const TMetadataArgument& value, std::string_view special) { return value && *value == special; } @@ -213,38 +207,32 @@ TOdbcScalar GetDateTimeSub(SQLSMALLINT type) { } } +TOdbcScalar GetColumnSize(const TYdbTypeInfo& type) { + return type.ColumnSize ? I(static_cast(type.ColumnSize)) : Null(); +} + TOdbcScalar GetBufferLength(const TYdbTypeInfo& type) { switch (type.SqlType) { - case SQL_BIT: - case SQL_TINYINT: return I(sizeof(SQLCHAR)); + case SQL_BIT: case SQL_TINYINT: return I(sizeof(SQLCHAR)); case SQL_SMALLINT: return I(sizeof(SQLSMALLINT)); - case SQL_INTEGER: - case SQL_REAL: return I(sizeof(SQLINTEGER)); - case SQL_BIGINT: - case SQL_DOUBLE: return I(sizeof(SQLBIGINT)); + case SQL_INTEGER: case SQL_REAL: return I(sizeof(SQLINTEGER)); + case SQL_BIGINT: case SQL_DOUBLE: return I(sizeof(SQLBIGINT)); case SQL_TYPE_DATE: return I(sizeof(SQL_DATE_STRUCT)); case SQL_TYPE_TIME: return I(sizeof(SQL_TIME_STRUCT)); case SQL_TYPE_TIMESTAMP: return I(sizeof(SQL_TIMESTAMP_STRUCT)); case SQL_GUID: return I(sizeof(SQLGUID)); - default: - return type.ColumnSize ? I(static_cast(type.ColumnSize)) : Null(); + default: return GetColumnSize(type); } } TOdbcScalar GetCharOctetLength(const TYdbTypeInfo& type) { switch (type.SqlType) { - case SQL_CHAR: - case SQL_VARCHAR: - case SQL_LONGVARCHAR: - case SQL_WCHAR: - case SQL_WVARCHAR: - case SQL_WLONGVARCHAR: - case SQL_BINARY: - case SQL_VARBINARY: + case SQL_CHAR: case SQL_VARCHAR: case SQL_LONGVARCHAR: + case SQL_WCHAR: case SQL_WVARCHAR: case SQL_WLONGVARCHAR: + case SQL_BINARY: case SQL_VARBINARY: case SQL_LONGVARBINARY: - return type.ColumnSize ? I(static_cast(type.ColumnSize)) : Null(); - default: - return Null(); + return GetColumnSize(type); + default: return Null(); } } @@ -260,7 +248,7 @@ std::string GetMetadataCatalogName(TConnection* connection) { template void DescribeTable(TConnection* connection, const std::string& path, - bool withTableStatistics, Visitor&& visitor) { + Visitor&& visitor, bool withTableStatistics = false) { auto client = connection->GetTableClient(); if (!client) { throw TOdbcException("HY000", 0, "No client connection"); @@ -277,11 +265,6 @@ void DescribeTable(TConnection* connection, const std::string& path, NStatusHelpers::ThrowOnError(status); } -template -void DescribeTable(TConnection* connection, const std::string& path, Visitor&& visitor) { - DescribeTable(connection, path, false, std::forward(visitor)); -} - bool MatchesTableTypeFilter(std::string_view filter, std::string_view entryType) { if (filter.empty()) { return true; @@ -337,10 +320,10 @@ TTable BuildTypeInfoRows(SQLSMALLINT dataType) { } // namespace -SQLRETURN TStatement::Columns(const std::optional& catalogName, - const std::optional& schemaName, - const std::optional& tableName, - const std::optional& columnName) { +SQLRETURN TStatement::Columns(const TMetadataArgument& catalogName, + const TMetadataArgument& schemaName, + const TMetadataArgument& tableName, + const TMetadataArgument& columnName) { if (Attributes_.GetMetadataId() == SQL_TRUE && (!catalogName || !schemaName || !tableName || !columnName)) { return AddError("HY009", 0, "Identifier arguments must not be null"); @@ -355,8 +338,7 @@ SQLRETURN TStatement::Columns(const std::optional& catalogName, const std::string catalog = GetMetadataCatalogName(Conn_); TTable table; for (const auto& entry : GetMetadataEntries(tableName, patternsAllowed)) { - if (entry.Type != NScheme::ESchemeEntryType::Table - && entry.Type != NScheme::ESchemeEntryType::ColumnTable) { + if (!IsTable(entry.Type)) { continue; } DescribeTable(Conn_, entry.Name, [&](const auto& description) { @@ -371,8 +353,7 @@ SQLRETURN TStatement::Columns(const std::optional& catalogName, continue; } const TYdbTypeInfo type = DescribeYdbType(column.Type); - const TOdbcScalar size = type.ColumnSize - ? I(static_cast(type.ColumnSize)) : Null(); + const TOdbcScalar size = GetColumnSize(type); const bool notNull = type.Nullable == SQL_NO_NULLS || (column.NotNull && *column.NotNull) || std::ranges::find(primaryKeyColumns, column.Name) != primaryKeyColumns.end(); @@ -391,10 +372,10 @@ SQLRETURN TStatement::Columns(const std::optional& catalogName, return SQL_SUCCESS; } -SQLRETURN TStatement::Tables(const std::optional& catalogName, - const std::optional& schemaName, - const std::optional& tableName, - const std::optional& tableType) { +SQLRETURN TStatement::Tables(const TMetadataArgument& catalogName, + const TMetadataArgument& schemaName, + const TMetadataArgument& tableName, + const TMetadataArgument& tableType) { if (Attributes_.GetMetadataId() == SQL_TRUE && (!catalogName || !schemaName || !tableName)) { return AddError("HY009", 0, "Identifier arguments must not be null"); @@ -419,14 +400,11 @@ SQLRETURN TStatement::Tables(const std::optional& catalogName, if (IsSpecialValue(tableType, SQL_ALL_TABLE_TYPES) && IsExplicitEmpty(catalogName) && emptySchema && emptyTable) { std::vector types; - types.reserve(kTableTypes.size()); for (const auto& [_, type] : kTableTypes) { types.push_back(type); } std::ranges::sort(types); - types.erase(std::unique(types.begin(), types.end()), types.end()); TTable table; - table.reserve(types.size()); for (const std::string_view type : types) { table.push_back({Null(), Null(), Null(), std::string(type), Null()}); } @@ -465,9 +443,9 @@ SQLRETURN TStatement::GetTypeInfo(SQLSMALLINT dataType) { return SQL_SUCCESS; } -SQLRETURN TStatement::Statistics(const std::optional& catalogName, - const std::optional& schemaName, - const std::optional& tableName, +SQLRETURN TStatement::Statistics(const TMetadataArgument& catalogName, + const TMetadataArgument& schemaName, + const TMetadataArgument& tableName, SQLUSMALLINT unique, SQLUSMALLINT accuracy) { if (unique != SQL_INDEX_UNIQUE && unique != SQL_INDEX_ALL) { return AddError("HY100", 0, "Invalid index uniqueness option"); @@ -490,14 +468,12 @@ SQLRETURN TStatement::Statistics(const std::optional& catalogName, auto entries = GetMetadataEntries(tableName, false); TTable table; - if (entries.size() == 1 - && (entries.front().Type == NScheme::ESchemeEntryType::Table - || entries.front().Type == NScheme::ESchemeEntryType::ColumnTable)) { + if (!entries.empty() && IsTable(entries.front().Type)) { const auto& entry = entries.front(); const std::string catalog = GetMetadataCatalogName(Conn_); const std::string tableNameResult = GetMetadataTableName(entry.Name); const bool withTableStatistics = accuracy == SQL_ENSURE; - DescribeTable(Conn_, entry.Name, withTableStatistics, [&](const auto& description) { + DescribeTable(Conn_, entry.Name, [&](const auto& description) { table.push_back({ catalog, Null(), tableNameResult, Null(), Null(), Null(), I(SQL_TABLE_STAT), Null(), Null(), Null(), @@ -506,12 +482,11 @@ SQLRETURN TStatement::Statistics(const std::optional& catalogName, }); auto indexes = description.GetIndexDescriptions(); - std::ranges::sort(indexes, [](const auto& lhs, const auto& rhs) { - const bool lhsUnique = lhs.GetIndexType() == NTable::EIndexType::GlobalUnique; - const bool rhsUnique = rhs.GetIndexType() == NTable::EIndexType::GlobalUnique; - return std::pair{!lhsUnique, lhs.GetIndexName()} - < std::pair{!rhsUnique, rhs.GetIndexName()}; - }); + const auto indexOrder = [](const auto& index) { + return std::pair{index.GetIndexType() != NTable::EIndexType::GlobalUnique, + index.GetIndexName()}; + }; + std::ranges::sort(indexes, {}, indexOrder); for (const auto& index : indexes) { const bool isUnique = index.GetIndexType() == NTable::EIndexType::GlobalUnique; if (unique == SQL_INDEX_UNIQUE && !isUnique) { @@ -526,7 +501,7 @@ SQLRETURN TStatement::Statistics(const std::optional& catalogName, }); } } - }); + }, withTableStatistics); } SetCursor(CreateVirtualCursor(kStatisticsSchema, std::move(table))); return SQL_SUCCESS; @@ -563,8 +538,7 @@ SQLRETURN TStatement::SpecialColumns(const std::string& catalogName, const std:: continue; } const TYdbTypeInfo type = DescribeYdbType(column->Type); - const TOdbcScalar size = type.ColumnSize - ? I(static_cast(type.ColumnSize)) : Null(); + const TOdbcScalar size = GetColumnSize(type); table.push_back({I(SQL_SCOPE_SESSION), pkName, I(type.SqlType), type.TypeName, size, size, Maybe(type.DecimalDigits), I(SQL_PC_NOT_PSEUDO)}); } @@ -574,9 +548,9 @@ SQLRETURN TStatement::SpecialColumns(const std::string& catalogName, const std:: return SQL_SUCCESS; } -SQLRETURN TStatement::PrimaryKeys(const std::optional& catalogName, - const std::optional& schemaName, - const std::optional& tableName) { +SQLRETURN TStatement::PrimaryKeys(const TMetadataArgument& catalogName, + const TMetadataArgument& schemaName, + const TMetadataArgument& tableName) { if (!tableName) { return AddError("HY009", 0, "TableName must not be null"); } @@ -620,32 +594,19 @@ SQLRETURN TStatement::ColumnPrivileges(const std::string&, const std::string&, return SQL_SUCCESS; } -std::string TStatement::QualifyMetadataTableName(const std::string& tableName) const { - if (!tableName.empty() && tableName.front() == '/') { - return tableName; - } - const std::string catalog = Conn_->GetCatalogBinding().Catalog; - if (catalog.empty() || catalog == "/") { - return "/" + tableName; - } - return catalog + "/" + tableName; -} - std::vector TStatement::GetMetadataEntries( - const std::optional& tableName, - bool patternsAllowed) { + const TMetadataArgument& tableName, bool patternsAllowed) { std::vector entries; if (tableName && tableName->empty()) { return entries; } const std::string catalog = Conn_->GetCatalogBinding().Catalog; - const std::string qualifiedName = tableName ? QualifyMetadataTableName(*tableName) : std::string{}; + const std::string prefix = catalog.empty() || catalog == "/" ? "/" : catalog + "/"; + const std::string qualifiedName = tableName + ? (tableName->starts_with('/') ? *tableName : prefix + *tableName) : std::string{}; if (tableName) { - const bool insideCatalog = catalog == "/" - ? qualifiedName.starts_with('/') - : qualifiedName == catalog || qualifiedName.starts_with(catalog + "/"); - if (!insideCatalog) { + if (catalog != "/" && qualifiedName != catalog && !qualifiedName.starts_with(prefix)) { return entries; } } @@ -666,31 +627,28 @@ std::string TStatement::GetMetadataTableName(const std::string& path) const { } bool TStatement::MetadataNamespaceMatches( - const std::optional& catalog, - const std::optional& schema, + const TMetadataArgument& catalog, + const TMetadataArgument& schema, bool catalogPatternsAllowed, bool schemaPatternsAllowed) const { - const auto matches = [](std::string_view value, std::string_view argument, - bool patternsAllowed) { - return patternsAllowed ? SqlLikeMatch(value, argument) : value == argument; + const auto matches = [](std::string_view value, std::string_view argument, bool patterns) { + return patterns ? SqlLikeMatch(value, argument) : value == argument; }; - - if (catalog) { - std::string catalogArgument = *catalog; - if (catalogArgument.starts_with('/')) { - catalogArgument.erase(0, 1); - } - if (!matches(GetMetadataCatalogName(Conn_), catalogArgument, - catalogPatternsAllowed)) { - return false; - } + const bool schemaMatches = !schema || matches("", *schema, schemaPatternsAllowed); + if (!catalog) { + return schemaMatches; + } + std::string_view catalogArgument = *catalog; + if (catalogArgument.starts_with('/')) { + catalogArgument.remove_prefix(1); } - return !schema || matches("", *schema, schemaPatternsAllowed); + return schemaMatches + && matches(GetMetadataCatalogName(Conn_), catalogArgument, catalogPatternsAllowed); } -SQLRETURN TStatement::VisitEntry(const std::string& path, const std::string& tableName, - bool patternsAllowed, - std::vector& result) { +void TStatement::VisitEntry(const std::string& path, const std::string& tableName, + bool patternsAllowed, + std::vector& result) { auto client = Conn_->GetSchemeClient(); if (!client) { throw TOdbcException("HY000", 0, "No client connection"); @@ -702,19 +660,13 @@ SQLRETURN TStatement::VisitEntry(const std::string& path, const std::string& tab if (entry.Type == NScheme::ESchemeEntryType::Directory || entry.Type == NScheme::ESchemeEntryType::SubDomain) { VisitEntry(fullPath, tableName, patternsAllowed, result); - } else if (IsMetadataTableMatch(fullPath, tableName, patternsAllowed)) { + } else if (tableName.empty() + || (patternsAllowed ? SqlLikeMatch(fullPath, tableName) + : fullPath == tableName)) { result.push_back(entry); result.back().Name = fullPath; } } - return SQL_SUCCESS; -} - -bool TStatement::IsMetadataTableMatch(const std::string& path, - const std::string& tableName, - bool patternsAllowed) { - return tableName.empty() || (patternsAllowed ? SqlLikeMatch(path, tableName) - : path == tableName); } std::optional TStatement::GetTableType(NScheme::ESchemeEntryType type) { diff --git a/odbc/tests/integration/metadata_api_it.cpp b/odbc/tests/integration/metadata_api_it.cpp index c0b9c8715f..e02d74f665 100644 --- a/odbc/tests/integration/metadata_api_it.cpp +++ b/odbc/tests/integration/metadata_api_it.cpp @@ -1,8 +1,12 @@ #include "test_utils.h" #include +#include #include #include +#include +#include +#include #include #ifndef SQL_ATTR_METADATA_ID @@ -11,6 +15,24 @@ namespace { +struct TMetadataHandles { + SQLHENV Env = SQL_NULL_HENV; + SQLHDBC Dbc = SQL_NULL_HDBC; + SQLHSTMT Stmt = SQL_NULL_HSTMT; + + TMetadataHandles() { + AllocEnvAndConnect(&Env, &Dbc); + EXPECT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, Dbc, &Stmt), SQL_SUCCESS); + } + + ~TMetadataHandles() { + SQLFreeHandle(SQL_HANDLE_STMT, Stmt); + SQLDisconnect(Dbc); + SQLFreeHandle(SQL_HANDLE_DBC, Dbc); + SQLFreeHandle(SQL_HANDLE_ENV, Env); + } +}; + void ExecuteAndClose(SQLHSTMT stmt, const char* sql) { CHECK_ODBC_OK(SQLExecDirect( stmt, reinterpret_cast(const_cast(sql)), SQL_NTS), @@ -27,23 +49,39 @@ std::optional ReadText(SQLHSTMT stmt, SQLUSMALLINT column) { ADD_FAILURE() << GetOdbcError(stmt, SQL_HANDLE_STMT); return std::nullopt; } - if (indicator == SQL_NULL_DATA) { - return std::nullopt; - } - return std::string(value); + return indicator == SQL_NULL_DATA ? std::nullopt + : std::optional{value}; } -template -std::optional ReadNumber(SQLHSTMT stmt, SQLUSMALLINT column, SQLSMALLINT targetType) { - T value = {}; - SQLLEN indicator = 0; - const SQLRETURN rc = SQLGetData( - stmt, column, targetType, &value, sizeof(value), &indicator); - if (rc != SQL_SUCCESS && rc != SQL_SUCCESS_WITH_INFO) { - ADD_FAILURE() << GetOdbcError(stmt, SQL_HANDLE_STMT); - return std::nullopt; +constexpr std::monostate AnyValue; +using TExpectedValue = std::variant; +using TExpectedCell = std::pair; + +void ExpectRow(SQLHSTMT stmt, std::initializer_list cells) { + ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); + for (const auto& [column, expected] : cells) { + const auto value = ReadText(stmt, column); + if (std::holds_alternative(expected)) { + EXPECT_FALSE(value) << "column " << column; + continue; + } + ASSERT_TRUE(value) << "column " << column; + if (const auto* text = std::get_if(&expected)) { + EXPECT_EQ(*value, *text) << "column " << column; + } else if (const auto* number = std::get_if(&expected)) { + EXPECT_EQ(*value, std::to_string(*number)) << "column " << column; + } } - return indicator == SQL_NULL_DATA ? std::nullopt : std::optional{value}; +} + +void FinishResult(SQLHSTMT stmt) { + EXPECT_EQ(SQLFetch(stmt), SQL_NO_DATA); + ASSERT_EQ(SQLFreeStmt(stmt, SQL_CLOSE), SQL_SUCCESS); +} + +void ExpectError(SQLRETURN result, SQLHSTMT stmt, std::string_view state) { + EXPECT_EQ(result, SQL_ERROR); + EXPECT_TRUE(SqlStatePrefix(GetOdbcError(stmt, SQL_HANDLE_STMT), state)); } } // namespace @@ -443,31 +481,21 @@ TEST(MetadataApi, DdlWithComment) { } TEST(MetadataApi, SQLTablesSpecialEnumerationsAndEmptyArguments) { - SQLHENV env; - SQLHDBC dbc; - SQLHSTMT stmt; - AllocEnvAndConnect(&env, &dbc); - ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + TMetadataHandles handles; + const SQLHSTMT stmt = handles.Stmt; SQLCHAR empty[] = ""; SQLCHAR all[] = "%"; CHECK_ODBC_OK(SQLTables( stmt, all, SQL_NTS, empty, 0, empty, 0, empty, 0), stmt, SQL_HANDLE_STMT); - ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); - EXPECT_EQ(ReadText(stmt, 1), std::optional{"local"}); - EXPECT_FALSE(ReadText(stmt, 2)); - EXPECT_FALSE(ReadText(stmt, 3)); - EXPECT_FALSE(ReadText(stmt, 4)); - EXPECT_FALSE(ReadText(stmt, 5)); - EXPECT_EQ(SQLFetch(stmt), SQL_NO_DATA); - ASSERT_EQ(SQLFreeStmt(stmt, SQL_CLOSE), SQL_SUCCESS); + ExpectRow(stmt, {{1, "local"}, {2, nullptr}, {3, nullptr}, {4, nullptr}, {5, nullptr}}); + FinishResult(stmt); CHECK_ODBC_OK(SQLTables( stmt, empty, 0, all, SQL_NTS, empty, 0, empty, 0), stmt, SQL_HANDLE_STMT); - EXPECT_EQ(SQLFetch(stmt), SQL_NO_DATA); - ASSERT_EQ(SQLFreeStmt(stmt, SQL_CLOSE), SQL_SUCCESS); + FinishResult(stmt); CHECK_ODBC_OK(SQLTables( stmt, empty, 0, empty, 0, empty, 0, @@ -490,26 +518,17 @@ TEST(MetadataApi, SQLTablesSpecialEnumerationsAndEmptyArguments) { CHECK_ODBC_OK(SQLTables( stmt, empty, 0, nullptr, 0, nullptr, 0, nullptr, 0), stmt, SQL_HANDLE_STMT); - EXPECT_EQ(SQLFetch(stmt), SQL_NO_DATA); - ASSERT_EQ(SQLFreeStmt(stmt, SQL_CLOSE), SQL_SUCCESS); + FinishResult(stmt); CHECK_ODBC_OK(SQLTables( stmt, nullptr, 0, nullptr, 0, empty, 0, nullptr, 0), stmt, SQL_HANDLE_STMT); - EXPECT_EQ(SQLFetch(stmt), SQL_NO_DATA); - - SQLFreeHandle(SQL_HANDLE_STMT, stmt); - SQLDisconnect(dbc); - SQLFreeHandle(SQL_HANDLE_DBC, dbc); - SQLFreeHandle(SQL_HANDLE_ENV, env); + FinishResult(stmt); } TEST(MetadataApi, SQLColumnsFieldsAndCompositePrimaryKey) { - SQLHENV env; - SQLHDBC dbc; - SQLHSTMT stmt; - AllocEnvAndConnect(&env, &dbc); - ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + TMetadataHandles handles; + const SQLHSTMT stmt = handles.Stmt; ExecuteAndClose(stmt, "DROP TABLE IF EXISTS test_column_metadata_fields"); ExecuteAndClose(stmt, @@ -522,16 +541,16 @@ TEST(MetadataApi, SQLColumnsFieldsAndCompositePrimaryKey) { SQLSMALLINT DataType; SQLSMALLINT Nullable; SQLSMALLINT SqlDataType; - std::optional DateTimeSub; - std::optional CharOctetLength; + TExpectedValue DateTimeSub; + TExpectedValue CharOctetLength; }; const std::array expectedColumns{ - TExpectedColumn{"pk_a", SQL_INTEGER, SQL_NO_NULLS, SQL_INTEGER, std::nullopt, std::nullopt}, - TExpectedColumn{"pk_b", SQL_BIGINT, SQL_NO_NULLS, SQL_BIGINT, std::nullopt, std::nullopt}, - TExpectedColumn{"text_value", SQL_VARCHAR, SQL_NULLABLE, SQL_VARCHAR, std::nullopt, 255}, - TExpectedColumn{"payload", SQL_VARBINARY, SQL_NULLABLE, SQL_VARBINARY, std::nullopt, 4096}, + TExpectedColumn{"pk_a", SQL_INTEGER, SQL_NO_NULLS, SQL_INTEGER, nullptr, nullptr}, + TExpectedColumn{"pk_b", SQL_BIGINT, SQL_NO_NULLS, SQL_BIGINT, nullptr, nullptr}, + TExpectedColumn{"text_value", SQL_VARCHAR, SQL_NULLABLE, SQL_VARCHAR, nullptr, 255}, + TExpectedColumn{"payload", SQL_VARBINARY, SQL_NULLABLE, SQL_VARBINARY, nullptr, 4096}, TExpectedColumn{"created", SQL_TYPE_TIMESTAMP, SQL_NULLABLE, SQL_DATETIME, - SQL_CODE_TIMESTAMP, std::nullopt}, + SQL_CODE_TIMESTAMP, nullptr}, }; SQLCHAR tableName[] = "test_column_metadata_fields"; @@ -539,53 +558,33 @@ TEST(MetadataApi, SQLColumnsFieldsAndCompositePrimaryKey) { stmt, nullptr, 0, nullptr, 0, tableName, SQL_NTS, nullptr, 0), stmt, SQL_HANDLE_STMT); for (size_t i = 0; i < expectedColumns.size(); ++i) { - ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); const auto& expected = expectedColumns[i]; - EXPECT_EQ(ReadText(stmt, 1), std::optional{"local"}); - EXPECT_FALSE(ReadText(stmt, 2)); - EXPECT_EQ(ReadText(stmt, 3), std::optional{"test_column_metadata_fields"}); - EXPECT_EQ(ReadText(stmt, 4), std::optional{expected.Name}); - EXPECT_EQ(ReadNumber(stmt, 5, SQL_C_SSHORT), expected.DataType); - EXPECT_TRUE(ReadNumber(stmt, 8, SQL_C_LONG)); - EXPECT_EQ(ReadNumber(stmt, 11, SQL_C_SSHORT), expected.Nullable); - EXPECT_EQ(ReadNumber(stmt, 14, SQL_C_SSHORT), expected.SqlDataType); - EXPECT_EQ(ReadNumber(stmt, 15, SQL_C_SSHORT), expected.DateTimeSub); - EXPECT_EQ(ReadNumber(stmt, 16, SQL_C_LONG), expected.CharOctetLength); - EXPECT_EQ(ReadNumber(stmt, 17, SQL_C_LONG), - std::optional{static_cast(i + 1)}); - EXPECT_EQ(ReadText(stmt, 18), - std::optional{expected.Nullable == SQL_NO_NULLS ? "NO" : "YES"}); + ExpectRow(stmt, { + {1, "local"}, {2, nullptr}, {3, "test_column_metadata_fields"}, {4, expected.Name}, + {5, expected.DataType}, {8, AnyValue}, {11, expected.Nullable}, + {14, expected.SqlDataType}, {15, expected.DateTimeSub}, + {16, expected.CharOctetLength}, {17, static_cast(i + 1)}, + {18, expected.Nullable == SQL_NO_NULLS ? "NO" : "YES"}, + }); } - EXPECT_EQ(SQLFetch(stmt), SQL_NO_DATA); - ASSERT_EQ(SQLFreeStmt(stmt, SQL_CLOSE), SQL_SUCCESS); + FinishResult(stmt); CHECK_ODBC_OK(SQLPrimaryKeys( stmt, nullptr, 0, nullptr, 0, tableName, SQL_NTS), stmt, SQL_HANDLE_STMT); const std::array expectedKeys{"pk_b", "pk_a"}; for (size_t i = 0; i < expectedKeys.size(); ++i) { - ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); - EXPECT_EQ(ReadText(stmt, 1), std::optional{"local"}); - EXPECT_FALSE(ReadText(stmt, 2)); - EXPECT_EQ(ReadText(stmt, 3), std::optional{"test_column_metadata_fields"}); - EXPECT_EQ(ReadText(stmt, 4), std::optional{expectedKeys[i]}); - EXPECT_EQ(ReadNumber(stmt, 5, SQL_C_SSHORT), - std::optional{static_cast(i + 1)}); - EXPECT_FALSE(ReadText(stmt, 6)); + ExpectRow(stmt, { + {1, "local"}, {2, nullptr}, {3, "test_column_metadata_fields"}, + {4, expectedKeys[i]}, {5, static_cast(i + 1)}, {6, nullptr}, + }); } - EXPECT_EQ(SQLFetch(stmt), SQL_NO_DATA); - - SQLFreeHandle(SQL_HANDLE_STMT, stmt); - SQLDisconnect(dbc); - SQLFreeHandle(SQL_HANDLE_DBC, dbc); - SQLFreeHandle(SQL_HANDLE_ENV, env); + FinishResult(stmt); } TEST(MetadataApi, MetadataUsesCurrentCatalogForSameNamedTables) { - SQLHENV env; - SQLHDBC dbc; - SQLHSTMT stmt; - AllocEnvAndConnect(&env, &dbc); - ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + TMetadataHandles handles; + const SQLHDBC dbc = handles.Dbc; + const SQLHSTMT stmt = handles.Stmt; ExecuteAndClose(stmt, "DROP TABLE IF EXISTS `/local/cat_a/catalog_metadata_same`"); ExecuteAndClose(stmt, "DROP TABLE IF EXISTS `/local/cat_b/catalog_metadata_same`"); @@ -604,11 +603,8 @@ TEST(MetadataApi, MetadataUsesCurrentCatalogForSameNamedTables) { CHECK_ODBC_OK(SQLTables( stmt, catalogA, SQL_NTS, nullptr, 0, relativeTable, SQL_NTS, (SQLCHAR*)"TABLE", SQL_NTS), stmt, SQL_HANDLE_STMT); - ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); - EXPECT_EQ(ReadText(stmt, 1), std::optional{"local/cat_a"}); - EXPECT_EQ(ReadText(stmt, 3), std::optional{"catalog_metadata_same"}); - EXPECT_EQ(SQLFetch(stmt), SQL_NO_DATA); - ASSERT_EQ(SQLFreeStmt(stmt, SQL_CLOSE), SQL_SUCCESS); + ExpectRow(stmt, {{1, "local/cat_a"}, {3, "catalog_metadata_same"}}); + FinishResult(stmt); CHECK_ODBC_OK(SQLColumns( stmt, nullptr, 0, nullptr, 0, relativeTable, SQL_NTS, nullptr, 0), @@ -628,15 +624,9 @@ TEST(MetadataApi, MetadataUsesCurrentCatalogForSameNamedTables) { CHECK_ODBC_OK(SQLPrimaryKeys( stmt, nullptr, 0, nullptr, 0, relativeTable, SQL_NTS), stmt, SQL_HANDLE_STMT); - ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); - EXPECT_EQ(ReadText(stmt, 1), std::optional{"local/cat_b"}); - EXPECT_EQ(ReadText(stmt, 4), std::optional{"tenant_id"}); - EXPECT_EQ(ReadNumber(stmt, 5, SQL_C_SSHORT), 1); - ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); - EXPECT_EQ(ReadText(stmt, 4), std::optional{"id"}); - EXPECT_EQ(ReadNumber(stmt, 5, SQL_C_SSHORT), 2); - EXPECT_EQ(SQLFetch(stmt), SQL_NO_DATA); - ASSERT_EQ(SQLFreeStmt(stmt, SQL_CLOSE), SQL_SUCCESS); + ExpectRow(stmt, {{1, "local/cat_b"}, {4, "tenant_id"}, {5, 1}}); + ExpectRow(stmt, {{4, "id"}, {5, 2}}); + FinishResult(stmt); CHECK_ODBC_OK(SQLSetConnectAttr( dbc, SQL_ATTR_CURRENT_CATALOG, (SQLPOINTER)"/local", SQL_NTS), @@ -645,34 +635,20 @@ TEST(MetadataApi, MetadataUsesCurrentCatalogForSameNamedTables) { CHECK_ODBC_OK(SQLTables( stmt, nullptr, 0, nullptr, 0, nestedTable, SQL_NTS, (SQLCHAR*)"TABLE", SQL_NTS), stmt, SQL_HANDLE_STMT); - ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); - EXPECT_EQ(ReadText(stmt, 3), - std::optional{"cat_a/catalog_metadata_same"}); - EXPECT_EQ(SQLFetch(stmt), SQL_NO_DATA); - ASSERT_EQ(SQLFreeStmt(stmt, SQL_CLOSE), SQL_SUCCESS); + ExpectRow(stmt, {{3, "cat_a/catalog_metadata_same"}}); + FinishResult(stmt); SQLCHAR absoluteTable[] = "/local/cat_b/catalog_metadata_same"; CHECK_ODBC_OK(SQLColumns( stmt, nullptr, 0, nullptr, 0, absoluteTable, SQL_NTS, (SQLCHAR*)"b_value", SQL_NTS), stmt, SQL_HANDLE_STMT); - ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); - EXPECT_EQ(ReadText(stmt, 3), - std::optional{"cat_b/catalog_metadata_same"}); - EXPECT_EQ(ReadText(stmt, 4), std::optional{"b_value"}); - EXPECT_EQ(SQLFetch(stmt), SQL_NO_DATA); - - SQLFreeHandle(SQL_HANDLE_STMT, stmt); - SQLDisconnect(dbc); - SQLFreeHandle(SQL_HANDLE_DBC, dbc); - SQLFreeHandle(SQL_HANDLE_ENV, env); + ExpectRow(stmt, {{3, "cat_b/catalog_metadata_same"}, {4, "b_value"}}); + FinishResult(stmt); } TEST(MetadataApi, SQLStatisticsReportsIndexesAndHonorsOptions) { - SQLHENV env; - SQLHDBC dbc; - SQLHSTMT stmt; - AllocEnvAndConnect(&env, &dbc); - ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + TMetadataHandles handles; + const SQLHSTMT stmt = handles.Stmt; ExecuteAndClose(stmt, "DROP TABLE IF EXISTS test_statistics_metadata"); ExecuteAndClose(stmt, @@ -690,90 +666,50 @@ TEST(MetadataApi, SQLStatisticsReportsIndexesAndHonorsOptions) { stmt, nullptr, 0, nullptr, 0, tableName, SQL_NTS, SQL_INDEX_ALL, SQL_QUICK), stmt, SQL_HANDLE_STMT); - ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); - EXPECT_EQ(ReadText(stmt, 3), std::optional{"test_statistics_metadata"}); - EXPECT_FALSE(ReadNumber(stmt, 4, SQL_C_SSHORT)); - EXPECT_FALSE(ReadText(stmt, 6)); - EXPECT_EQ(ReadNumber(stmt, 7, SQL_C_SSHORT), SQL_TABLE_STAT); - EXPECT_FALSE(ReadNumber(stmt, 8, SQL_C_SSHORT)); - EXPECT_FALSE(ReadText(stmt, 9)); - EXPECT_FALSE(ReadText(stmt, 10)); - EXPECT_FALSE(ReadNumber(stmt, 11, SQL_C_LONG)); - EXPECT_FALSE(ReadNumber(stmt, 12, SQL_C_LONG)); - EXPECT_FALSE(ReadText(stmt, 13)); - - ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); - EXPECT_EQ(ReadNumber(stmt, 4, SQL_C_SSHORT), SQL_FALSE); - EXPECT_EQ(ReadText(stmt, 6), std::optional{"idx_email"}); - EXPECT_EQ(ReadNumber(stmt, 7, SQL_C_SSHORT), SQL_INDEX_OTHER); - EXPECT_EQ(ReadNumber(stmt, 8, SQL_C_SSHORT), 1); - EXPECT_EQ(ReadText(stmt, 9), std::optional{"email"}); - EXPECT_FALSE(ReadNumber(stmt, 11, SQL_C_LONG)); + ExpectRow(stmt, { + {3, "test_statistics_metadata"}, {4, nullptr}, {6, nullptr}, {7, SQL_TABLE_STAT}, + {8, nullptr}, {9, nullptr}, {10, nullptr}, {11, nullptr}, {12, nullptr}, {13, nullptr}, + }); + ExpectRow(stmt, { + {4, SQL_FALSE}, {6, "idx_email"}, {7, SQL_INDEX_OTHER}, {8, 1}, + {9, "email"}, {11, nullptr}, + }); const std::array regularIndexColumns{"city", "created"}; for (size_t i = 0; i < regularIndexColumns.size(); ++i) { - ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); - EXPECT_EQ(ReadNumber(stmt, 4, SQL_C_SSHORT), SQL_TRUE); - EXPECT_EQ(ReadText(stmt, 6), std::optional{"idx_city_created"}); - EXPECT_EQ(ReadNumber(stmt, 7, SQL_C_SSHORT), SQL_INDEX_OTHER); - EXPECT_EQ(ReadNumber(stmt, 8, SQL_C_SSHORT), - std::optional{static_cast(i + 1)}); - EXPECT_EQ(ReadText(stmt, 9), - std::optional{regularIndexColumns[i]}); - EXPECT_FALSE(ReadNumber(stmt, 11, SQL_C_LONG)); - EXPECT_FALSE(ReadNumber(stmt, 12, SQL_C_LONG)); + ExpectRow(stmt, { + {4, SQL_TRUE}, {6, "idx_city_created"}, {7, SQL_INDEX_OTHER}, + {8, static_cast(i + 1)}, {9, regularIndexColumns[i]}, + {11, nullptr}, {12, nullptr}, + }); } - EXPECT_EQ(SQLFetch(stmt), SQL_NO_DATA); - ASSERT_EQ(SQLFreeStmt(stmt, SQL_CLOSE), SQL_SUCCESS); + FinishResult(stmt); CHECK_ODBC_OK(SQLStatistics( stmt, nullptr, 0, nullptr, 0, tableName, SQL_NTS, SQL_INDEX_UNIQUE, SQL_ENSURE), stmt, SQL_HANDLE_STMT); - ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); - EXPECT_EQ(ReadNumber(stmt, 7, SQL_C_SSHORT), SQL_TABLE_STAT); - EXPECT_TRUE(ReadNumber(stmt, 11, SQL_C_LONG)); - ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); - EXPECT_EQ(ReadNumber(stmt, 4, SQL_C_SSHORT), SQL_FALSE); - EXPECT_EQ(ReadText(stmt, 6), std::optional{"idx_email"}); - EXPECT_EQ(ReadText(stmt, 9), std::optional{"email"}); - EXPECT_EQ(SQLFetch(stmt), SQL_NO_DATA); - - SQLFreeHandle(SQL_HANDLE_STMT, stmt); - SQLDisconnect(dbc); - SQLFreeHandle(SQL_HANDLE_DBC, dbc); - SQLFreeHandle(SQL_HANDLE_ENV, env); + ExpectRow(stmt, {{7, SQL_TABLE_STAT}, {11, AnyValue}}); + ExpectRow(stmt, {{4, SQL_FALSE}, {6, "idx_email"}, {9, "email"}}); + FinishResult(stmt); } TEST(MetadataApi, RequiredTableArgumentsAndStatisticsOptionsAreValidated) { - SQLHENV env; - SQLHDBC dbc; - SQLHSTMT stmt; - AllocEnvAndConnect(&env, &dbc); - ASSERT_EQ(SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt), SQL_SUCCESS); + TMetadataHandles handles; + const SQLHSTMT stmt = handles.Stmt; - EXPECT_EQ(SQLPrimaryKeys(stmt, nullptr, 0, nullptr, 0, nullptr, 0), SQL_ERROR); - EXPECT_TRUE(SqlStatePrefix(GetOdbcError(stmt, SQL_HANDLE_STMT), "HY009")); - EXPECT_EQ(SQLStatistics( - stmt, nullptr, 0, nullptr, 0, nullptr, 0, SQL_INDEX_ALL, SQL_QUICK), SQL_ERROR); - EXPECT_TRUE(SqlStatePrefix(GetOdbcError(stmt, SQL_HANDLE_STMT), "HY009")); + ExpectError(SQLPrimaryKeys(stmt, nullptr, 0, nullptr, 0, nullptr, 0), stmt, "HY009"); + ExpectError(SQLStatistics( + stmt, nullptr, 0, nullptr, 0, nullptr, 0, SQL_INDEX_ALL, SQL_QUICK), stmt, "HY009"); SQLCHAR tableName[] = "not_used"; - EXPECT_EQ(SQLStatistics( - stmt, nullptr, 0, nullptr, 0, tableName, SQL_NTS, 99, SQL_QUICK), SQL_ERROR); - EXPECT_TRUE(SqlStatePrefix(GetOdbcError(stmt, SQL_HANDLE_STMT), "HY100")); - EXPECT_EQ(SQLStatistics( - stmt, nullptr, 0, nullptr, 0, tableName, SQL_NTS, SQL_INDEX_ALL, 99), SQL_ERROR); - EXPECT_TRUE(SqlStatePrefix(GetOdbcError(stmt, SQL_HANDLE_STMT), "HY101")); + ExpectError(SQLStatistics( + stmt, nullptr, 0, nullptr, 0, tableName, SQL_NTS, 99, SQL_QUICK), stmt, "HY100"); + ExpectError(SQLStatistics( + stmt, nullptr, 0, nullptr, 0, tableName, SQL_NTS, SQL_INDEX_ALL, 99), stmt, "HY101"); CHECK_ODBC_OK(SQLSetStmtAttr( stmt, SQL_ATTR_METADATA_ID, (SQLPOINTER)(uintptr_t)SQL_TRUE, 0), stmt, SQL_HANDLE_STMT); - EXPECT_EQ(SQLTables( - stmt, nullptr, 0, nullptr, 0, tableName, SQL_NTS, nullptr, 0), SQL_ERROR); - EXPECT_TRUE(SqlStatePrefix(GetOdbcError(stmt, SQL_HANDLE_STMT), "HY009")); - - SQLFreeHandle(SQL_HANDLE_STMT, stmt); - SQLDisconnect(dbc); - SQLFreeHandle(SQL_HANDLE_DBC, dbc); - SQLFreeHandle(SQL_HANDLE_ENV, env); + ExpectError(SQLTables( + stmt, nullptr, 0, nullptr, 0, tableName, SQL_NTS, nullptr, 0), stmt, "HY009"); } diff --git a/odbc/tests/unit/sql_like_ut.cpp b/odbc/tests/unit/sql_like_ut.cpp index 34858652d4..293617392d 100644 --- a/odbc/tests/unit/sql_like_ut.cpp +++ b/odbc/tests/unit/sql_like_ut.cpp @@ -33,9 +33,6 @@ TEST(SqlLikeMatch, BackslashEscapesWildcards) { EXPECT_TRUE(SqlLikeMatch("rate%", "rate\\%")); EXPECT_FALSE(SqlLikeMatch("rate123", "rate\\%")); EXPECT_TRUE(SqlLikeMatch("a\\b", "a\\\\b")); -} - -TEST(SqlLikeMatch, PercentBacktrackingHonorsEscapes) { EXPECT_TRUE(SqlLikeMatch("prefix_value_suffix", "%\\_value\\_%")); EXPECT_FALSE(SqlLikeMatch("prefixXvalueYsuffix", "%\\_value\\_%")); } From 86e379ddee1cea2fc6df20d31c283d322a8dc997 Mon Sep 17 00:00:00 2001 From: Artem Ermoshkin Date: Mon, 28 Sep 2026 17:23:58 +0300 Subject: [PATCH 3/5] YDBAPPTEAM-1843 gate unique index integration coverage by server capability --- odbc/tests/integration/metadata_api_it.cpp | 45 +++++++++++++++++----- 1 file changed, 35 insertions(+), 10 deletions(-) diff --git a/odbc/tests/integration/metadata_api_it.cpp b/odbc/tests/integration/metadata_api_it.cpp index e02d74f665..828de97cf6 100644 --- a/odbc/tests/integration/metadata_api_it.cpp +++ b/odbc/tests/integration/metadata_api_it.cpp @@ -653,13 +653,12 @@ TEST(MetadataApi, SQLStatisticsReportsIndexesAndHonorsOptions) { ExecuteAndClose(stmt, "DROP TABLE IF EXISTS test_statistics_metadata"); ExecuteAndClose(stmt, "CREATE TABLE test_statistics_metadata (" - "id Int32, email Utf8 NOT NULL, city Utf8, created Timestamp, " + "id Int32, city Utf8, created Timestamp, " "INDEX idx_city_created GLOBAL SYNC ON (city, created), " - "INDEX idx_email GLOBAL UNIQUE SYNC ON (email), " "PRIMARY KEY (id))"); ExecuteAndClose(stmt, - "UPSERT INTO test_statistics_metadata (id, email, city) VALUES " - "(1, 'one@example.test', 'A'), (2, 'two@example.test', 'B')"); + "UPSERT INTO test_statistics_metadata (id, city) VALUES " + "(1, 'A'), (2, 'B')"); SQLCHAR tableName[] = "test_statistics_metadata"; CHECK_ODBC_OK(SQLStatistics( @@ -670,11 +669,6 @@ TEST(MetadataApi, SQLStatisticsReportsIndexesAndHonorsOptions) { {3, "test_statistics_metadata"}, {4, nullptr}, {6, nullptr}, {7, SQL_TABLE_STAT}, {8, nullptr}, {9, nullptr}, {10, nullptr}, {11, nullptr}, {12, nullptr}, {13, nullptr}, }); - ExpectRow(stmt, { - {4, SQL_FALSE}, {6, "idx_email"}, {7, SQL_INDEX_OTHER}, {8, 1}, - {9, "email"}, {11, nullptr}, - }); - const std::array regularIndexColumns{"city", "created"}; for (size_t i = 0; i < regularIndexColumns.size(); ++i) { ExpectRow(stmt, { @@ -689,7 +683,38 @@ TEST(MetadataApi, SQLStatisticsReportsIndexesAndHonorsOptions) { stmt, nullptr, 0, nullptr, 0, tableName, SQL_NTS, SQL_INDEX_UNIQUE, SQL_ENSURE), stmt, SQL_HANDLE_STMT); ExpectRow(stmt, {{7, SQL_TABLE_STAT}, {11, AnyValue}}); - ExpectRow(stmt, {{4, SQL_FALSE}, {6, "idx_email"}, {9, "email"}}); + FinishResult(stmt); +} + +TEST(MetadataApi, SQLStatisticsUniqueIndexWhenSupported) { + TMetadataHandles handles; + const SQLHSTMT stmt = handles.Stmt; + + ExecuteAndClose(stmt, "DROP TABLE IF EXISTS test_statistics_unique_metadata"); + const char* createTable = + "CREATE TABLE test_statistics_unique_metadata (" + "id Int32, email Utf8 NOT NULL, " + "INDEX idx_email GLOBAL UNIQUE SYNC ON (email), " + "PRIMARY KEY (id))"; + const SQLRETURN rc = SQLExecDirect( + stmt, reinterpret_cast(const_cast(createTable)), SQL_NTS); + if (rc == SQL_ERROR) { + const std::string error = GetOdbcError(stmt, SQL_HANDLE_STMT); + if (error.find("Unique constraint feature is disabled") != std::string::npos) { + GTEST_SKIP() << error; + } + } + CHECK_ODBC_OK(rc, stmt, SQL_HANDLE_STMT); + ASSERT_EQ(SQLFreeStmt(stmt, SQL_CLOSE), SQL_SUCCESS); + + SQLCHAR tableName[] = "test_statistics_unique_metadata"; + CHECK_ODBC_OK(SQLStatistics( + stmt, nullptr, 0, nullptr, 0, tableName, SQL_NTS, + SQL_INDEX_UNIQUE, SQL_QUICK), stmt, SQL_HANDLE_STMT); + ExpectRow(stmt, {{7, SQL_TABLE_STAT}}); + ExpectRow(stmt, { + {4, SQL_FALSE}, {6, "idx_email"}, {7, SQL_INDEX_OTHER}, {8, 1}, {9, "email"}, + }); FinishResult(stmt); } From 2ccacef6c5122b5d4b42009f810e8209cc38d941 Mon Sep 17 00:00:00 2001 From: Ermoshkin Artem <94714022+Shfdis@users.noreply.github.com> Date: Tue, 29 Sep 2026 15:42:56 +0300 Subject: [PATCH 4/5] YDBAPPTEAM-1843 address ODBC metadata review feedback --- odbc/src/statement.h | 3 +- odbc/src/statement_metadata.cpp | 47 ++++++++++++++++------ odbc/src/utils/sql_like.h | 4 +- odbc/tests/integration/metadata_api_it.cpp | 43 +++++++++++++++++++- odbc/tests/unit/sql_like_ut.cpp | 7 ++++ 5 files changed, 89 insertions(+), 15 deletions(-) diff --git a/odbc/src/statement.h b/odbc/src/statement.h index 244a08210d..ed692c8def 100644 --- a/odbc/src/statement.h +++ b/odbc/src/statement.h @@ -14,6 +14,7 @@ #include #include #include +#include namespace NYdb::NOdbc { @@ -177,7 +178,7 @@ class TStatement : public TErrorManager { std::vector GetMetadataEntries(const TMetadataArgument& tableName, bool patternsAllowed); void VisitEntry(const std::string& path, const std::string& tableName, - bool patternsAllowed, + bool patternsAllowed, std::string_view literalPrefix, bool hasWildcard, std::vector& resultEntries); std::optional GetTableType(NScheme::ESchemeEntryType type); }; diff --git a/odbc/src/statement_metadata.cpp b/odbc/src/statement_metadata.cpp index 279c974cc9..5b3a7ef176 100644 --- a/odbc/src/statement_metadata.cpp +++ b/odbc/src/statement_metadata.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include namespace NYdb::NOdbc { @@ -177,11 +178,29 @@ TOdbcScalar Maybe(const std::optional& value) { } TOdbcScalar Int32OrNull(uint64_t value) { + // CARDINALITY is SQL_INTEGER; NULL is safer than reporting a clipped row count. return value <= static_cast(std::numeric_limits::max()) ? I(static_cast(value)) : Null(); } +std::pair GetLiteralPrefix(std::string_view pattern) { + std::string prefix; + for (size_t i = 0; i < pattern.size(); ++i) { + const char ch = pattern[i]; + if (ch == '\\' && i + 1 < pattern.size() + && (pattern[i + 1] == '%' || pattern[i + 1] == '_' + || pattern[i + 1] == '\\')) { + prefix.push_back(pattern[++i]); + } else if (ch == '%' || ch == '_') { + return {prefix, true}; + } else { + prefix.push_back(ch); + } + } + return {prefix, false}; +} + bool IsExplicitEmpty(const TMetadataArgument& value) { return value && value->empty(); } @@ -385,14 +404,15 @@ SQLRETURN TStatement::Tables(const TMetadataArgument& catalogName, const bool emptySchema = IsExplicitEmpty(schemaName); const bool emptyTable = IsExplicitEmpty(tableName); const bool emptyType = IsExplicitEmpty(tableType); - if (IsSpecialValue(catalogName, SQL_ALL_CATALOGS) + const bool patternsAllowed = Attributes_.GetMetadataId() != SQL_TRUE; + if (patternsAllowed && IsSpecialValue(catalogName, SQL_ALL_CATALOGS) && emptySchema && emptyTable && emptyType) { SetCursor(CreateVirtualCursor(kTablesSchema, {{ GetMetadataCatalogName(Conn_), Null(), Null(), Null(), Null(), }})); return SQL_SUCCESS; } - if (IsSpecialValue(schemaName, SQL_ALL_SCHEMAS) + if (patternsAllowed && IsSpecialValue(schemaName, SQL_ALL_SCHEMAS) && IsExplicitEmpty(catalogName) && emptyTable && emptyType) { SetCursor(CreateVirtualCursor(kTablesSchema)); return SQL_SUCCESS; @@ -412,7 +432,6 @@ SQLRETURN TStatement::Tables(const TMetadataArgument& catalogName, return SQL_SUCCESS; } - const bool patternsAllowed = Attributes_.GetMetadataId() != SQL_TRUE; if (!MetadataNamespaceMatches(catalogName, schemaName, patternsAllowed, patternsAllowed)) { SetCursor(CreateVirtualCursor(kTablesSchema)); @@ -421,18 +440,16 @@ SQLRETURN TStatement::Tables(const TMetadataArgument& catalogName, const std::string catalog = GetMetadataCatalogName(Conn_); TTable table; - auto entries = GetMetadataEntries(tableName, patternsAllowed); - std::ranges::sort(entries, [this](const auto& lhs, const auto& rhs) { - return std::pair{GetTableType(lhs.Type).value_or(""), GetMetadataTableName(lhs.Name)} - < std::pair{GetTableType(rhs.Type).value_or(""), GetMetadataTableName(rhs.Name)}; - }); const std::string_view typeFilter = tableType ? std::string_view(*tableType) : std::string_view{}; - for (const auto& entry : entries) { + for (const auto& entry : GetMetadataEntries(tableName, patternsAllowed)) { const auto type = GetTableType(entry.Type); if (type && MatchesTableTypeFilter(typeFilter, *type)) { table.push_back({catalog, Null(), GetMetadataTableName(entry.Name), *type, Null()}); } } + std::ranges::sort(table, {}, [](const auto& row) { + return std::tie(std::get(row[3]), std::get(row[2])); + }); SetCursor(CreateVirtualCursor(kTablesSchema, std::move(table))); return SQL_SUCCESS; } @@ -611,8 +628,10 @@ std::vector TStatement::GetMetadataEntries( } } + const auto [literalPrefix, hasWildcard] = patternsAllowed + ? GetLiteralPrefix(qualifiedName) : std::pair{qualifiedName, false}; VisitEntry(catalog.empty() ? "/" : catalog, qualifiedName, - tableName ? patternsAllowed : true, entries); + patternsAllowed, literalPrefix, hasWildcard, entries); std::ranges::sort(entries, {}, &NScheme::TSchemeEntry::Name); return entries; } @@ -647,7 +666,8 @@ bool TStatement::MetadataNamespaceMatches( } void TStatement::VisitEntry(const std::string& path, const std::string& tableName, - bool patternsAllowed, + bool patternsAllowed, std::string_view literalPrefix, + bool hasWildcard, std::vector& result) { auto client = Conn_->GetSchemeClient(); if (!client) { @@ -659,7 +679,10 @@ void TStatement::VisitEntry(const std::string& path, const std::string& tableNam const std::string fullPath = path == "/" ? path + entry.Name : path + "/" + entry.Name; if (entry.Type == NScheme::ESchemeEntryType::Directory || entry.Type == NScheme::ESchemeEntryType::SubDomain) { - VisitEntry(fullPath, tableName, patternsAllowed, result); + if (tableName.empty() || literalPrefix.starts_with(fullPath + "/") + || (hasWildcard && fullPath.starts_with(literalPrefix))) { + VisitEntry(fullPath, tableName, patternsAllowed, literalPrefix, hasWildcard, result); + } } else if (tableName.empty() || (patternsAllowed ? SqlLikeMatch(fullPath, tableName) : fullPath == tableName)) { diff --git a/odbc/src/utils/sql_like.h b/odbc/src/utils/sql_like.h index 9bd98e1e2b..33d1418837 100644 --- a/odbc/src/utils/sql_like.h +++ b/odbc/src/utils/sql_like.h @@ -25,7 +25,9 @@ inline bool SqlLikeMatch(std::string_view text, std::string_view pattern) { size_t nextPat = patPos + 1; char expected = pattern[patPos]; bool anyCharacter = expected == '_'; - if (expected == '\\' && nextPat < patLen) { + if (expected == '\\' && nextPat < patLen + && (pattern[nextPat] == '%' || pattern[nextPat] == '_' + || pattern[nextPat] == '\\')) { expected = pattern[nextPat++]; anyCharacter = false; } diff --git a/odbc/tests/integration/metadata_api_it.cpp b/odbc/tests/integration/metadata_api_it.cpp index 828de97cf6..d79ecd49c0 100644 --- a/odbc/tests/integration/metadata_api_it.cpp +++ b/odbc/tests/integration/metadata_api_it.cpp @@ -524,6 +524,17 @@ TEST(MetadataApi, SQLTablesSpecialEnumerationsAndEmptyArguments) { stmt, nullptr, 0, nullptr, 0, empty, 0, nullptr, 0), stmt, SQL_HANDLE_STMT); FinishResult(stmt); + + CHECK_ODBC_OK(SQLSetStmtAttr( + stmt, SQL_ATTR_METADATA_ID, (SQLPOINTER)(uintptr_t)SQL_TRUE, 0), + stmt, SQL_HANDLE_STMT); + CHECK_ODBC_OK(SQLTables( + stmt, all, SQL_NTS, empty, 0, empty, 0, empty, 0), stmt, SQL_HANDLE_STMT); + FinishResult(stmt); + CHECK_ODBC_OK(SQLTables( + stmt, empty, 0, empty, 0, empty, 0, all, SQL_NTS), stmt, SQL_HANDLE_STMT); + ExpectRow(stmt, {{4, AnyValue}}); + ASSERT_EQ(SQLFreeStmt(stmt, SQL_CLOSE), SQL_SUCCESS); } TEST(MetadataApi, SQLColumnsFieldsAndCompositePrimaryKey) { @@ -644,6 +655,27 @@ TEST(MetadataApi, MetadataUsesCurrentCatalogForSameNamedTables) { (SQLCHAR*)"b_value", SQL_NTS), stmt, SQL_HANDLE_STMT); ExpectRow(stmt, {{3, "cat_b/catalog_metadata_same"}, {4, "b_value"}}); FinishResult(stmt); + + SQLCHAR allTables[] = "%"; + CHECK_ODBC_OK(SQLTables( + stmt, nullptr, 0, nullptr, 0, allTables, SQL_NTS, + (SQLCHAR*)"TABLE", SQL_NTS), stmt, SQL_HANDLE_STMT); + std::set names; + while (SQLFetch(stmt) == SQL_SUCCESS) { + const auto name = ReadText(stmt, 3); + ASSERT_TRUE(name); + names.insert(*name); + } + EXPECT_TRUE(names.contains("cat_a/catalog_metadata_same")); + EXPECT_TRUE(names.contains("cat_b/catalog_metadata_same")); + ASSERT_EQ(SQLFreeStmt(stmt, SQL_CLOSE), SQL_SUCCESS); + + SQLCHAR nestedPattern[] = "cat_a/catalog_metadata_%"; + CHECK_ODBC_OK(SQLTables( + stmt, nullptr, 0, nullptr, 0, nestedPattern, SQL_NTS, + (SQLCHAR*)"TABLE", SQL_NTS), stmt, SQL_HANDLE_STMT); + ExpectRow(stmt, {{3, "cat_a/catalog_metadata_same"}}); + FinishResult(stmt); } TEST(MetadataApi, SQLStatisticsReportsIndexesAndHonorsOptions) { @@ -693,7 +725,8 @@ TEST(MetadataApi, SQLStatisticsUniqueIndexWhenSupported) { ExecuteAndClose(stmt, "DROP TABLE IF EXISTS test_statistics_unique_metadata"); const char* createTable = "CREATE TABLE test_statistics_unique_metadata (" - "id Int32, email Utf8 NOT NULL, " + "id Int32, email Utf8 NOT NULL, city Utf8, " + "INDEX idx_city GLOBAL SYNC ON (city), " "INDEX idx_email GLOBAL UNIQUE SYNC ON (email), " "PRIMARY KEY (id))"; const SQLRETURN rc = SQLExecDirect( @@ -708,6 +741,14 @@ TEST(MetadataApi, SQLStatisticsUniqueIndexWhenSupported) { ASSERT_EQ(SQLFreeStmt(stmt, SQL_CLOSE), SQL_SUCCESS); SQLCHAR tableName[] = "test_statistics_unique_metadata"; + CHECK_ODBC_OK(SQLStatistics( + stmt, nullptr, 0, nullptr, 0, tableName, SQL_NTS, + SQL_INDEX_ALL, SQL_QUICK), stmt, SQL_HANDLE_STMT); + ExpectRow(stmt, {{7, SQL_TABLE_STAT}}); + ExpectRow(stmt, {{4, SQL_FALSE}, {6, "idx_email"}, {8, 1}, {9, "email"}}); + ExpectRow(stmt, {{4, SQL_TRUE}, {6, "idx_city"}, {8, 1}, {9, "city"}}); + FinishResult(stmt); + CHECK_ODBC_OK(SQLStatistics( stmt, nullptr, 0, nullptr, 0, tableName, SQL_NTS, SQL_INDEX_UNIQUE, SQL_QUICK), stmt, SQL_HANDLE_STMT); diff --git a/odbc/tests/unit/sql_like_ut.cpp b/odbc/tests/unit/sql_like_ut.cpp index 293617392d..48bdffbe46 100644 --- a/odbc/tests/unit/sql_like_ut.cpp +++ b/odbc/tests/unit/sql_like_ut.cpp @@ -36,3 +36,10 @@ TEST(SqlLikeMatch, BackslashEscapesWildcards) { EXPECT_TRUE(SqlLikeMatch("prefix_value_suffix", "%\\_value\\_%")); EXPECT_FALSE(SqlLikeMatch("prefixXvalueYsuffix", "%\\_value\\_%")); } + +TEST(SqlLikeMatch, BackslashWithoutSpecialCharacterIsLiteral) { + EXPECT_TRUE(SqlLikeMatch("a\\b", "a\\b")); + EXPECT_FALSE(SqlLikeMatch("ab", "a\\b")); + EXPECT_TRUE(SqlLikeMatch("abc\\", "abc\\")); + EXPECT_FALSE(SqlLikeMatch("abc", "abc\\")); +} From bb229783cfae448f05ef5a4feb75a056279a9bc0 Mon Sep 17 00:00:00 2001 From: Ermoshkin Artem <94714022+Shfdis@users.noreply.github.com> Date: Wed, 30 Sep 2026 15:29:06 +0300 Subject: [PATCH 5/5] YDBAPPTEAM-1843 address follow-up ODBC metadata review --- odbc/src/odbc_driver.cpp | 4 +- odbc/src/statement.h | 6 +-- odbc/src/statement_metadata.cpp | 29 ++++++++---- odbc/tests/integration/metadata_api_it.cpp | 55 ++++++++++++++++++++++ 4 files changed, 80 insertions(+), 14 deletions(-) diff --git a/odbc/src/odbc_driver.cpp b/odbc/src/odbc_driver.cpp index 77ed9bd91c..77dcb1d64d 100644 --- a/odbc/src/odbc_driver.cpp +++ b/odbc/src/odbc_driver.cpp @@ -389,8 +389,8 @@ ODBC_FORWARD(SQLSpecialColumns, TStatement, TStatement::SpecialColumns, (SQLHSTMT statementHandle, SQLUSMALLINT identifierType, SQLCHAR* catalogName, SQLSMALLINT nameLength1, SQLCHAR* schemaName, SQLSMALLINT nameLength2, SQLCHAR* tableName, SQLSMALLINT nameLength3, SQLUSMALLINT scope, SQLUSMALLINT), - (statementHandle, Text(catalogName, nameLength1), Text(schemaName, nameLength2), - Text(tableName, nameLength3), identifierType, scope)) + (statementHandle, OptionalText(catalogName, nameLength1), OptionalText(schemaName, nameLength2), + OptionalText(tableName, nameLength3), identifierType, scope)) ODBC_FORWARD(SQLColAttribute, TStatement, Odbc::NMetadata::ColAttribute, (SQLHSTMT statementHandle, SQLUSMALLINT columnNumber, SQLUSMALLINT fieldIdentifier, diff --git a/odbc/src/statement.h b/odbc/src/statement.h index ed692c8def..18147ccb38 100644 --- a/odbc/src/statement.h +++ b/odbc/src/statement.h @@ -54,9 +54,9 @@ class TStatement : public TErrorManager { const TMetadataArgument& tableName, SQLUSMALLINT unique, SQLUSMALLINT accuracy); - SQLRETURN SpecialColumns(const std::string& catalogName, - const std::string& schemaName, - const std::string& tableName, + SQLRETURN SpecialColumns(const TMetadataArgument& catalogName, + const TMetadataArgument& schemaName, + const TMetadataArgument& tableName, SQLUSMALLINT identifierType, SQLUSMALLINT scope); SQLRETURN PrimaryKeys(const TMetadataArgument& catalogName, const TMetadataArgument& schemaName, diff --git a/odbc/src/statement_metadata.cpp b/odbc/src/statement_metadata.cpp index 5b3a7ef176..254b6ccfa7 100644 --- a/odbc/src/statement_metadata.cpp +++ b/odbc/src/statement_metadata.cpp @@ -432,6 +432,11 @@ SQLRETURN TStatement::Tables(const TMetadataArgument& catalogName, return SQL_SUCCESS; } + if (emptyType) { + SetCursor(CreateVirtualCursor(kTablesSchema)); + return SQL_SUCCESS; + } + if (!MetadataNamespaceMatches(catalogName, schemaName, patternsAllowed, patternsAllowed)) { SetCursor(CreateVirtualCursor(kTablesSchema)); @@ -524,23 +529,26 @@ SQLRETURN TStatement::Statistics(const TMetadataArgument& catalogName, return SQL_SUCCESS; } -SQLRETURN TStatement::SpecialColumns(const std::string& catalogName, const std::string& schemaName, - const std::string& tableName, SQLUSMALLINT identifierType, +SQLRETURN TStatement::SpecialColumns(const TMetadataArgument& catalogName, + const TMetadataArgument& schemaName, + const TMetadataArgument& tableName, SQLUSMALLINT identifierType, SQLUSMALLINT) { if (identifierType != SQL_BEST_ROWID) { return AddError("HYC00", 0, "Optional feature not implemented"); } + if (!tableName) { + return AddError("HY009", 0, "TableName must not be null"); + } + if (Attributes_.GetMetadataId() == SQL_TRUE && (!catalogName || !schemaName)) { + return AddError("HY009", 0, "Identifier arguments must not be null"); + } ResetForMetadata(); - const std::optional catalog = catalogName.empty() - ? std::nullopt : std::optional{catalogName}; - const std::optional schema = schemaName.empty() - ? std::nullopt : std::optional{schemaName}; - if (!MetadataNamespaceMatches(catalog, schema, false, false)) { + if (!MetadataNamespaceMatches(catalogName, schemaName, false, false)) { SetCursor(CreateVirtualCursor(kSpecialColumnsSchema)); return SQL_SUCCESS; } - auto entries = GetMetadataEntries(std::optional{tableName}, false); + auto entries = GetMetadataEntries(tableName, false); if (entries.size() > 1) { throw TOdbcException("HY000", 0, "Ambiguous table name"); } @@ -679,7 +687,10 @@ void TStatement::VisitEntry(const std::string& path, const std::string& tableNam const std::string fullPath = path == "/" ? path + entry.Name : path + "/" + entry.Name; if (entry.Type == NScheme::ESchemeEntryType::Directory || entry.Type == NScheme::ESchemeEntryType::SubDomain) { - if (tableName.empty() || literalPrefix.starts_with(fullPath + "/") + if (tableName.empty() + || (literalPrefix.size() > fullPath.size() + && literalPrefix.starts_with(fullPath) + && literalPrefix[fullPath.size()] == '/') || (hasWildcard && fullPath.starts_with(literalPrefix))) { VisitEntry(fullPath, tableName, patternsAllowed, literalPrefix, hasWildcard, result); } diff --git a/odbc/tests/integration/metadata_api_it.cpp b/odbc/tests/integration/metadata_api_it.cpp index d79ecd49c0..ffa6672dd1 100644 --- a/odbc/tests/integration/metadata_api_it.cpp +++ b/odbc/tests/integration/metadata_api_it.cpp @@ -457,6 +457,12 @@ TEST(MetadataApi, SQLTablesFilterByType) { stmt, SQL_HANDLE_STMT); ASSERT_EQ(SQLFetch(stmt), SQL_SUCCESS); ASSERT_EQ(SQLFetch(stmt), SQL_NO_DATA); + SQLFreeStmt(stmt, SQL_CLOSE); + CHECK_ODBC_OK(SQLTables(stmt, nullptr, 0, nullptr, 0, + (SQLCHAR*)"/local/test_type_filter", SQL_NTS, + (SQLCHAR*)"", 0), + stmt, SQL_HANDLE_STMT); + ASSERT_EQ(SQLFetch(stmt), SQL_NO_DATA); SQLFreeHandle(SQL_HANDLE_STMT, stmt); SQLDisconnect(dbc); SQLFreeHandle(SQL_HANDLE_DBC, dbc); @@ -580,6 +586,18 @@ TEST(MetadataApi, SQLColumnsFieldsAndCompositePrimaryKey) { } FinishResult(stmt); + SQLCHAR catalog[] = "local"; + SQLCHAR catalogPattern[] = "loca%"; + CHECK_ODBC_OK(SQLColumns( + stmt, catalog, SQL_NTS, nullptr, 0, tableName, SQL_NTS, + (SQLCHAR*)"pk_a", SQL_NTS), stmt, SQL_HANDLE_STMT); + ExpectRow(stmt, {{4, "pk_a"}}); + FinishResult(stmt); + CHECK_ODBC_OK(SQLColumns( + stmt, catalogPattern, SQL_NTS, nullptr, 0, tableName, SQL_NTS, + (SQLCHAR*)"pk_a", SQL_NTS), stmt, SQL_HANDLE_STMT); + FinishResult(stmt); + CHECK_ODBC_OK(SQLPrimaryKeys( stmt, nullptr, 0, nullptr, 0, tableName, SQL_NTS), stmt, SQL_HANDLE_STMT); const std::array expectedKeys{"pk_b", "pk_a"}; @@ -779,3 +797,40 @@ TEST(MetadataApi, RequiredTableArgumentsAndStatisticsOptionsAreValidated) { ExpectError(SQLTables( stmt, nullptr, 0, nullptr, 0, tableName, SQL_NTS, nullptr, 0), stmt, "HY009"); } + +TEST(MetadataApi, SQLSpecialColumnsDistinguishesNullAndEmptyArguments) { + TMetadataHandles handles; + const SQLHSTMT stmt = handles.Stmt; + + ExecuteAndClose(stmt, "DROP TABLE IF EXISTS test_special_columns_metadata"); + ExecuteAndClose(stmt, + "CREATE TABLE test_special_columns_metadata (id Int32, PRIMARY KEY (id))"); + + SQLCHAR tableName[] = "test_special_columns_metadata"; + SQLCHAR catalog[] = "local"; + SQLCHAR empty[] = ""; + ExpectError(SQLSpecialColumns( + stmt, SQL_BEST_ROWID, nullptr, 0, nullptr, 0, nullptr, 0, + SQL_SCOPE_SESSION, SQL_NULLABLE), stmt, "HY009"); + + CHECK_ODBC_OK(SQLSpecialColumns( + stmt, SQL_BEST_ROWID, empty, 0, nullptr, 0, tableName, SQL_NTS, + SQL_SCOPE_SESSION, SQL_NULLABLE), stmt, SQL_HANDLE_STMT); + FinishResult(stmt); + + CHECK_ODBC_OK(SQLSetStmtAttr( + stmt, SQL_ATTR_METADATA_ID, (SQLPOINTER)(uintptr_t)SQL_TRUE, 0), + stmt, SQL_HANDLE_STMT); + ExpectError(SQLSpecialColumns( + stmt, SQL_BEST_ROWID, nullptr, 0, empty, 0, tableName, SQL_NTS, + SQL_SCOPE_SESSION, SQL_NULLABLE), stmt, "HY009"); + ExpectError(SQLSpecialColumns( + stmt, SQL_BEST_ROWID, catalog, SQL_NTS, nullptr, 0, tableName, SQL_NTS, + SQL_SCOPE_SESSION, SQL_NULLABLE), stmt, "HY009"); + + CHECK_ODBC_OK(SQLSpecialColumns( + stmt, SQL_BEST_ROWID, catalog, SQL_NTS, empty, 0, tableName, SQL_NTS, + SQL_SCOPE_SESSION, SQL_NULLABLE), stmt, SQL_HANDLE_STMT); + ExpectRow(stmt, {{2, "id"}}); + FinishResult(stmt); +}