Skip to content

Commit 8c10ebe

Browse files
authored
Merge pull request #838 from Altinity/backports/24.3.18/80334
24.3 Port of ClickHouse#80334 - Allow to use named collections in ODBC/JDBC
2 parents 9ab826c + 585c2b1 commit 8c10ebe

File tree

8 files changed

+180
-24
lines changed

8 files changed

+180
-24
lines changed

docs/en/engines/table-engines/integrations/jdbc.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ ENGINE = JDBC(datasource_uri, external_database, external_table)
3939

4040
- `external_table` — Name of the table in `external_database` or a select query like `select * from table1 where column1=1`.
4141

42+
- These parameters can also be passed using [named collections](operations/named-collections.md).
43+
4244
## Usage Example {#usage-example}
4345

4446
Creating a table in MySQL server by connecting directly with it’s console client:

docs/en/engines/table-engines/integrations/odbc.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ The table structure can differ from the source table structure:
3838
- `external_database` — Name of a database in an external DBMS.
3939
- `external_table` — Name of a table in the `external_database`.
4040

41+
These parameters can also be passed using [named collections](operations/named-collections.md).
42+
4143
## Usage Example {#usage-example}
4244

4345
**Retrieving data from the local MySQL installation via ODBC**

docs/en/sql-reference/table-functions/jdbc.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,19 @@ clickhouse-jdbc-bridge contains experimental codes and is no longer supported. I
1111
ClickHouse recommend using built-in table functions in ClickHouse which provide a better alternative for ad-hoc querying scenarios (Postgres, MySQL, MongoDB, etc).
1212
:::
1313

14-
`jdbc(datasource, schema, table)` - returns table that is connected via JDBC driver.
14+
JDBC table function returns table that is connected via JDBC driver.
1515

1616
This table function requires separate [clickhouse-jdbc-bridge](https://github.com/ClickHouse/clickhouse-jdbc-bridge) program to be running.
1717
It supports Nullable types (based on DDL of remote table that is queried).
1818

19+
## Syntax {#syntax}
20+
21+
```sql
22+
jdbc(datasource, schema, table)
23+
jdbc(datasource, table)
24+
jdbc(named_collection)
25+
```
26+
1927
**Examples**
2028

2129
``` sql

docs/en/sql-reference/table-functions/odbc.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ Returns table that is connected via [ODBC](https://en.wikipedia.org/wiki/Open_Da
1010

1111
``` sql
1212
odbc(connection_settings, external_database, external_table)
13+
odbc(connection_settings, external_table)
14+
odbc(named_collection)
1315
```
1416

1517
Parameters:
@@ -18,6 +20,8 @@ Parameters:
1820
- `external_database` — Name of a database in an external DBMS.
1921
- `external_table` — Name of a table in the `external_database`.
2022

23+
These parameters can also be passed using [named collections](operations/named-collections.md).
24+
2125
To safely implement ODBC connections, ClickHouse uses a separate program `clickhouse-odbc-bridge`. If the ODBC driver is loaded directly from `clickhouse-server`, driver problems can crash the ClickHouse server. ClickHouse automatically starts `clickhouse-odbc-bridge` when it is required. The ODBC bridge program is installed from the same package as the `clickhouse-server`.
2226

2327
The fields with the `NULL` values from the external table are converted into the default values for the base data type. For example, if a remote MySQL table field has the `INT NULL` type it is converted to 0 (the default value for ClickHouse `Int32` data type).

src/Storages/StorageXDBC.cpp

Lines changed: 45 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#include <Storages/StorageURL.h>
44
#include <Storages/transformQueryForExternalDatabase.h>
55
#include <Storages/checkAndGetLiteralArgument.h>
6+
#include <Storages/NamedCollectionsHelpers.h>
67

78
#include <Formats/FormatFactory.h>
89
#include <IO/ConnectionTimeouts.h>
@@ -171,21 +172,54 @@ namespace
171172
{
172173
ASTs & engine_args = args.engine_args;
173174

174-
if (engine_args.size() != 3)
175-
throw Exception(ErrorCodes::NUMBER_OF_ARGUMENTS_DOESNT_MATCH,
176-
"Storage {} requires exactly 3 parameters: {}('DSN', database or schema, table)", name, name);
177-
178-
for (size_t i = 0; i < 3; ++i)
179-
engine_args[i] = evaluateConstantExpressionOrIdentifierAsLiteral(engine_args[i], args.getLocalContext());
180-
181-
BridgeHelperPtr bridge_helper = std::make_shared<XDBCBridgeHelper<BridgeHelperMixin>>(args.getContext(),
175+
String connection_string;
176+
String database_or_schema;
177+
String table;
178+
179+
if (auto named_collection = tryGetNamedCollectionWithOverrides(engine_args, args.getLocalContext()))
180+
{
181+
if (name == "JDBC")
182+
{
183+
validateNamedCollection<>(*named_collection, {"datasource", "schema", "table"}, {});
184+
connection_string = named_collection->get<String>("datasource");
185+
database_or_schema = named_collection->get<String>("schema");
186+
table = named_collection->get<String>("table");
187+
}
188+
else
189+
{
190+
validateNamedCollection<>(*named_collection, {"connection_settings", "external_database", "external_table"}, {});
191+
connection_string = named_collection->get<String>("connection_settings");
192+
database_or_schema = named_collection->get<String>("external_database");
193+
table = named_collection->get<String>("external_table");
194+
}
195+
}
196+
else
197+
{
198+
if (engine_args.size() != 3)
199+
throw Exception(
200+
ErrorCodes::NUMBER_OF_ARGUMENTS_DOESNT_MATCH,
201+
"Storage {} requires exactly 3 parameters: {}('DSN', database or schema, table)",
202+
name,
203+
name);
204+
205+
for (size_t i = 0; i < 3; ++i)
206+
engine_args[i] = evaluateConstantExpressionOrIdentifierAsLiteral(engine_args[i], args.getLocalContext());
207+
208+
connection_string = checkAndGetLiteralArgument<String>(engine_args[0], "connection_string");
209+
database_or_schema = checkAndGetLiteralArgument<String>(engine_args[1], "database_name");
210+
table = checkAndGetLiteralArgument<String>(engine_args[2], "table_name");
211+
}
212+
213+
BridgeHelperPtr bridge_helper = std::make_shared<XDBCBridgeHelper<BridgeHelperMixin>>(
214+
args.getContext(),
182215
args.getContext()->getSettingsRef().http_receive_timeout.value,
183-
checkAndGetLiteralArgument<String>(engine_args[0], "connection_string"),
216+
connection_string,
184217
args.getContext()->getSettingsRef().odbc_bridge_use_connection_pooling.value);
218+
185219
return std::make_shared<StorageXDBC>(
186220
args.table_id,
187-
checkAndGetLiteralArgument<String>(engine_args[1], "database_name"),
188-
checkAndGetLiteralArgument<String>(engine_args[2], "table_name"),
221+
database_or_schema,
222+
table,
189223
args.columns,
190224
args.constraints,
191225
args.comment,

src/TableFunctions/ITableFunctionXDBC.cpp

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#include <Parsers/ASTLiteral.h>
99
#include <Parsers/parseQuery.h>
1010
#include <Storages/StorageXDBC.h>
11+
#include <Storages/NamedCollectionsHelpers.h>
1112
#include <TableFunctions/ITableFunction.h>
1213
#include <TableFunctions/TableFunctionFactory.h>
1314
#include <Poco/Net/HTTPRequest.h>
@@ -26,6 +27,7 @@ namespace ErrorCodes
2627
{
2728
extern const int NUMBER_OF_ARGUMENTS_DOESNT_MATCH;
2829
extern const int LOGICAL_ERROR;
30+
extern const int BAD_ARGUMENTS;
2931
}
3032

3133
namespace
@@ -109,23 +111,54 @@ void ITableFunctionXDBC::parseArguments(const ASTPtr & ast_function, ContextPtr
109111
throw Exception(ErrorCodes::LOGICAL_ERROR, "Table function '{}' must have arguments.", getName());
110112

111113
ASTs & args = args_func.arguments->children;
112-
if (args.size() != 2 && args.size() != 3)
113-
throw Exception(ErrorCodes::NUMBER_OF_ARGUMENTS_DOESNT_MATCH,
114-
"Table function '{0}' requires 2 or 3 arguments: {0}('DSN', table) or {0}('DSN', schema, table)", getName());
115114

116-
for (auto & arg : args)
117-
arg = evaluateConstantExpressionOrIdentifierAsLiteral(arg, context);
115+
if (args.empty() || args.size() > 3)
116+
throw Exception(ErrorCodes::NUMBER_OF_ARGUMENTS_DOESNT_MATCH,
117+
"Table function '{0}' requires 1, 2 or 3 arguments: {0}(named_collection) or {0}('DSN', table) or {0}('DSN', schema, table)", getName());
118118

119-
if (args.size() == 3)
119+
if (args.size() == 1)
120120
{
121-
connection_string = args[0]->as<ASTLiteral &>().value.safeGet<String>();
122-
schema_name = args[1]->as<ASTLiteral &>().value.safeGet<String>();
123-
remote_table_name = args[2]->as<ASTLiteral &>().value.safeGet<String>();
121+
if (auto named_collection = tryGetNamedCollectionWithOverrides(ast_function->children.at(0)->children, context))
122+
{
123+
if (getName() == "JDBC")
124+
{
125+
validateNamedCollection<>(*named_collection, {"datasource"}, {"schema", "table"});
126+
connection_string = named_collection->get<String>("datasource");
127+
schema_name = named_collection->getOrDefault<String>("schema", "");
128+
remote_table_name = named_collection->getOrDefault<String>("table", "");
129+
}
130+
else
131+
{
132+
validateNamedCollection<>(*named_collection, {"connection_settings"}, {"external_database", "external_table"});
133+
134+
connection_string = named_collection->get<String>("connection_settings");
135+
schema_name = named_collection->getOrDefault<String>("external_database", "");
136+
remote_table_name = named_collection->getOrDefault<String>("external_table", "");
137+
138+
}
139+
}
140+
else
141+
{
142+
throw Exception(ErrorCodes::BAD_ARGUMENTS,
143+
"Table function '{0}' has 1 argument, it is expected to be named collection", getName());
144+
}
124145
}
125-
else if (args.size() == 2)
146+
else
126147
{
127-
connection_string = args[0]->as<ASTLiteral &>().value.safeGet<String>();
128-
remote_table_name = args[1]->as<ASTLiteral &>().value.safeGet<String>();
148+
for (auto & arg : args)
149+
arg = evaluateConstantExpressionOrIdentifierAsLiteral(arg, context);
150+
151+
if (args.size() == 3)
152+
{
153+
connection_string = args[0]->as<ASTLiteral &>().value.safeGet<String>();
154+
schema_name = args[1]->as<ASTLiteral &>().value.safeGet<String>();
155+
remote_table_name = args[2]->as<ASTLiteral &>().value.safeGet<String>();
156+
}
157+
else if (args.size() == 2)
158+
{
159+
connection_string = args[0]->as<ASTLiteral &>().value.safeGet<String>();
160+
remote_table_name = args[1]->as<ASTLiteral &>().value.safeGet<String>();
161+
}
129162
}
130163
}
131164

tests/integration/test_odbc_interaction/configs/users.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
</networks>
1313
<profile>default</profile>
1414
<quota>default</quota>
15+
<named_collection_control>1</named_collection_control>
16+
<use_named_collections>1</use_named_collections>
1517
</default>
1618
</users>
1719

tests/integration/test_odbc_interaction/test.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,15 @@ def started_cluster():
156156
privileged=True,
157157
user="root",
158158
)
159+
node1.exec_in_container(
160+
[
161+
"sqlite3",
162+
sqlite_db,
163+
"CREATE TABLE t5(id INTEGER PRIMARY KEY ASC, X INTEGER, Y, Z);",
164+
],
165+
privileged=True,
166+
user="root",
167+
)
159168
node1.exec_in_container(
160169
[
161170
"sqlite3",
@@ -275,6 +284,37 @@ def test_mysql_simple_select_works(started_cluster):
275284
conn.close()
276285

277286

287+
def test_table_function_odbc_with_named_collection(started_cluster):
288+
skip_test_msan(node1)
289+
290+
mysql_setup = node1.odbc_drivers["MySQL"]
291+
292+
table_name = "test_mysql_with_named_collection"
293+
conn = get_mysql_conn()
294+
create_mysql_table(conn, table_name)
295+
296+
# Check that NULL-values are handled correctly by the ODBC-bridge
297+
with conn.cursor() as cursor:
298+
cursor.execute(
299+
"INSERT INTO clickhouse.{} VALUES(50, 'name1', 127, 255, 512), (100, 'name2', 127, 255, 511);".format(
300+
table_name
301+
)
302+
)
303+
conn.commit()
304+
305+
node1.query(f"""
306+
DROP NAMED COLLECTION IF EXISTS odbc_collection;
307+
CREATE NAMED COLLECTION odbc_collection AS
308+
connection_settings = 'DSN={mysql_setup["DSN"]}',
309+
external_table = '{table_name}';
310+
""")
311+
assert node1.query("SELECT name FROM odbc(odbc_collection)") == "name1\nname2\n"
312+
313+
node1.query(f"DROP TABLE IF EXISTS {table_name}")
314+
drop_mysql_table(conn, table_name)
315+
conn.close()
316+
317+
278318
def test_mysql_insert(started_cluster):
279319
skip_test_msan(node1)
280320

@@ -447,6 +487,37 @@ def test_sqlite_simple_select_storage_works(started_cluster):
447487
)
448488

449489

490+
def test_table_engine_odbc_named_collection(started_cluster):
491+
skip_test_msan(node1)
492+
493+
sqlite_setup = node1.odbc_drivers["SQLite3"]
494+
sqlite_db = sqlite_setup["Database"]
495+
496+
node1.exec_in_container(
497+
["sqlite3", sqlite_db, "INSERT INTO t5 values(1, 1, 2, 3);"],
498+
privileged=True,
499+
user="root",
500+
)
501+
502+
node1.query(f"""
503+
DROP NAMED COLLECTION IF EXISTS engine_odbc_collection;
504+
CREATE NAMED COLLECTION engine_odbc_collection AS
505+
connection_settings = 'DSN={sqlite_setup["DSN"]}',
506+
external_database = '',
507+
external_table = 't5';
508+
""")
509+
node1.query("CREATE TABLE SqliteODBCNamedCol (x Int32, y String, z String) ENGINE = ODBC(engine_odbc_collection)")
510+
511+
assert node1.query("SELECT * FROM SqliteODBCNamedCol") == "1\t2\t3\n"
512+
node1.query("DROP TABLE IF EXISTS SqliteODBCNamedCol")
513+
514+
node1.exec_in_container(
515+
["sqlite3", sqlite_db, "DELETE FROM t5;"],
516+
privileged=True,
517+
user="root",
518+
)
519+
520+
450521
def test_sqlite_odbc_hashed_dictionary(started_cluster):
451522
skip_test_msan(node1)
452523

0 commit comments

Comments
 (0)