Skip to content

Commit 210d0b7

Browse files
committed
Merge branch 'master' of github.com:ClickHouse/ClickHouse into issues/42990/simple_streaming
2 parents a2bd7f4 + 738567c commit 210d0b7

File tree

15 files changed

+267
-26
lines changed

15 files changed

+267
-26
lines changed

contrib/openssl-cmake/CMakeLists.txt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ elseif(ARCH_AARCH64)
146146
else()
147147
macro(perl_generate_asm FILE_IN FILE_OUT)
148148
add_custom_command(OUTPUT ${FILE_OUT}
149-
COMMAND /usr/bin/env perl ${FILE_IN} "linux64" ${FILE_OUT})
149+
COMMAND ${CMAKE_COMMAND} -E env "CC=${CMAKE_CXX_COMPILER}" /usr/bin/env perl ${FILE_IN} "linux64" ${FILE_OUT})
150150
endmacro()
151151

152152
perl_generate_asm(${OPENSSL_SOURCE_DIR}/crypto/aes/asm/aesv8-armx.pl ${OPENSSL_BINARY_DIR}/crypto/aes/aesv8-armx.S)
@@ -175,7 +175,7 @@ elseif(ARCH_AARCH64)
175175
elseif(ARCH_PPC64LE)
176176
macro(perl_generate_asm FILE_IN FILE_OUT)
177177
add_custom_command(OUTPUT ${FILE_OUT}
178-
COMMAND /usr/bin/env perl ${FILE_IN} "linux64v2" ${FILE_OUT})
178+
COMMAND ${CMAKE_COMMAND} -E env "CC=${CMAKE_CXX_COMPILER}" /usr/bin/env perl ${FILE_IN} "linux64v2" ${FILE_OUT})
179179
endmacro()
180180

181181
perl_generate_asm(${OPENSSL_SOURCE_DIR}/crypto/aes/asm/aesp8-ppc.pl ${OPENSSL_BINARY_DIR}/crypto/aes/aesp8-ppc.s)
@@ -185,15 +185,15 @@ elseif(ARCH_PPC64LE)
185185
elseif(ARCH_S390X)
186186
macro(perl_generate_asm FILE_IN FILE_OUT)
187187
add_custom_command(OUTPUT ${FILE_OUT}
188-
COMMAND /usr/bin/env perl ${FILE_IN} "linux64" ${FILE_OUT})
188+
COMMAND ${CMAKE_COMMAND} -E env "CC=${CMAKE_CXX_COMPILER}" /usr/bin/env perl ${FILE_IN} "linux64" ${FILE_OUT})
189189
endmacro()
190190

191191
perl_generate_asm(${OPENSSL_SOURCE_DIR}/crypto/aes/asm/aes-s390x.pl ${OPENSSL_BINARY_DIR}/crypto/aes/aes-s390x.S)
192192
perl_generate_asm(${OPENSSL_SOURCE_DIR}/crypto/s390xcpuid.pl ${OPENSSL_BINARY_DIR}/crypto/s390xcpuid.S)
193193
elseif(ARCH_RISCV64)
194194
macro(perl_generate_asm FILE_IN FILE_OUT)
195195
add_custom_command(OUTPUT ${FILE_OUT}
196-
COMMAND /usr/bin/env perl ${FILE_IN} "linux64" ${FILE_OUT})
196+
COMMAND ${CMAKE_COMMAND} -E env "CC=${CMAKE_CXX_COMPILER}" /usr/bin/env perl ${FILE_IN} "linux64" ${FILE_OUT})
197197
endmacro()
198198

199199
perl_generate_asm(${OPENSSL_SOURCE_DIR}/crypto/riscv64cpuid.pl ${OPENSSL_BINARY_DIR}/crypto/riscv64cpuid.S)

docs/en/sql-reference/functions/string-search-functions.md

Lines changed: 143 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1322,9 +1322,9 @@ Result:
13221322

13231323
## countSubstrings
13241324

1325-
Returns how often substring `needle` occurs in string `haystack`.
1325+
Returns how often a substring `needle` occurs in a string `haystack`.
13261326

1327-
Functions `countSubstringsCaseInsensitive` and `countSubstringsCaseInsensitiveUTF8` provide a case-insensitive and case-insensitive + UTF-8 variants of this function.
1327+
Functions [`countSubstringsCaseInsensitive`](#countsubstringscaseinsensitive) and [`countSubstringsCaseInsensitiveUTF8`](#countsubstringscaseinsensitiveutf8) provide case-insensitive and case-insensitive + UTF-8 variants of this function respectively.
13281328

13291329
**Syntax**
13301330

@@ -1371,6 +1371,113 @@ Result:
13711371
│ 1 │
13721372
└────────────────────────────────────────┘
13731373
```
1374+
## countSubstringsCaseInsensitive
1375+
1376+
Returns how often a substring `needle` occurs in a string `haystack`. Ignores case.
1377+
1378+
**Syntax**
1379+
1380+
``` sql
1381+
countSubstringsCaseInsensitive(haystack, needle[, start_pos])
1382+
```
1383+
1384+
**Arguments**
1385+
1386+
- `haystack` — String in which the search is performed. [String](../../sql-reference/syntax.md#syntax-string-literal).
1387+
- `needle` — Substring to be searched. [String](../../sql-reference/syntax.md#syntax-string-literal).
1388+
- `start_pos` – Position (1-based) in `haystack` at which the search starts. [UInt](../../sql-reference/data-types/int-uint.md). Optional.
1389+
1390+
**Returned values**
1391+
1392+
- The number of occurrences.
1393+
1394+
Type: [UInt64](../../sql-reference/data-types/int-uint.md).
1395+
1396+
**Examples**
1397+
1398+
Query:
1399+
1400+
``` sql
1401+
SELECT countSubstringsCaseInsensitive('AAAA', 'aa');
1402+
```
1403+
1404+
Result:
1405+
1406+
``` text
1407+
┌─countSubstringsCaseInsensitive('AAAA', 'aa')─┐
1408+
│ 2 │
1409+
└──────────────────────────────────────────────┘
1410+
```
1411+
1412+
Example with `start_pos` argument:
1413+
1414+
Query:
1415+
1416+
```sql
1417+
SELECT countSubstringsCaseInsensitive('abc___ABC___abc', 'abc', 4);
1418+
```
1419+
1420+
Result:
1421+
1422+
``` text
1423+
┌─countSubstringsCaseInsensitive('abc___ABC___abc', 'abc', 4)─┐
1424+
│ 2 │
1425+
└─────────────────────────────────────────────────────────────┘
1426+
```
1427+
1428+
## countSubstringsCaseInsensitiveUTF8
1429+
1430+
Returns how often a substring `needle` occurs in a string `haystack`. Ignores case and assumes that `haystack` is a UTF8 string.
1431+
1432+
**Syntax**
1433+
1434+
``` sql
1435+
countSubstringsCaseInsensitiveUTF8(haystack, needle[, start_pos])
1436+
```
1437+
1438+
**Arguments**
1439+
1440+
- `haystack` — UTF-8 string in which the search is performed. [String](../../sql-reference/syntax.md#syntax-string-literal).
1441+
- `needle` — Substring to be searched. [String](../../sql-reference/syntax.md#syntax-string-literal).
1442+
- `start_pos` – Position (1-based) in `haystack` at which the search starts. [UInt](../../sql-reference/data-types/int-uint.md). Optional.
1443+
1444+
**Returned values**
1445+
1446+
- The number of occurrences.
1447+
1448+
Type: [UInt64](../../sql-reference/data-types/int-uint.md).
1449+
1450+
**Examples**
1451+
1452+
Query:
1453+
1454+
``` sql
1455+
SELECT countSubstringsCaseInsensitiveUTF8('ложка, кошка, картошка', 'КА');
1456+
```
1457+
1458+
Result:
1459+
1460+
``` text
1461+
┌─countSubstringsCaseInsensitiveUTF8('ложка, кошка, картошка', 'КА')─┐
1462+
│ 4 │
1463+
└────────────────────────────────────────────────────────────────────┘
1464+
```
1465+
1466+
Example with `start_pos` argument:
1467+
1468+
Query:
1469+
1470+
```sql
1471+
SELECT countSubstringsCaseInsensitiveUTF8('ложка, кошка, картошка', 'КА', 13);
1472+
```
1473+
1474+
Result:
1475+
1476+
``` text
1477+
┌─countSubstringsCaseInsensitiveUTF8('ложка, кошка, картошка', 'КА', 13)─┐
1478+
│ 2 │
1479+
└────────────────────────────────────────────────────────────────────────┘
1480+
```
13741481

13751482
## countMatches
13761483

@@ -1421,7 +1528,40 @@ Result:
14211528

14221529
## countMatchesCaseInsensitive
14231530

1424-
Like `countMatches(haystack, pattern)` but matching ignores the case.
1531+
Returns the number of regular expression matches for a pattern in a haystack like [`countMatches`](#countmatches) but matching ignores the case.
1532+
1533+
**Syntax**
1534+
1535+
``` sql
1536+
countMatchesCaseInsensitive(haystack, pattern)
1537+
```
1538+
1539+
**Arguments**
1540+
1541+
- `haystack` — The string to search in. [String](../../sql-reference/syntax.md#syntax-string-literal).
1542+
- `pattern` — The regular expression with [re2 syntax](https://github.com/google/re2/wiki/Syntax). [String](../../sql-reference/data-types/string.md).
1543+
1544+
**Returned value**
1545+
1546+
- The number of matches.
1547+
1548+
Type: [UInt64](../../sql-reference/data-types/int-uint.md).
1549+
1550+
**Examples**
1551+
1552+
Query:
1553+
1554+
``` sql
1555+
SELECT countMatchesCaseInsensitive('AAAA', 'aa');
1556+
```
1557+
1558+
Result:
1559+
1560+
``` text
1561+
┌─countMatchesCaseInsensitive('AAAA', 'aa')────┐
1562+
│ 2 │
1563+
└──────────────────────────────────────────────┘
1564+
```
14251565

14261566
## regexpExtract
14271567

programs/server/config.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -742,9 +742,9 @@
742742
It also enables 'changeable_in_readonly' constraint type -->
743743
<settings_constraints_replace_previous>true</settings_constraints_replace_previous>
744744

745-
<!-- By default, for backward compatibility create table with a specific table engine ignores grant,
745+
<!-- By default, for backward compatibility creating table with a specific table engine ignores grant,
746746
however you can change this behaviour by setting this to true -->
747-
<table_engines_require_grant>true</table_engines_require_grant>
747+
<table_engines_require_grant>false</table_engines_require_grant>
748748

749749
<!-- Number of seconds since last access a role is stored in the Role Cache -->
750750
<role_cache_expiration_time_seconds>600</role_cache_expiration_time_seconds>

src/Common/CgroupsMemoryUsageObserver.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ void CgroupsMemoryUsageObserver::setMemoryUsageLimits(uint64_t hard_limit_, uint
7777
{
7878
if (up)
7979
{
80-
LOG_WARNING(log, "Exceeded sort memory limit ({})", ReadableSize(soft_limit_));
80+
LOG_WARNING(log, "Exceeded soft memory limit ({})", ReadableSize(soft_limit_));
8181

8282
#if USE_JEMALLOC
8383
LOG_INFO(log, "Purging jemalloc arenas");

src/Databases/DatabaseReplicated.cpp

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1708,9 +1708,18 @@ void registerDatabaseReplicated(DatabaseFactory & factory)
17081708
String shard_name = safeGetLiteralValue<String>(arguments[1], "Replicated");
17091709
String replica_name = safeGetLiteralValue<String>(arguments[2], "Replicated");
17101710

1711-
zookeeper_path = args.context->getMacros()->expand(zookeeper_path);
1712-
shard_name = args.context->getMacros()->expand(shard_name);
1713-
replica_name = args.context->getMacros()->expand(replica_name);
1711+
/// Expand macros.
1712+
Macros::MacroExpansionInfo info;
1713+
info.table_id.database_name = args.database_name;
1714+
info.table_id.uuid = args.uuid;
1715+
zookeeper_path = args.context->getMacros()->expand(zookeeper_path, info);
1716+
1717+
info.level = 0;
1718+
info.table_id.uuid = UUIDHelpers::Nil;
1719+
shard_name = args.context->getMacros()->expand(shard_name, info);
1720+
1721+
info.level = 0;
1722+
replica_name = args.context->getMacros()->expand(replica_name, info);
17141723

17151724
DatabaseReplicatedSettings database_replicated_settings{};
17161725
if (engine_define->settings)

src/Interpreters/InterpreterCreateQuery.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -790,6 +790,9 @@ InterpreterCreateQuery::TableProperties InterpreterCreateQuery::getTableProperti
790790
auto as_storage_metadata = as_storage->getInMemoryMetadataPtr();
791791
properties.columns = as_storage_metadata->getColumns();
792792

793+
if (!create.comment && !as_storage_metadata->comment.empty())
794+
create.set(create.comment, std::make_shared<ASTLiteral>(as_storage_metadata->comment));
795+
793796
/// Secondary indices and projections make sense only for MergeTree family of storage engines.
794797
/// We should not copy them for other storages.
795798
if (create.storage && endsWith(create.storage->engine->name, "MergeTree"))

src/Processors/QueryPlan/ReadFromMergeTree.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1792,6 +1792,11 @@ bool ReadFromMergeTree::requestOutputEachPartitionThroughSeparatePort()
17921792
if (isQueryWithFinal())
17931793
return false;
17941794

1795+
/// With parallel replicas we have to have only a single instance of `MergeTreeReadPoolParallelReplicas` per replica.
1796+
/// With aggregation-by-partitions optimisation we might create a separate pool for each partition.
1797+
if (is_parallel_reading_from_replicas)
1798+
return false;
1799+
17951800
const auto & settings = context->getSettingsRef();
17961801

17971802
const auto partitions_cnt = countPartitions(prepared_parts);

src/Storages/Kafka/StorageKafka.cpp

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -915,11 +915,22 @@ void StorageKafka::updateGlobalConfiguration(cppkafka::Configuration & kafka_con
915915
#endif // USE_KRB5
916916

917917
// No need to add any prefix, messages can be distinguished
918-
kafka_config.set_log_callback([this](cppkafka::KafkaHandleBase &, int level, const std::string & facility, const std::string & message)
919-
{
920-
auto [poco_level, client_logs_level] = parseSyslogLevel(level);
921-
LOG_IMPL(log, client_logs_level, poco_level, "[rdk:{}] {}", facility, message);
922-
});
918+
kafka_config.set_log_callback(
919+
[this](cppkafka::KafkaHandleBase & handle, int level, const std::string & facility, const std::string & message)
920+
{
921+
auto [poco_level, client_logs_level] = parseSyslogLevel(level);
922+
const auto & kafka_object_config = handle.get_configuration();
923+
const std::string client_id_key{"client.id"};
924+
chassert(kafka_object_config.has_property(client_id_key) && "Kafka configuration doesn't have expected client.id set");
925+
LOG_IMPL(
926+
log,
927+
client_logs_level,
928+
poco_level,
929+
"[client.id:{}] [rdk:{}] {}",
930+
kafka_object_config.get(client_id_key),
931+
facility,
932+
message);
933+
});
923934

924935
/// NOTE: statistics should be consumed, otherwise it creates too much
925936
/// entries in the queue, that leads to memory leak and slow shutdown.

tests/integration/test_backup_restore_on_cluster/test.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ def drop_after_test():
6969
node1.query("DROP TABLE IF EXISTS tbl ON CLUSTER 'cluster3' SYNC")
7070
node1.query("DROP TABLE IF EXISTS tbl2 ON CLUSTER 'cluster3' SYNC")
7171
node1.query("DROP DATABASE IF EXISTS mydb ON CLUSTER 'cluster3' SYNC")
72+
node1.query("DROP DATABASE IF EXISTS mydb2 ON CLUSTER 'cluster3' SYNC")
7273
node1.query("DROP USER IF EXISTS u1, u2 ON CLUSTER 'cluster3'")
7374

7475

@@ -524,6 +525,43 @@ def test_replicated_database_async():
524525
assert node2.query("SELECT * FROM mydb.tbl2 ORDER BY y") == TSV(["a", "bb"])
525526

526527

528+
@pytest.mark.parametrize("special_macro", ["uuid", "database"])
529+
def test_replicated_database_with_special_macro_in_zk_path(special_macro):
530+
zk_path = "/clickhouse/databases/{" + special_macro + "}"
531+
node1.query(
532+
"CREATE DATABASE mydb ON CLUSTER 'cluster' ENGINE=Replicated('"
533+
+ zk_path
534+
+ "','{shard}','{replica}')"
535+
)
536+
537+
# ReplicatedMergeTree without arguments means ReplicatedMergeTree('/clickhouse/tables/{uuid}/{shard}', '{replica}')
538+
node1.query("CREATE TABLE mydb.tbl(x Int64) ENGINE=ReplicatedMergeTree ORDER BY x")
539+
540+
node1.query("INSERT INTO mydb.tbl VALUES (-3)")
541+
node1.query("INSERT INTO mydb.tbl VALUES (1)")
542+
node1.query("INSERT INTO mydb.tbl VALUES (10)")
543+
544+
backup_name = new_backup_name()
545+
node1.query(f"BACKUP DATABASE mydb ON CLUSTER 'cluster' TO {backup_name}")
546+
547+
# RESTORE DATABASE with rename should work here because the new database will have another UUID and thus another zookeeper path.
548+
node1.query(
549+
f"RESTORE DATABASE mydb AS mydb2 ON CLUSTER 'cluster' FROM {backup_name}"
550+
)
551+
552+
node1.query("INSERT INTO mydb.tbl VALUES (2)")
553+
554+
node1.query("SYSTEM SYNC DATABASE REPLICA ON CLUSTER 'cluster' mydb2")
555+
node1.query("SYSTEM SYNC REPLICA ON CLUSTER 'cluster' mydb2.tbl")
556+
557+
assert node1.query("SELECT * FROM mydb.tbl ORDER BY x") == TSV(
558+
[[-3], [1], [2], [10]]
559+
)
560+
561+
assert node1.query("SELECT * FROM mydb2.tbl ORDER BY x") == TSV([[-3], [1], [10]])
562+
assert node2.query("SELECT * FROM mydb2.tbl ORDER BY x") == TSV([[-3], [1], [10]])
563+
564+
527565
# By default `backup_restore_keeper_value_max_size` is 1 MB, but in this test we'll set it to 50 bytes just to check it works.
528566
def test_keeper_value_max_size():
529567
node1.query(
Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
CREATE TABLE default.dist_monitor_batch_inserts\n(\n `dummy` UInt8\n)\nENGINE = Distributed(\'test_shard_localhost\', \'system\', \'one\')\nSETTINGS monitor_batch_inserts = 1
2-
CREATE TABLE default.dist_monitor_split_batch_on_failure\n(\n `dummy` UInt8\n)\nENGINE = Distributed(\'test_shard_localhost\', \'system\', \'one\')\nSETTINGS monitor_split_batch_on_failure = 1
3-
CREATE TABLE default.dist_monitor_sleep_time_ms\n(\n `dummy` UInt8\n)\nENGINE = Distributed(\'test_shard_localhost\', \'system\', \'one\')\nSETTINGS monitor_sleep_time_ms = 1
4-
CREATE TABLE default.dist_monitor_max_sleep_time_ms\n(\n `dummy` UInt8\n)\nENGINE = Distributed(\'test_shard_localhost\', \'system\', \'one\')\nSETTINGS monitor_max_sleep_time_ms = 1
5-
CREATE TABLE default.dist_background_insert_batch\n(\n `dummy` UInt8\n)\nENGINE = Distributed(\'test_shard_localhost\', \'system\', \'one\')\nSETTINGS background_insert_batch = 1
6-
CREATE TABLE default.dist_background_insert_split_batch_on_failure\n(\n `dummy` UInt8\n)\nENGINE = Distributed(\'test_shard_localhost\', \'system\', \'one\')\nSETTINGS background_insert_split_batch_on_failure = 1
7-
CREATE TABLE default.dist_background_insert_sleep_time_ms\n(\n `dummy` UInt8\n)\nENGINE = Distributed(\'test_shard_localhost\', \'system\', \'one\')\nSETTINGS background_insert_sleep_time_ms = 1
8-
CREATE TABLE default.dist_background_insert_max_sleep_time_ms\n(\n `dummy` UInt8\n)\nENGINE = Distributed(\'test_shard_localhost\', \'system\', \'one\')\nSETTINGS background_insert_max_sleep_time_ms = 1
1+
CREATE TABLE default.dist_monitor_batch_inserts\n(\n `dummy` UInt8\n)\nENGINE = Distributed(\'test_shard_localhost\', \'system\', \'one\')\nSETTINGS monitor_batch_inserts = 1\nCOMMENT \'This table contains a single row with a single dummy UInt8 column containing the value 0. Used when the table is not specified explicitly, for example in queries like `SELECT 1`.\'
2+
CREATE TABLE default.dist_monitor_split_batch_on_failure\n(\n `dummy` UInt8\n)\nENGINE = Distributed(\'test_shard_localhost\', \'system\', \'one\')\nSETTINGS monitor_split_batch_on_failure = 1\nCOMMENT \'This table contains a single row with a single dummy UInt8 column containing the value 0. Used when the table is not specified explicitly, for example in queries like `SELECT 1`.\'
3+
CREATE TABLE default.dist_monitor_sleep_time_ms\n(\n `dummy` UInt8\n)\nENGINE = Distributed(\'test_shard_localhost\', \'system\', \'one\')\nSETTINGS monitor_sleep_time_ms = 1\nCOMMENT \'This table contains a single row with a single dummy UInt8 column containing the value 0. Used when the table is not specified explicitly, for example in queries like `SELECT 1`.\'
4+
CREATE TABLE default.dist_monitor_max_sleep_time_ms\n(\n `dummy` UInt8\n)\nENGINE = Distributed(\'test_shard_localhost\', \'system\', \'one\')\nSETTINGS monitor_max_sleep_time_ms = 1\nCOMMENT \'This table contains a single row with a single dummy UInt8 column containing the value 0. Used when the table is not specified explicitly, for example in queries like `SELECT 1`.\'
5+
CREATE TABLE default.dist_background_insert_batch\n(\n `dummy` UInt8\n)\nENGINE = Distributed(\'test_shard_localhost\', \'system\', \'one\')\nSETTINGS background_insert_batch = 1\nCOMMENT \'This table contains a single row with a single dummy UInt8 column containing the value 0. Used when the table is not specified explicitly, for example in queries like `SELECT 1`.\'
6+
CREATE TABLE default.dist_background_insert_split_batch_on_failure\n(\n `dummy` UInt8\n)\nENGINE = Distributed(\'test_shard_localhost\', \'system\', \'one\')\nSETTINGS background_insert_split_batch_on_failure = 1\nCOMMENT \'This table contains a single row with a single dummy UInt8 column containing the value 0. Used when the table is not specified explicitly, for example in queries like `SELECT 1`.\'
7+
CREATE TABLE default.dist_background_insert_sleep_time_ms\n(\n `dummy` UInt8\n)\nENGINE = Distributed(\'test_shard_localhost\', \'system\', \'one\')\nSETTINGS background_insert_sleep_time_ms = 1\nCOMMENT \'This table contains a single row with a single dummy UInt8 column containing the value 0. Used when the table is not specified explicitly, for example in queries like `SELECT 1`.\'
8+
CREATE TABLE default.dist_background_insert_max_sleep_time_ms\n(\n `dummy` UInt8\n)\nENGINE = Distributed(\'test_shard_localhost\', \'system\', \'one\')\nSETTINGS background_insert_max_sleep_time_ms = 1\nCOMMENT \'This table contains a single row with a single dummy UInt8 column containing the value 0. Used when the table is not specified explicitly, for example in queries like `SELECT 1`.\'

0 commit comments

Comments
 (0)