summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorYDBot <[email protected]>2026-07-17 13:57:26 +0000
committerYDBot <[email protected]>2026-07-17 13:57:26 +0000
commit462b35b1d3bc7a668b784cf0f313bec2793738e2 (patch)
tree4e65b456392a49930301625b9320e74e01eeb0ee
parent08a0f6a3ebbc624aa892835e03eeaa5c027f2bca (diff)
parent880f1f4a67831e1f0732ea8d8d5a9ba43756d78e (diff)
Merge pull request #46914 from ydb-platform/merge-rightlib-260717-1016
-rw-r--r--build/mapping.conf.json8
-rw-r--r--build/platform/test_tool/host.ya.make.inc10
-rw-r--r--build/platform/test_tool/host_os.ya.make.inc10
-rw-r--r--build/platform/yfm/resources.json8
-rw-r--r--contrib/python/argcomplete/py3/.dist-info/METADATA4
-rw-r--r--contrib/python/argcomplete/py3/.dist-info/entry_points.txt1
-rw-r--r--contrib/python/argcomplete/py3/argcomplete/bash_completion.d/_python-argcomplete5
-rw-r--r--contrib/python/argcomplete/py3/argcomplete/completers.py11
-rw-r--r--contrib/python/argcomplete/py3/argcomplete/finders.py2
-rw-r--r--contrib/python/argcomplete/py3/argcomplete/packages/_argparse.py24
-rw-r--r--contrib/python/argcomplete/py3/argcomplete/scripts/python_argcomplete_check_easy_install_script.py84
-rw-r--r--contrib/python/argcomplete/py3/ya.make3
-rw-r--r--contrib/python/clickhouse-connect/.dist-info/METADATA2
-rw-r--r--contrib/python/clickhouse-connect/clickhouse_connect/_version.py2
-rw-r--r--contrib/python/clickhouse-connect/clickhouse_connect/cc_sqlalchemy/alembic/adapter.py22
-rw-r--r--contrib/python/clickhouse-connect/clickhouse_connect/cc_sqlalchemy/alembic/impl.py4
-rw-r--r--contrib/python/clickhouse-connect/clickhouse_connect/cc_sqlalchemy/alembic/utils.py4
-rw-r--r--contrib/python/clickhouse-connect/clickhouse_connect/cc_sqlalchemy/datatypes/base.py13
-rw-r--r--contrib/python/clickhouse-connect/clickhouse_connect/cc_sqlalchemy/datatypes/sqltypes.py201
-rw-r--r--contrib/python/clickhouse-connect/clickhouse_connect/cc_sqlalchemy/ddl/custom.py4
-rw-r--r--contrib/python/clickhouse-connect/clickhouse_connect/cc_sqlalchemy/ddl/tableengine.py80
-rw-r--r--contrib/python/clickhouse-connect/clickhouse_connect/cc_sqlalchemy/dialect.py2
-rw-r--r--contrib/python/clickhouse-connect/clickhouse_connect/cc_sqlalchemy/sql/__init__.py40
-rw-r--r--contrib/python/clickhouse-connect/clickhouse_connect/cc_sqlalchemy/sql/preparer.py2
-rw-r--r--contrib/python/clickhouse-connect/clickhouse_connect/datatypes/base.py37
-rw-r--r--contrib/python/clickhouse-connect/clickhouse_connect/datatypes/container.py6
-rw-r--r--contrib/python/clickhouse-connect/clickhouse_connect/datatypes/dynamic.py12
-rw-r--r--contrib/python/clickhouse-connect/clickhouse_connect/datatypes/format.py2
-rw-r--r--contrib/python/clickhouse-connect/clickhouse_connect/datatypes/network.py10
-rw-r--r--contrib/python/clickhouse-connect/clickhouse_connect/datatypes/numeric.py6
-rw-r--r--contrib/python/clickhouse-connect/clickhouse_connect/datatypes/registry.py4
-rw-r--r--contrib/python/clickhouse-connect/clickhouse_connect/datatypes/special.py6
-rw-r--r--contrib/python/clickhouse-connect/clickhouse_connect/datatypes/temporal.py22
-rw-r--r--contrib/python/clickhouse-connect/clickhouse_connect/datatypes/vector.py4
-rw-r--r--contrib/python/clickhouse-connect/clickhouse_connect/dbapi/__init__.py10
-rw-r--r--contrib/python/clickhouse-connect/clickhouse_connect/dbapi/connection.py24
-rw-r--r--contrib/python/clickhouse-connect/clickhouse_connect/dbapi/cursor.py63
-rw-r--r--contrib/python/clickhouse-connect/clickhouse_connect/driver/__init__.py30
-rw-r--r--contrib/python/clickhouse-connect/clickhouse_connect/driver/asyncclient.py86
-rw-r--r--contrib/python/clickhouse-connect/clickhouse_connect/driver/asyncqueue.py14
-rw-r--r--contrib/python/clickhouse-connect/clickhouse_connect/driver/binding.py18
-rw-r--r--contrib/python/clickhouse-connect/clickhouse_connect/driver/buffer.py8
-rw-r--r--contrib/python/clickhouse-connect/clickhouse_connect/driver/client.py117
-rw-r--r--contrib/python/clickhouse-connect/clickhouse_connect/driver/common.py10
-rw-r--r--contrib/python/clickhouse-connect/clickhouse_connect/driver/compression.py11
-rw-r--r--contrib/python/clickhouse-connect/clickhouse_connect/driver/context.py4
-rw-r--r--contrib/python/clickhouse-connect/clickhouse_connect/driver/dataconv.py11
-rw-r--r--contrib/python/clickhouse-connect/clickhouse_connect/driver/ddl.py16
-rw-r--r--contrib/python/clickhouse-connect/clickhouse_connect/driver/exceptions.py2
-rw-r--r--contrib/python/clickhouse-connect/clickhouse_connect/driver/httpclient.py99
-rw-r--r--contrib/python/clickhouse-connect/clickhouse_connect/driver/httputil.py16
-rw-r--r--contrib/python/clickhouse-connect/clickhouse_connect/driver/insert.py1
-rw-r--r--contrib/python/clickhouse-connect/clickhouse_connect/driver/npquery.py9
-rw-r--r--contrib/python/clickhouse-connect/clickhouse_connect/driver/parser.py10
-rw-r--r--contrib/python/clickhouse-connect/clickhouse_connect/driver/query.py50
-rw-r--r--contrib/python/clickhouse-connect/clickhouse_connect/driver/streaming.py8
-rw-r--r--contrib/python/clickhouse-connect/clickhouse_connect/driver/summary.py6
-rw-r--r--contrib/python/clickhouse-connect/clickhouse_connect/driver/transform.py11
-rw-r--r--contrib/python/clickhouse-connect/clickhouse_connect/driver/types.py4
-rw-r--r--contrib/python/clickhouse-connect/clickhouse_connect/driver/tzutil.py6
-rw-r--r--contrib/python/clickhouse-connect/clickhouse_connect/driverc/buffer.pyx18
-rw-r--r--contrib/python/clickhouse-connect/clickhouse_connect/driverc/dataconv.pyx1
-rw-r--r--contrib/python/clickhouse-connect/clickhouse_connect/driverc/npconv.pyx1
-rw-r--r--contrib/python/clickhouse-connect/clickhouse_connect/py.typed0
-rw-r--r--contrib/python/clickhouse-connect/ya.make3
-rw-r--r--contrib/python/textual/.dist-info/METADATA2
-rw-r--r--contrib/python/textual/textual/_compositor.py7
-rw-r--r--contrib/python/textual/textual/_xterm_parser.py86
-rw-r--r--contrib/python/textual/textual/pilot.py2
-rw-r--r--contrib/python/textual/textual/screen.py15
-rw-r--r--contrib/python/textual/textual/widgets/_input.py12
-rw-r--r--contrib/python/textual/textual/widgets/_text_area.py6
-rw-r--r--contrib/python/textual/ya.make2
-rw-r--r--library/cpp/yt/error/error-inl.h20
-rw-r--r--library/cpp/yt/error/error.h13
-rw-r--r--library/cpp/yt/logging/backends/arcadia/backend.cpp2
-rw-r--r--library/cpp/yt/logging/benchmark/logging.cpp167
-rw-r--r--library/cpp/yt/logging/benchmark/ya.make1
-rw-r--r--library/cpp/yt/logging/logger-inl.h399
-rw-r--r--library/cpp/yt/logging/logger.cpp94
-rw-r--r--library/cpp/yt/logging/logger.h333
-rw-r--r--library/cpp/yt/logging/plain_text_formatter/formatter.cpp43
-rw-r--r--library/cpp/yt/logging/private.h15
-rw-r--r--library/cpp/yt/logging/public.h19
-rw-r--r--library/cpp/yt/logging/structured_payload.cpp19
-rw-r--r--library/cpp/yt/logging/structured_payload.h25
-rw-r--r--library/cpp/yt/logging/tagged_payload-inl.h108
-rw-r--r--library/cpp/yt/logging/tagged_payload.cpp214
-rw-r--r--library/cpp/yt/logging/tagged_payload.h201
-rw-r--r--library/cpp/yt/logging/unittests/helpers.cpp27
-rw-r--r--library/cpp/yt/logging/unittests/helpers.h22
-rw-r--r--library/cpp/yt/logging/unittests/logger_ut.cpp331
-rw-r--r--library/cpp/yt/logging/unittests/structured_payload_ut.cpp36
-rw-r--r--library/cpp/yt/logging/unittests/tagged_payload_ut.cpp146
-rw-r--r--library/cpp/yt/logging/unittests/ya.make3
-rw-r--r--library/cpp/yt/logging/ya.make2
-rwxr-xr-xya20
-rw-r--r--yql/essentials/ast/yql_constraint.cpp2
-rw-r--r--yql/essentials/core/ut/yql_expr_constraint_ut.cpp73
-rw-r--r--yql/essentials/core/yql_expr_constraint.cpp44
-rw-r--r--yql/essentials/public/purecalc/io_specs/protobuf/no_llvm/ya.make23
-rw-r--r--yql/essentials/public/purecalc/io_specs/protobuf/ya.make4
-rw-r--r--yql/essentials/public/purecalc/io_specs/protobuf_raw/no_llvm/ya.make23
-rw-r--r--yql/essentials/public/purecalc/io_specs/protobuf_raw/ya.make4
-rw-r--r--yql/essentials/sql/v1/SQLv1Antlr4.g.in11
-rw-r--r--yql/essentials/sql/v1/format/sql_format_ut.h26
-rw-r--r--yql/essentials/sql/v1/node.h8
-rw-r--r--yql/essentials/sql/v1/query.cpp17
-rw-r--r--yql/essentials/sql/v1/sql_query.cpp16
-rw-r--r--yql/essentials/sql/v1/sql_translation.cpp71
-rw-r--r--yql/essentials/sql/v1/sql_ut_common.h411
-rw-r--r--yt/cpp/mapreduce/interface/logging/yt_log.cpp9
-rw-r--r--yt/yql/providers/yt/codec/yt_codec_tz.h19
-rw-r--r--yt/yql/providers/yt/provider/yql_yt_ytflow_integration.cpp9
-rw-r--r--yt/yql/providers/ytflow/integration/proto/yt.proto1
-rw-r--r--yt/yql/tests/sql/suites/ytflow/multiple_sorted_outputs_with_different_keys.cfg4
-rw-r--r--yt/yql/tests/sql/suites/ytflow/multiple_sorted_outputs_with_different_keys.yql27
-rw-r--r--yt/yql/tests/sql/suites/ytflow/output_composite_key_value_table.txt0
-rw-r--r--yt/yql/tests/sql/suites/ytflow/output_composite_key_value_table.txt.attr23
-rw-r--r--yt/yql/tests/sql/suites/ytflow/output_sorted_by_key.txt0
-rw-r--r--yt/yql/tests/sql/suites/ytflow/output_sorted_by_key.txt.attr18
-rw-r--r--yt/yql/tests/sql/suites/ytflow/output_sorted_by_value.txt0
-rw-r--r--yt/yql/tests/sql/suites/ytflow/output_sorted_by_value.txt.attr18
-rw-r--r--yt/yql/tests/sql/suites/ytflow/reuse_output_to_sorted_tables_with_different_keys.cfg4
-rw-r--r--yt/yql/tests/sql/suites/ytflow/reuse_output_to_sorted_tables_with_different_keys.yql16
-rw-r--r--yt/yql/tests/sql/suites/ytflow/reuse_output_to_sorted_tables_with_same_key.cfg4
-rw-r--r--yt/yql/tests/sql/suites/ytflow/reuse_output_to_sorted_tables_with_same_key.yql16
-rw-r--r--yt/yql/tests/sql/suites/ytflow/sorted_and_unsorted_outputs.cfg4
-rw-r--r--yt/yql/tests/sql/suites/ytflow/sorted_and_unsorted_outputs.yql28
-rw-r--r--yt/yql/tests/sql/suites/ytflow/write_to_sorted_table_with_composite_key.cfg3
-rw-r--r--yt/yql/tests/sql/suites/ytflow/write_to_sorted_table_with_composite_key.yql15
-rw-r--r--yt/yql/tools/mrjob/mrjob.cpp6
-rw-r--r--yt/yql/tools/mrjob/ya.make7
-rw-r--r--yt/yt/RpcProxyProtocolVersion.txt2
-rw-r--r--yt/yt/client/api/rpc_proxy/client_impl.h1
-rw-r--r--yt/yt/client/object_client/helpers.cpp1
-rw-r--r--yt/yt/client/object_client/public.h7
-rw-r--r--yt/yt/client/sequoia_client/public.h1
-rw-r--r--yt/yt/core/logging/config.cpp2
-rw-r--r--yt/yt/core/logging/config.h10
-rw-r--r--yt/yt/core/logging/formatter.cpp90
-rw-r--r--yt/yt/core/logging/formatter.h47
-rw-r--r--yt/yt/core/logging/log-inl.h12
-rw-r--r--yt/yt/core/logging/log_manager.cpp32
-rw-r--r--yt/yt/core/logging/system_log_event_provider.cpp20
-rw-r--r--yt/yt/core/logging/unittests/logging_ut.cpp234
-rw-r--r--yt/yt/core/ytree/serialize-inl.h5
-rw-r--r--yt/yt_proto/yt/client/node_tracker_client/proto/node.proto1
148 files changed, 3977 insertions, 1166 deletions
diff --git a/build/mapping.conf.json b/build/mapping.conf.json
index 6483820b6db..cef8ab62008 100644
--- a/build/mapping.conf.json
+++ b/build/mapping.conf.json
@@ -622,6 +622,7 @@
"12727426752": "{registry_endpoint}/12727426752",
"12765030473": "{registry_endpoint}/12765030473",
"12778660629": "{registry_endpoint}/12778660629",
+ "12807488115": "{registry_endpoint}/12807488115",
"5486713852": "{registry_endpoint}/5486713852",
"5514352253": "{registry_endpoint}/5514352253",
"5523579199": "{registry_endpoint}/5523579199",
@@ -2109,6 +2110,7 @@
"12606129409": "{registry_endpoint}/12606129409",
"12636194389": "{registry_endpoint}/12636194389",
"12666249275": "{registry_endpoint}/12666249275",
+ "12811590910": "{registry_endpoint}/12811590910",
"6662972834": "{registry_endpoint}/6662972834",
"11823170312": "{registry_endpoint}/11823170312",
"11879814525": "{registry_endpoint}/11879814525",
@@ -2129,6 +2131,7 @@
"12606127844": "{registry_endpoint}/12606127844",
"12636192320": "{registry_endpoint}/12636192320",
"12666248010": "{registry_endpoint}/12666248010",
+ "12811589861": "{registry_endpoint}/12811589861",
"6662965433": "{registry_endpoint}/6662965433",
"11823173798": "{registry_endpoint}/11823173798",
"11879819941": "{registry_endpoint}/11879819941",
@@ -2149,6 +2152,7 @@
"12606130988": "{registry_endpoint}/12606130988",
"12636196166": "{registry_endpoint}/12636196166",
"12666251081": "{registry_endpoint}/12666251081",
+ "12811592057": "{registry_endpoint}/12811592057",
"6662980150": "{registry_endpoint}/6662980150",
"5476908047": "{registry_endpoint}/5476908047",
"5509380757": "{registry_endpoint}/5509380757",
@@ -3040,6 +3044,7 @@
"12727426752": "devtools/ya/test/programs/test_tool/bin/test_tool for linux",
"12765030473": "devtools/ya/test/programs/test_tool/bin/test_tool for linux",
"12778660629": "devtools/ya/test/programs/test_tool/bin/test_tool for linux",
+ "12807488115": "devtools/ya/test/programs/test_tool/bin/test_tool for linux",
"5486713852": "devtools/ya/test/programs/test_tool/bin/test_tool for linux",
"5514352253": "devtools/ya/test/programs/test_tool/bin/test_tool for linux",
"5523579199": "devtools/ya/test/programs/test_tool/bin/test_tool for linux",
@@ -4527,6 +4532,7 @@
"12606129409": "yfm-docs for darwin",
"12636194389": "yfm-docs for darwin",
"12666249275": "yfm-docs for darwin",
+ "12811590910": "yfm-docs for darwin",
"6662972834": "yfm-docs for darwin",
"11823170312": "yfm-docs for linux",
"11879814525": "yfm-docs for linux",
@@ -4547,6 +4553,7 @@
"12606127844": "yfm-docs for linux",
"12636192320": "yfm-docs for linux",
"12666248010": "yfm-docs for linux",
+ "12811589861": "yfm-docs for linux",
"6662965433": "yfm-docs for linux",
"11823173798": "yfm-docs for win32",
"11879819941": "yfm-docs for win32",
@@ -4567,6 +4574,7 @@
"12606130988": "yfm-docs for win32",
"12636196166": "yfm-docs for win32",
"12666251081": "yfm-docs for win32",
+ "12811592057": "yfm-docs for win32",
"6662980150": "yfm-docs for win32",
"5476908047": "ymake for darwin",
"5509380757": "ymake for darwin",
diff --git a/build/platform/test_tool/host.ya.make.inc b/build/platform/test_tool/host.ya.make.inc
index e23ae9ddf1a..45904018c0f 100644
--- a/build/platform/test_tool/host.ya.make.inc
+++ b/build/platform/test_tool/host.ya.make.inc
@@ -1,12 +1,12 @@
IF (HOST_OS_DARWIN AND HOST_ARCH_X86_64)
- DECLARE_EXTERNAL_RESOURCE(TEST_TOOL_HOST sbr:12778677593)
+ DECLARE_EXTERNAL_RESOURCE(TEST_TOOL_HOST sbr:12807476180)
ELSEIF (HOST_OS_DARWIN AND HOST_ARCH_ARM64)
- DECLARE_EXTERNAL_RESOURCE(TEST_TOOL_HOST sbr:12778675385)
+ DECLARE_EXTERNAL_RESOURCE(TEST_TOOL_HOST sbr:12807473502)
ELSEIF (HOST_OS_LINUX AND HOST_ARCH_X86_64)
- DECLARE_EXTERNAL_RESOURCE(TEST_TOOL_HOST sbr:12778682489)
+ DECLARE_EXTERNAL_RESOURCE(TEST_TOOL_HOST sbr:12807481043)
ELSEIF (HOST_OS_LINUX AND HOST_ARCH_AARCH64)
- DECLARE_EXTERNAL_RESOURCE(TEST_TOOL_HOST sbr:12778673354)
+ DECLARE_EXTERNAL_RESOURCE(TEST_TOOL_HOST sbr:12807470623)
ELSEIF (HOST_OS_WINDOWS AND HOST_ARCH_X86_64)
- DECLARE_EXTERNAL_RESOURCE(TEST_TOOL_HOST sbr:12778679810)
+ DECLARE_EXTERNAL_RESOURCE(TEST_TOOL_HOST sbr:12807478544)
ENDIF()
diff --git a/build/platform/test_tool/host_os.ya.make.inc b/build/platform/test_tool/host_os.ya.make.inc
index d65679840ec..f6061502b1c 100644
--- a/build/platform/test_tool/host_os.ya.make.inc
+++ b/build/platform/test_tool/host_os.ya.make.inc
@@ -1,12 +1,12 @@
IF (HOST_OS_DARWIN AND HOST_ARCH_X86_64)
- DECLARE_EXTERNAL_RESOURCE(TEST_TOOL_HOST sbr:12778655677)
+ DECLARE_EXTERNAL_RESOURCE(TEST_TOOL_HOST sbr:12807484254)
ELSEIF (HOST_OS_DARWIN AND HOST_ARCH_ARM64)
- DECLARE_EXTERNAL_RESOURCE(TEST_TOOL_HOST sbr:12778653112)
+ DECLARE_EXTERNAL_RESOURCE(TEST_TOOL_HOST sbr:12807482262)
ELSEIF (HOST_OS_LINUX AND HOST_ARCH_X86_64)
- DECLARE_EXTERNAL_RESOURCE(TEST_TOOL_HOST sbr:12778660629)
+ DECLARE_EXTERNAL_RESOURCE(TEST_TOOL_HOST sbr:12807488115)
ELSEIF (HOST_OS_LINUX AND HOST_ARCH_AARCH64)
- DECLARE_EXTERNAL_RESOURCE(TEST_TOOL_HOST sbr:12778650528)
+ DECLARE_EXTERNAL_RESOURCE(TEST_TOOL_HOST sbr:12807480382)
ELSEIF (HOST_OS_WINDOWS AND HOST_ARCH_X86_64)
- DECLARE_EXTERNAL_RESOURCE(TEST_TOOL_HOST sbr:12778657924)
+ DECLARE_EXTERNAL_RESOURCE(TEST_TOOL_HOST sbr:12807486192)
ENDIF()
diff --git a/build/platform/yfm/resources.json b/build/platform/yfm/resources.json
index 08b886b25aa..0b90b6fab0a 100644
--- a/build/platform/yfm/resources.json
+++ b/build/platform/yfm/resources.json
@@ -1,16 +1,16 @@
{
"by_platform": {
"win32-x86_64": {
- "uri": "sbr:12666251081"
+ "uri": "sbr:12811592057"
},
"darwin-x86_64": {
- "uri": "sbr:12666249275"
+ "uri": "sbr:12811590910"
},
"linux-x86_64": {
- "uri": "sbr:12666248010"
+ "uri": "sbr:12811589861"
},
"darwin-arm64": {
- "uri": "sbr:12666249275"
+ "uri": "sbr:12811590910"
}
}
}
diff --git a/contrib/python/argcomplete/py3/.dist-info/METADATA b/contrib/python/argcomplete/py3/.dist-info/METADATA
index 33d51a548ba..fa5e1d45d3a 100644
--- a/contrib/python/argcomplete/py3/.dist-info/METADATA
+++ b/contrib/python/argcomplete/py3/.dist-info/METADATA
@@ -1,6 +1,6 @@
Metadata-Version: 2.4
Name: argcomplete
-Version: 3.6.3
+Version: 3.7.0
Summary: Bash tab completion for argparse
Project-URL: Homepage, https://github.com/kislyuk/argcomplete
Project-URL: Documentation, https://kislyuk.github.io/argcomplete
@@ -22,12 +22,12 @@ Classifier: Operating System :: MacOS :: MacOS X
Classifier: Operating System :: POSIX
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
-Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
+Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy
Classifier: Topic :: Software Development
diff --git a/contrib/python/argcomplete/py3/.dist-info/entry_points.txt b/contrib/python/argcomplete/py3/.dist-info/entry_points.txt
index f13e435fafb..874a9e1481d 100644
--- a/contrib/python/argcomplete/py3/.dist-info/entry_points.txt
+++ b/contrib/python/argcomplete/py3/.dist-info/entry_points.txt
@@ -1,4 +1,3 @@
[console_scripts]
activate-global-python-argcomplete = argcomplete.scripts.activate_global_python_argcomplete:main
-python-argcomplete-check-easy-install-script = argcomplete.scripts.python_argcomplete_check_easy_install_script:main
register-python-argcomplete = argcomplete.scripts.register_python_argcomplete:main
diff --git a/contrib/python/argcomplete/py3/argcomplete/bash_completion.d/_python-argcomplete b/contrib/python/argcomplete/py3/argcomplete/bash_completion.d/_python-argcomplete
index 1155f1ce5e8..6d395c66ed9 100644
--- a/contrib/python/argcomplete/py3/argcomplete/bash_completion.d/_python-argcomplete
+++ b/contrib/python/argcomplete/py3/argcomplete/bash_completion.d/_python-argcomplete
@@ -190,10 +190,7 @@ _python_argcomplete_global() {
interpreter=($interpreter)
fi
- if (__python_argcomplete_scan_head_noerr "$SCRIPT_NAME" easy_install \
- && "${interpreter[@]}" "$(__python_argcomplete_which python-argcomplete-check-easy-install-script)" "$SCRIPT_NAME") >/dev/null 2>&1; then
- ARGCOMPLETE=1
- elif ([[ "${interpreter[@]}" == *python* ]] || [[ "${interpreter[@]}" == *pypy* ]])\
+ if ([[ "${interpreter[@]}" == *python* ]] || [[ "${interpreter[@]}" == *pypy* ]])\
&& __python_argcomplete_run "${interpreter[@]}" -m argcomplete._check_console_script "$SCRIPT_NAME"; then
ARGCOMPLETE=1
fi
diff --git a/contrib/python/argcomplete/py3/argcomplete/completers.py b/contrib/python/argcomplete/py3/argcomplete/completers.py
index 4c01e695189..48715811898 100644
--- a/contrib/python/argcomplete/py3/argcomplete/completers.py
+++ b/contrib/python/argcomplete/py3/argcomplete/completers.py
@@ -4,6 +4,7 @@
import argparse
import os
import subprocess
+from shlex import quote
def _call(*args, **kwargs):
@@ -64,20 +65,22 @@ class FilesCompleter(BaseCompleter):
# correctly in older versions and calling bind makes them available. For details, see
# https://savannah.gnu.org/support/index.php?111125
files = _call(
- ["bash", "-c", "bind; compgen -A directory -- '{p}'".format(p=prefix)], stderr=subprocess.DEVNULL
+ ["bash", "-c", "bind; compgen -A directory -- {p}".format(p=quote(prefix))],
+ stderr=subprocess.DEVNULL,
)
completion += [f + "/" for f in files]
for x in self.allowednames:
completion += _call(
- ["bash", "-c", "bind; compgen -A file -X '!*.{0}' -- '{p}'".format(x, p=prefix)],
+ ["bash", "-c", "bind; compgen -A file -X '!*.{0}' -- {p}".format(x, p=quote(prefix))],
stderr=subprocess.DEVNULL,
)
else:
completion += _call(
- ["bash", "-c", "bind; compgen -A file -- '{p}'".format(p=prefix)], stderr=subprocess.DEVNULL
+ ["bash", "-c", "bind; compgen -A file -- {p}".format(p=quote(prefix))], stderr=subprocess.DEVNULL
)
anticomp = _call(
- ["bash", "-c", "bind; compgen -A directory -- '{p}'".format(p=prefix)], stderr=subprocess.DEVNULL
+ ["bash", "-c", "bind; compgen -A directory -- {p}".format(p=quote(prefix))],
+ stderr=subprocess.DEVNULL,
)
completion = list(set(completion) - set(anticomp))
diff --git a/contrib/python/argcomplete/py3/argcomplete/finders.py b/contrib/python/argcomplete/py3/argcomplete/finders.py
index ab8bb1d90a2..2159b7dea2f 100644
--- a/contrib/python/argcomplete/py3/argcomplete/finders.py
+++ b/contrib/python/argcomplete/py3/argcomplete/finders.py
@@ -527,7 +527,7 @@ class CompletionFinder(object):
# (extended to characters other than the colon).
if last_wordbreak_pos is not None:
completions = [c[last_wordbreak_pos + 1 :] for c in completions]
- special_chars += "();<>|&!`$* \t\n\"'"
+ special_chars += "();<>|&!`$*?[]{} \t\n\"'"
elif cword_prequote == '"':
special_chars += '"`$!'
diff --git a/contrib/python/argcomplete/py3/argcomplete/packages/_argparse.py b/contrib/python/argcomplete/py3/argcomplete/packages/_argparse.py
index f6ecb1f4172..574d9d8207a 100644
--- a/contrib/python/argcomplete/py3/argcomplete/packages/_argparse.py
+++ b/contrib/python/argcomplete/py3/argcomplete/packages/_argparse.py
@@ -19,7 +19,13 @@ from argparse import (
_SubParsersAction,
)
from gettext import gettext
-from typing import Dict, List, Set, Tuple
+from typing import Dict, List, Optional, Set, Tuple, Union, cast
+
+_OptionTuple = Union[
+ Tuple[Optional[Action], str, Optional[str]],
+ Tuple[Optional[Action], str, Optional[str], Optional[str]],
+]
+_OptionTupleEntry = Union[_OptionTuple, List[_OptionTuple]]
_num_consumed_args: Dict[Action, int] = {}
@@ -97,7 +103,7 @@ class IntrospectiveArgumentParser(ArgumentParser):
# find all option indices, and determine the arg_string_pattern
# which has an 'O' if there is an option at an index,
# an 'A' if there is an argument, or a '-' if there is a '--'
- option_string_indices = {}
+ option_string_indices: Dict[int, _OptionTupleEntry] = {}
arg_string_pattern_parts = []
arg_strings_iter = iter(arg_strings)
for i, arg_string in enumerate(arg_strings_iter):
@@ -114,7 +120,7 @@ class IntrospectiveArgumentParser(ArgumentParser):
if option_tuple is None:
pattern = 'A'
else:
- option_string_indices[i] = option_tuple
+ option_string_indices[i] = cast(_OptionTupleEntry, option_tuple)
pattern = 'O'
arg_string_pattern_parts.append(pattern)
@@ -161,9 +167,11 @@ class IntrospectiveArgumentParser(ArgumentParser):
# function to convert arg_strings into an optional action
def consume_optional(start_index):
# get the optional identified at this index
- option_tuple = option_string_indices[start_index]
- if isinstance(option_tuple, list): # Python 3.12.7+
- option_tuple = option_tuple[0]
+ raw_option_tuple = option_string_indices[start_index]
+ if isinstance(raw_option_tuple, list): # Python 3.12.7+
+ option_tuple = raw_option_tuple[0]
+ else:
+ option_tuple = raw_option_tuple
if len(option_tuple) == 3:
action, option_string, explicit_arg = option_tuple
else: # Python 3.11.9+, 3.12.3+, 3.13+
@@ -241,8 +249,8 @@ class IntrospectiveArgumentParser(ArgumentParser):
# add the Optional to the list and return the index at which
# the Optional's string args stopped
assert action_tuples
- for action, args, option_string in action_tuples:
- take_action(action, args, option_string)
+ for optional_action, args, option_string in action_tuples:
+ take_action(optional_action, args, option_string)
return stop
# the list of Positionals left to be parsed; this is modified
diff --git a/contrib/python/argcomplete/py3/argcomplete/scripts/python_argcomplete_check_easy_install_script.py b/contrib/python/argcomplete/py3/argcomplete/scripts/python_argcomplete_check_easy_install_script.py
deleted file mode 100644
index 0eb744c9e96..00000000000
--- a/contrib/python/argcomplete/py3/argcomplete/scripts/python_argcomplete_check_easy_install_script.py
+++ /dev/null
@@ -1,84 +0,0 @@
-#!/usr/bin/env python3
-
-# Copyright 2012-2023, Andrey Kislyuk and argcomplete contributors.
-# Licensed under the Apache License. See https://github.com/kislyuk/argcomplete for more info.
-
-"""
-This script is part of the Python argcomplete package (https://github.com/kislyuk/argcomplete).
-It is used to check if an EASY-INSTALL-SCRIPT wrapper redirects to a script that contains the string
-"PYTHON_ARGCOMPLETE_OK". If you have enabled global completion in argcomplete, the completion hook will run it every
-time you press <TAB> in your shell.
-
-Usage:
- python-argcomplete-check-easy-install-script <input executable file>
-"""
-
-import sys
-
-# PEP 366
-__package__ = "argcomplete.scripts"
-
-
-def main():
- if len(sys.argv) != 2:
- sys.exit(__doc__)
-
- sys.tracebacklimit = 0
-
- with open(sys.argv[1]) as fh:
- line1, head = fh.read(1024).split("\n", 1)[:2]
- if line1.startswith("#") and ("py" in line1 or "Py" in line1):
- import re
-
- lines = head.split("\n", 12)
- for line in lines:
- if line.startswith("# EASY-INSTALL-SCRIPT"):
- import pkg_resources # type: ignore
-
- re_match = re.match("# EASY-INSTALL-SCRIPT: '(.+)','(.+)'", line)
- assert re_match is not None
- dist, script = re_match.groups()
- if "PYTHON_ARGCOMPLETE_OK" in pkg_resources.get_distribution(dist).get_metadata(
- "scripts/" + script
- ):
- return 0
- elif line.startswith("# EASY-INSTALL-ENTRY-SCRIPT"):
- re_match = re.match("# EASY-INSTALL-ENTRY-SCRIPT: '(.+)','(.+)','(.+)'", line)
- assert re_match is not None
- dist, group, name = re_match.groups()
- import pkgutil
-
- import pkg_resources # type: ignore
-
- entry_point_info = pkg_resources.get_distribution(dist).get_entry_info(group, name)
- assert entry_point_info is not None
- module_name = entry_point_info.module_name
- with open(pkgutil.get_loader(module_name).get_filename()) as mod_fh: # type: ignore
- if "PYTHON_ARGCOMPLETE_OK" in mod_fh.read(1024):
- return 0
- elif line.startswith("# EASY-INSTALL-DEV-SCRIPT"):
- for line2 in lines:
- if line2.startswith("__file__"):
- re_match = re.match("__file__ = '(.+)'", line2)
- assert re_match is not None
- filename = re_match.group(1)
- with open(filename) as mod_fh:
- if "PYTHON_ARGCOMPLETE_OK" in mod_fh.read(1024):
- return 0
- elif line.startswith("# PBR Generated"):
- re_match = re.search("from (.*) import", head)
- assert re_match is not None
- module = re_match.groups()[0]
- import pkgutil
-
- import pkg_resources # type: ignore
-
- with open(pkgutil.get_loader(module).get_filename()) as mod_fh: # type: ignore
- if "PYTHON_ARGCOMPLETE_OK" in mod_fh.read(1024):
- return 0
-
- return 1
-
-
-if __name__ == "__main__":
- sys.exit(main())
diff --git a/contrib/python/argcomplete/py3/ya.make b/contrib/python/argcomplete/py3/ya.make
index 1ffd0082bb2..397e1ff3f2e 100644
--- a/contrib/python/argcomplete/py3/ya.make
+++ b/contrib/python/argcomplete/py3/ya.make
@@ -2,7 +2,7 @@
PY3_LIBRARY()
-VERSION(3.6.3)
+VERSION(3.7.0)
LICENSE(Apache-2.0)
@@ -23,7 +23,6 @@ PY_SRCS(
argcomplete/packages/_shlex.py
argcomplete/scripts/__init__.py
argcomplete/scripts/activate_global_python_argcomplete.py
- argcomplete/scripts/python_argcomplete_check_easy_install_script.py
argcomplete/scripts/register_python_argcomplete.py
argcomplete/shell_integration.py
)
diff --git a/contrib/python/clickhouse-connect/.dist-info/METADATA b/contrib/python/clickhouse-connect/.dist-info/METADATA
index cf511761a8d..656e95416df 100644
--- a/contrib/python/clickhouse-connect/.dist-info/METADATA
+++ b/contrib/python/clickhouse-connect/.dist-info/METADATA
@@ -1,6 +1,6 @@
Metadata-Version: 2.4
Name: clickhouse-connect
-Version: 1.3.0
+Version: 1.4.1
Summary: ClickHouse Database Core Driver for Python, Pandas, and Superset
Home-page: https://github.com/ClickHouse/clickhouse-connect
Author: ClickHouse Inc.
diff --git a/contrib/python/clickhouse-connect/clickhouse_connect/_version.py b/contrib/python/clickhouse-connect/clickhouse_connect/_version.py
index d28b3ddc3b1..6abaa204304 100644
--- a/contrib/python/clickhouse-connect/clickhouse_connect/_version.py
+++ b/contrib/python/clickhouse-connect/clickhouse_connect/_version.py
@@ -1 +1 @@
-version = "1.3.0"
+version = "1.4.1"
diff --git a/contrib/python/clickhouse-connect/clickhouse_connect/cc_sqlalchemy/alembic/adapter.py b/contrib/python/clickhouse-connect/clickhouse_connect/cc_sqlalchemy/alembic/adapter.py
index d444b69e73a..0319a329855 100644
--- a/contrib/python/clickhouse-connect/clickhouse_connect/cc_sqlalchemy/alembic/adapter.py
+++ b/contrib/python/clickhouse-connect/clickhouse_connect/cc_sqlalchemy/alembic/adapter.py
@@ -5,6 +5,7 @@ from alembic.operations import Operations, ops
from alembic.runtime.migration import MigrationContext
from alembic.util import DispatchPriority, PriorityDispatchResult
+from clickhouse_connect.cc_sqlalchemy.alembic.impl import ClickHouseImpl
from clickhouse_connect.cc_sqlalchemy.datatypes.base import ChSqlaType
from clickhouse_connect.cc_sqlalchemy.sql.ddlcompiler import ClickHouseDDLHelper
@@ -57,6 +58,12 @@ def clickhouse_writer(context: MigrationContext, revision, directives):
_add_common_imports(directive)
+def _is_clickhouse_autogen(autogen_context: AutogenContext) -> bool:
+ """True only when the active migration context targets the ClickHouse dialect."""
+ migration_context = getattr(autogen_context, "migration_context", None)
+ return isinstance(getattr(migration_context, "impl", None), ClickHouseImpl)
+
+
def render_clickhouse_column(column, autogen_context: AutogenContext) -> str:
rendered = render._user_defined_render("column", column, autogen_context)
if rendered is not False:
@@ -98,8 +105,19 @@ def render_clickhouse_column(column, autogen_context: AutogenContext) -> str:
)
+# Alembic renderers have no dialect qualifier, so replace=True overrides rendering
+# process-wide. Capture each built-in renderer before replacing it and delegate to it for
+# non-ClickHouse dialects so autogenerate stays correct for other databases (#832). Held on
+# the module so importlib.reload does not re-capture one of our own renderers and recurse.
+_DEFAULT_RENDERERS = globals().get("_DEFAULT_RENDERERS") or {
+ op: render.renderers.dispatch(op) for op in (ops.CreateTableOp, ops.AddColumnOp, ops.DropTableOp)
+}
+
+
@render.renderers.dispatch_for(ops.CreateTableOp, replace=True)
def render_create_table(autogen_context: AutogenContext, op: ops.CreateTableOp) -> str:
+ if not _is_clickhouse_autogen(autogen_context):
+ return _DEFAULT_RENDERERS[ops.CreateTableOp](autogen_context, op)
table = op.to_table()
args = [column for column in [render_clickhouse_column(column, autogen_context) for column in table.columns] if column] + sorted(
@@ -138,6 +156,8 @@ def render_create_table(autogen_context: AutogenContext, op: ops.CreateTableOp)
@render.renderers.dispatch_for(ops.AddColumnOp, replace=True)
def render_add_column(autogen_context: AutogenContext, op: ops.AddColumnOp) -> str:
+ if not _is_clickhouse_autogen(autogen_context):
+ return _DEFAULT_RENDERERS[ops.AddColumnOp](autogen_context, op)
schema, table_name, column, if_not_exists = op.schema, op.table_name, op.column, op.if_not_exists
prefix = render._alembic_autogenerate_prefix(autogen_context)
rendered_column = render_clickhouse_column(column, autogen_context)
@@ -155,6 +175,8 @@ def render_add_column(autogen_context: AutogenContext, op: ops.AddColumnOp) -> s
@render.renderers.dispatch_for(ops.DropTableOp, replace=True)
def render_drop_table(autogen_context: AutogenContext, op: ops.DropTableOp) -> str:
+ if not _is_clickhouse_autogen(autogen_context):
+ return _DEFAULT_RENDERERS[ops.DropTableOp](autogen_context, op)
prefix = render._alembic_autogenerate_prefix(autogen_context)
rendered = f"{prefix}drop_table({render._ident(op.table_name)!r}"
arguments = []
diff --git a/contrib/python/clickhouse-connect/clickhouse_connect/cc_sqlalchemy/alembic/impl.py b/contrib/python/clickhouse-connect/clickhouse_connect/cc_sqlalchemy/alembic/impl.py
index 1f942979c07..32e2b985cd4 100644
--- a/contrib/python/clickhouse-connect/clickhouse_connect/cc_sqlalchemy/alembic/impl.py
+++ b/contrib/python/clickhouse-connect/clickhouse_connect/cc_sqlalchemy/alembic/impl.py
@@ -65,7 +65,7 @@ class ClickHouseImpl(DefaultImpl):
if self.connection is None:
return
try:
- self.connection.connection.driver_connection.client._add_integration_tag("alembic")
+ self.connection.connection.driver_connection.client._add_integration_tag("alembic") # type: ignore[union-attr]
except Exception:
pass
@@ -100,7 +100,7 @@ class ClickHouseImpl(DefaultImpl):
construct,
execution_options=execution_options,
multiparams=multiparams,
- params=params or {},
+ params=params or {}, # type: ignore[arg-type]
)
def add_column(
diff --git a/contrib/python/clickhouse-connect/clickhouse_connect/cc_sqlalchemy/alembic/utils.py b/contrib/python/clickhouse-connect/clickhouse_connect/cc_sqlalchemy/alembic/utils.py
index 35ff8fb7b94..14ac01c27fc 100644
--- a/contrib/python/clickhouse-connect/clickhouse_connect/cc_sqlalchemy/alembic/utils.py
+++ b/contrib/python/clickhouse-connect/clickhouse_connect/cc_sqlalchemy/alembic/utils.py
@@ -75,9 +75,9 @@ def prevent_empty_migrations(writer_fn: Callable) -> Callable:
if not directives:
return
config = context.config
- if getattr(config.cmd_opts, "autogenerate", False):
+ if config is not None and getattr(config.cmd_opts, "autogenerate", False):
script = directives[0]
- if script.upgrade_ops.is_empty():
+ if script.upgrade_ops is not None and script.upgrade_ops.is_empty():
directives.clear()
return
writer_fn(context, revision, directives)
diff --git a/contrib/python/clickhouse-connect/clickhouse_connect/cc_sqlalchemy/datatypes/base.py b/contrib/python/clickhouse-connect/clickhouse_connect/cc_sqlalchemy/datatypes/base.py
index a4c1d4a94f2..175c203668e 100644
--- a/contrib/python/clickhouse-connect/clickhouse_connect/cc_sqlalchemy/datatypes/base.py
+++ b/contrib/python/clickhouse-connect/clickhouse_connect/cc_sqlalchemy/datatypes/base.py
@@ -15,11 +15,10 @@ class ChSqlaType:
subclasses will inherit from TypeEngine.
"""
- ch_type: ClickHouseType = None
+ ch_type: ClickHouseType | None = None
generic_type: None
- _ch_type_cls = None
- _instance = None
- _instance_cache: dict[TypeDef, "ChSqlaType"] = None
+ _ch_type_cls: type[ClickHouseType] | None = None
+ _instance_cache: dict[TypeDef, "ChSqlaType"] | None = None
def __init_subclass__(cls):
"""
@@ -44,7 +43,7 @@ class ChSqlaType:
:param type_def: -- TypeDef tuple that defines arguments for this instance
:return: Shared instance of a configured ChSqlaType
"""
- return cls._instance_cache.setdefault(type_def, cls(type_def=type_def))
+ return cls._instance_cache.setdefault(type_def, cls(type_def=type_def)) # type: ignore[union-attr]
def __init__(self, type_def: TypeDef = EMPTY_TYPE_DEF):
"""
@@ -55,7 +54,7 @@ class ChSqlaType:
parse_name function
"""
self.type_def = type_def
- self.ch_type = self._ch_type_cls.build(type_def)
+ self.ch_type = self._ch_type_cls.build(type_def) # type: ignore[union-attr]
@property
def name(self):
@@ -124,7 +123,7 @@ class CaseInsensitiveDict(dict):
sqla_type_map: dict[str, type[ChSqlaType]] = CaseInsensitiveDict()
-schema_types = []
+schema_types: list[str] = []
def sqla_type_from_name(name: str) -> ChSqlaType:
diff --git a/contrib/python/clickhouse-connect/clickhouse_connect/cc_sqlalchemy/datatypes/sqltypes.py b/contrib/python/clickhouse-connect/clickhouse_connect/cc_sqlalchemy/datatypes/sqltypes.py
index 5356150545b..ad7ee5ef173 100644
--- a/contrib/python/clickhouse-connect/clickhouse_connect/cc_sqlalchemy/datatypes/sqltypes.py
+++ b/contrib/python/clickhouse-connect/clickhouse_connect/cc_sqlalchemy/datatypes/sqltypes.py
@@ -2,6 +2,7 @@ import ipaddress
import uuid
from collections.abc import Sequence
from enum import Enum as PyEnum
+from typing import Any, cast
from sqlalchemy.exc import ArgumentError
from sqlalchemy.types import (
@@ -10,6 +11,7 @@ from sqlalchemy.types import (
Integer,
Interval,
Numeric,
+ TypeEngine,
UserDefinedType,
)
from sqlalchemy.types import (
@@ -33,67 +35,67 @@ from clickhouse_connect.driver import tzutil
from clickhouse_connect.driver.common import decimal_prec
-class Int8(ChSqlaType, Integer):
+class Int8(ChSqlaType, Integer): # type: ignore[misc]
pass
-class UInt8(ChSqlaType, Integer):
+class UInt8(ChSqlaType, Integer): # type: ignore[misc]
pass
-class Int16(ChSqlaType, Integer):
+class Int16(ChSqlaType, Integer): # type: ignore[misc]
pass
-class UInt16(ChSqlaType, Integer):
+class UInt16(ChSqlaType, Integer): # type: ignore[misc]
pass
-class Int32(ChSqlaType, Integer):
+class Int32(ChSqlaType, Integer): # type: ignore[misc]
pass
-class UInt32(ChSqlaType, Integer):
+class UInt32(ChSqlaType, Integer): # type: ignore[misc]
pass
-class Int64(ChSqlaType, Integer):
+class Int64(ChSqlaType, Integer): # type: ignore[misc]
pass
-class UInt64(ChSqlaType, Integer):
+class UInt64(ChSqlaType, Integer): # type: ignore[misc]
pass
-class Int128(ChSqlaType, Integer):
+class Int128(ChSqlaType, Integer): # type: ignore[misc]
pass
-class UInt128(ChSqlaType, Integer):
+class UInt128(ChSqlaType, Integer): # type: ignore[misc]
pass
-class Int256(ChSqlaType, Integer):
+class Int256(ChSqlaType, Integer): # type: ignore[misc]
pass
-class UInt256(ChSqlaType, Integer):
+class UInt256(ChSqlaType, Integer): # type: ignore[misc]
pass
-class Float32(ChSqlaType, Float):
+class Float32(ChSqlaType, Float): # type: ignore[misc]
def __init__(self, type_def: TypeDef = EMPTY_TYPE_DEF):
ChSqlaType.__init__(self, type_def)
Float.__init__(self)
-class Float64(ChSqlaType, Float):
+class Float64(ChSqlaType, Float): # type: ignore[misc]
def __init__(self, type_def: TypeDef = EMPTY_TYPE_DEF):
ChSqlaType.__init__(self, type_def)
Float.__init__(self)
-class Bool(ChSqlaType, SqlaBoolean):
+class Bool(ChSqlaType, SqlaBoolean): # type: ignore[misc]
def __init__(self, type_def: TypeDef = EMPTY_TYPE_DEF, **kwargs):
ChSqlaType.__init__(self, type_def)
SqlaBoolean.__init__(self, **kwargs)
@@ -103,10 +105,10 @@ class Boolean(Bool):
pass
-class Decimal(ChSqlaType, Numeric):
+class Decimal(ChSqlaType, Numeric): # type: ignore[misc]
dec_size = 0
- def __init__(self, precision: int = 0, scale: int = 0, type_def: TypeDef = None):
+ def __init__(self, precision: int = 0, scale: int = 0, type_def: TypeDef | None = None):
"""
Construct either with precision and scale (for DDL), or a TypeDef with those values (by name)
:param precision: Number of digits the Decimal
@@ -143,16 +145,16 @@ class Decimal256(Decimal):
dec_size = 256
-class Enum(ChSqlaType, UserDefinedType):
+class Enum(ChSqlaType, UserDefinedType): # type: ignore[misc]
_size = 16
python_type = str
def __init__(
self,
- enum: type[PyEnum] = None,
- keys: Sequence[str] = None,
- values: Sequence[int] = None,
- type_def: TypeDef = None,
+ enum: type[PyEnum] | None = None,
+ keys: Sequence[str] | None = None,
+ values: Sequence[int] | None = None,
+ type_def: TypeDef | None = None,
):
"""
Construct a ClickHouse enum either from a Python Enum or parallel lists of keys and value. Note that
@@ -166,6 +168,8 @@ class Enum(ChSqlaType, UserDefinedType):
if enum:
keys = [e.name for e in enum]
values = [e.value for e in enum]
+ if keys is None or values is None:
+ raise ArgumentError("Enum requires either a Python enum or both 'keys' and 'values'")
self._validate(keys, values)
if self.__class__.__name__ == "Enum":
if max(values) <= 127 and min(values) >= -128:
@@ -176,7 +180,7 @@ class Enum(ChSqlaType, UserDefinedType):
super().__init__(type_def)
@classmethod
- def _validate(cls, keys: Sequence, values: Sequence):
+ def _validate(cls, keys: Sequence[str], values: Sequence[int]):
bad_key = next((x for x in keys if not isinstance(x, str)), None)
if bad_key:
raise ArgumentError(f"ClickHouse enum key {bad_key} is not a string")
@@ -199,63 +203,63 @@ class Enum16(Enum):
_ch_type_cls = ChEnum16
-class String(ChSqlaType, UserDefinedType):
+class String(ChSqlaType, UserDefinedType): # type: ignore[misc]
python_type = str
-class FixedString(ChSqlaType, SqlaString):
- def __init__(self, size: int = -1, type_def: TypeDef = None):
+class FixedString(ChSqlaType, SqlaString): # type: ignore[misc]
+ def __init__(self, size: int = -1, type_def: TypeDef | None = None):
if not type_def:
type_def = TypeDef(values=(size,))
ChSqlaType.__init__(self, type_def)
SqlaString.__init__(self, size)
-class IPv4(ChSqlaType, UserDefinedType):
+class IPv4(ChSqlaType, UserDefinedType): # type: ignore[misc]
python_type = ipaddress.IPv4Address
-class IPv6(ChSqlaType, UserDefinedType):
+class IPv6(ChSqlaType, UserDefinedType): # type: ignore[misc]
python_type = ipaddress.IPv6Address
-class UUID(ChSqlaType, UserDefinedType):
+class UUID(ChSqlaType, UserDefinedType): # type: ignore[misc]
python_type = uuid.UUID
-class Nothing(ChSqlaType, UserDefinedType):
+class Nothing(ChSqlaType, UserDefinedType): # type: ignore[misc]
python_type = type(None)
-class Point(ChSqlaType, UserDefinedType):
+class Point(ChSqlaType, UserDefinedType): # type: ignore[misc]
python_type = tuple
-class Ring(ChSqlaType, UserDefinedType):
+class Ring(ChSqlaType, UserDefinedType): # type: ignore[misc]
python_type = list
-class Polygon(ChSqlaType, UserDefinedType):
+class Polygon(ChSqlaType, UserDefinedType): # type: ignore[misc]
python_type = list
-class MultiPolygon(ChSqlaType, UserDefinedType):
+class MultiPolygon(ChSqlaType, UserDefinedType): # type: ignore[misc]
python_type = list
-class LineString(ChSqlaType, UserDefinedType):
+class LineString(ChSqlaType, UserDefinedType): # type: ignore[misc]
python_type = list
-class MultiLineString(ChSqlaType, UserDefinedType):
+class MultiLineString(ChSqlaType, UserDefinedType): # type: ignore[misc]
python_type = list
-class Date(ChSqlaType, SqlaDate):
+class Date(ChSqlaType, SqlaDate): # type: ignore[misc]
pass
-class Date32(ChSqlaType, SqlaDate):
+class Date32(ChSqlaType, SqlaDate): # type: ignore[misc]
pass
@@ -283,8 +287,8 @@ def _resolve_tz_alias(tz, timezone):
return tz
-class DateTime(ChSqlaType, SqlaDateTime):
- def __init__(self, tz: str = None, type_def: TypeDef = None, timezone=_TIMEZONE_SENTINEL):
+class DateTime(ChSqlaType, SqlaDateTime): # type: ignore[misc]
+ def __init__(self, tz: str | None = None, type_def: TypeDef | None = None, timezone=_TIMEZONE_SENTINEL):
"""tz / timezone: IANA zone string (resolved via zoneinfo; install `tzdata` on Windows)."""
tz = _resolve_tz_alias(tz, timezone)
if not type_def:
@@ -297,8 +301,8 @@ class DateTime(ChSqlaType, SqlaDateTime):
SqlaDateTime.__init__(self)
-class DateTime64(ChSqlaType, SqlaDateTime):
- def __init__(self, precision: int = None, tz: str = None, type_def: TypeDef = None, timezone=_TIMEZONE_SENTINEL):
+class DateTime64(ChSqlaType, SqlaDateTime): # type: ignore[misc]
+ def __init__(self, precision: int | None = None, tz: str | None = None, type_def: TypeDef | None = None, timezone=_TIMEZONE_SENTINEL):
"""precision: 3/6/9 for ms/us/ns. tz / timezone: IANA zone string."""
tz = _resolve_tz_alias(tz, timezone)
if not type_def:
@@ -314,7 +318,7 @@ class DateTime64(ChSqlaType, SqlaDateTime):
SqlaDateTime.__init__(self)
-class Time(ChSqlaType, Interval):
+class Time(ChSqlaType, Interval): # type: ignore[misc]
"""
Represents the ClickHouse Time type, which corresponds to a timedelta.
@@ -336,7 +340,7 @@ class Time(ChSqlaType, Interval):
return None
-class Time64(ChSqlaType, Interval):
+class Time64(ChSqlaType, Interval): # type: ignore[misc]
"""
Represents the ClickHouse Time64 type with configurable precision.
@@ -345,7 +349,7 @@ class Time64(ChSqlaType, Interval):
If no precision is defined it default to 3.
"""
- def __init__(self, precision: int = None, type_def: TypeDef = None):
+ def __init__(self, precision: int | None = None, type_def: TypeDef | None = None):
"""
Time64 constructor with precision if not constructed with TypeDef.
:param precision: 3 (ms), 6 (us), or 9 (ns) for sub-second precision.
@@ -375,51 +379,29 @@ class Time64(ChSqlaType, Interval):
return None
-class Nullable:
- """
- Class "wrapper" to use in DDL construction. It is never actually initialized but instead creates the "wrapped"
- type with a Nullable wrapper
- """
+def Nullable(element: ChSqlaType | type[ChSqlaType]) -> ChSqlaType: # noqa: N802
+ """Wrap a ChSqlaType instance or class with a Nullable modifier for DDL construction."""
+ if callable(element):
+ return element(type_def=NULLABLE_TYPE_DEF)
+ orig = element.type_def
+ wrappers = orig if "Nullable" in orig.wrappers else orig.wrappers + ("Nullable",)
+ return element.__class__(type_def=TypeDef(wrappers, orig.keys, orig.values))
- def __new__(cls, element: ChSqlaType | type[ChSqlaType]):
- """
- Actually returns an instance of the enclosed type with a Nullable wrapper. If element is an instance,
- constructs a new instance with a copied TypeDef plus the Nullable wrapper. If element is just a type,
- constructs a new element of that type with only the Nullable wrapper.
- :param element: ChSqlaType instance or class to wrap
- """
- if callable(element):
- return element(type_def=NULLABLE_TYPE_DEF)
- orig = element.type_def
- wrappers = orig if "Nullable" in orig.wrappers else orig.wrappers + ("Nullable",)
- return element.__class__(type_def=TypeDef(wrappers, orig.keys, orig.values))
+def LowCardinality(element: ChSqlaType | type[ChSqlaType]) -> ChSqlaType: # noqa: N802
+ """Wrap a ChSqlaType instance or class with a LowCardinality modifier for DDL construction."""
+ if callable(element):
+ return element(type_def=LC_TYPE_DEF)
+ orig = element.type_def
+ wrappers = orig if "LowCardinality" in orig.wrappers else ("LowCardinality",) + orig.wrappers
+ return element.__class__(type_def=TypeDef(wrappers, orig.keys, orig.values))
-class LowCardinality:
- """
- Class "wrapper" to use in DDL construction. It is never actually instantiated but instead creates the "wrapped"
- type with a LowCardinality wrapper
- """
- def __new__(cls, element: ChSqlaType | type[ChSqlaType]):
- """
- Actually returns an instance of the enclosed type with a LowCardinality wrapper. If element is an instance,
- constructs a new instance with a copied TypeDef plus the LowCardinality wrapper. If element is just a type,
- constructs a new element of that type with only the LowCardinality wrapper.
- :param element: ChSqlaType instance or class to wrap
- """
- if callable(element):
- return element(type_def=LC_TYPE_DEF)
- orig = element.type_def
- wrappers = orig if "LowCardinality" in orig.wrappers else ("LowCardinality",) + orig.wrappers
- return element.__class__(type_def=TypeDef(wrappers, orig.keys, orig.values))
-
-
-class Array(ChSqlaType, ARRAY):
+class Array(ChSqlaType, ARRAY): # type: ignore[misc]
python_type = list
dimensions = 1
- def __init__(self, element: ChSqlaType | type[ChSqlaType] = None, type_def: TypeDef = None):
+ def __init__(self, element: ChSqlaType | type[ChSqlaType] | None = None, type_def: TypeDef | None = None):
"""
Array constructor that can take a wrapped Array type if not constructed from a TypeDef
:param element: ChSqlaType instance or class to wrap
@@ -428,23 +410,25 @@ class Array(ChSqlaType, ARRAY):
if not type_def:
if callable(element):
element = element()
+ if element is None:
+ raise ArgumentError("Array requires an element type or type_def")
type_def = TypeDef(values=(element.name,))
ChSqlaType.__init__(self, type_def)
# Set item_type directly; calling ARRAY.__init__ would reject nested Array(Array(T)),
# which CH supports natively (CH expresses dimensions via nesting, not a dim count).
# as_tuple has no class-level default, so set it here to satisfy ARRAY result processing.
- self.item_type = sqla_type_from_name(type_def.values[0])
+ self.item_type = cast("TypeEngine[Any]", sqla_type_from_name(type_def.values[0]))
self.as_tuple = False
-class Map(ChSqlaType, UserDefinedType):
+class Map(ChSqlaType, UserDefinedType): # type: ignore[misc]
python_type = dict
def __init__(
self,
- key_type: ChSqlaType | type[ChSqlaType] = None,
- value_type: ChSqlaType | type[ChSqlaType] = None,
- type_def: TypeDef = None,
+ key_type: ChSqlaType | type[ChSqlaType] | None = None,
+ value_type: ChSqlaType | type[ChSqlaType] | None = None,
+ type_def: TypeDef | None = None,
):
"""
Map constructor that can take a wrapped key/values types if not constructed from a TypeDef
@@ -457,18 +441,20 @@ class Map(ChSqlaType, UserDefinedType):
key_type = key_type()
if callable(value_type):
value_type = value_type()
+ if key_type is None or value_type is None:
+ raise ArgumentError("Map requires key_type and value_type, or type_def")
type_def = TypeDef(values=(key_type.name, value_type.name))
super().__init__(type_def)
-class Tuple(ChSqlaType, UserDefinedType):
+class Tuple(ChSqlaType, UserDefinedType): # type: ignore[misc]
python_type = tuple
def __init__(
self,
*args,
- elements: Sequence[ChSqlaType | type[ChSqlaType]] = None,
- type_def: TypeDef = None,
+ elements: Sequence[ChSqlaType | type[ChSqlaType]] | None = None,
+ type_def: TypeDef | None = None,
):
"""Tuple(UInt32, UUID) variadic form or Tuple(elements=[UInt32, UUID]) list form, not both."""
if type_def is None and not args and elements is None:
@@ -481,7 +467,7 @@ class Tuple(ChSqlaType, UserDefinedType):
raise ArgumentError("Cannot specify both positional elements and the 'elements' kwarg")
if args:
elements = args
- values = [et() if callable(et) else et for et in elements]
+ values = [et() if callable(et) else et for et in elements] # type: ignore[union-attr]
type_def = TypeDef(values=tuple(v.name for v in values))
super().__init__(type_def)
@@ -493,7 +479,7 @@ class Tuple(ChSqlaType, UserDefinedType):
return inst
-class JSON(ChSqlaType, UserDefinedType):
+class JSON(ChSqlaType, UserDefinedType): # type: ignore[misc]
"""
Note this isn't currently supported for insert/select, only table definitions
"""
@@ -501,7 +487,7 @@ class JSON(ChSqlaType, UserDefinedType):
python_type = dict
-class Nested(ChSqlaType, UserDefinedType):
+class Nested(ChSqlaType, UserDefinedType): # type: ignore[misc]
"""
Note this isn't currently supported for insert/select, only table definitions
"""
@@ -509,14 +495,14 @@ class Nested(ChSqlaType, UserDefinedType):
python_type = list
-class SimpleAggregateFunction(ChSqlaType, UserDefinedType):
+class SimpleAggregateFunction(ChSqlaType, UserDefinedType): # type: ignore[misc]
python_type = str
def __init__(
self,
- name: str = None,
- element: ChSqlaType | type[ChSqlaType] = None,
- type_def: TypeDef = None,
+ name: str | None = None,
+ element: ChSqlaType | type[ChSqlaType] | None = None,
+ type_def: TypeDef | None = None,
):
"""
Constructor that can take the SimpleAggregateFunction name and wrapped type if not constructed from a TypeDef
@@ -527,23 +513,20 @@ class SimpleAggregateFunction(ChSqlaType, UserDefinedType):
if not type_def:
if callable(element):
element = element()
- type_def = TypeDef(
- values=(
- name,
- element.name,
- )
- )
+ if element is None:
+ raise ArgumentError("SimpleAggregateFunction requires an element type or type_def")
+ type_def = TypeDef(values=(name, element.name))
super().__init__(type_def)
-class AggregateFunction(ChSqlaType, UserDefinedType):
+class AggregateFunction(ChSqlaType, UserDefinedType): # type: ignore[misc]
"""
Note this isn't currently supported for insert/select, only table definitions
"""
python_type = str
- def __init__(self, *params, type_def: TypeDef = None):
+ def __init__(self, *params, type_def: TypeDef | None = None):
"""
Simply wraps the parameters for AggregateFunction for DDL, unless the TypeDef is specified.
Callables or actual types are converted to their names.
@@ -551,7 +534,7 @@ class AggregateFunction(ChSqlaType, UserDefinedType):
:param type_def: TypeDef from parse_name function
"""
if not type_def:
- values = ()
+ values: tuple[Any, ...] = ()
for x in params:
if callable(x):
x = x()
@@ -562,10 +545,10 @@ class AggregateFunction(ChSqlaType, UserDefinedType):
super().__init__(type_def)
-class QBit(ChSqlaType, UserDefinedType):
+class QBit(ChSqlaType, UserDefinedType): # type: ignore[misc]
python_type = list
- def __init__(self, element_type: str = None, dimension: int = None, type_def: TypeDef = None):
+ def __init__(self, element_type: str | None = None, dimension: int | None = None, type_def: TypeDef | None = None):
"""
QBit constructor for bit-transposed vector types
:param element_type: Element type (BFloat16, Float32, or Float64)
diff --git a/contrib/python/clickhouse-connect/clickhouse_connect/cc_sqlalchemy/ddl/custom.py b/contrib/python/clickhouse-connect/clickhouse_connect/cc_sqlalchemy/ddl/custom.py
index b29e8e6b26a..2e7187ed6d6 100644
--- a/contrib/python/clickhouse-connect/clickhouse_connect/cc_sqlalchemy/ddl/custom.py
+++ b/contrib/python/clickhouse-connect/clickhouse_connect/cc_sqlalchemy/ddl/custom.py
@@ -12,8 +12,8 @@ class CreateDatabase(DDL):
def __init__(
self,
name: str,
- engine: str = None,
- zoo_path: str = None,
+ engine: str | None = None,
+ zoo_path: str | None = None,
shard_name: str = "{shard}",
replica_name: str = "{replica}",
exists_ok: bool = False,
diff --git a/contrib/python/clickhouse-connect/clickhouse_connect/cc_sqlalchemy/ddl/tableengine.py b/contrib/python/clickhouse-connect/clickhouse_connect/cc_sqlalchemy/ddl/tableengine.py
index 85759cfc6df..358c86aa0a5 100644
--- a/contrib/python/clickhouse-connect/clickhouse_connect/cc_sqlalchemy/ddl/tableengine.py
+++ b/contrib/python/clickhouse-connect/clickhouse_connect/cc_sqlalchemy/ddl/tableengine.py
@@ -4,9 +4,8 @@ from typing import Any
from sqlalchemy import Column
from sqlalchemy.exc import ArgumentError, SQLAlchemyError
-from sqlalchemy.sql.base import SchemaEventTarget
from sqlalchemy.sql.elements import TextClause
-from sqlalchemy.sql.visitors import Visitable
+from sqlalchemy.sql.schema import SchemaItem
from clickhouse_connect.cc_sqlalchemy.sql.sqlparse import split_top_level, walk_sql
from clickhouse_connect.driver.binding import format_str, quote_identifier
@@ -36,7 +35,7 @@ def _render_setting_value(value: Any) -> str:
return format_str(str(value))
-def tuple_expr(expr_name, value: EngineParam):
+def tuple_expr(expr_name: str, value: EngineParam) -> str:
"""
Create a table parameter with a tuple or list correctly formatted
:param expr_name: parameter
@@ -48,7 +47,7 @@ def tuple_expr(expr_name, value: EngineParam):
v = f"{expr_name.strip()}"
if isinstance(value, (tuple, list)):
return f" {v} ({','.join(_render_engine_expr(item) for item in value)})"
- return f"{v} {_render_engine_expr(value)}"
+ return f"{v} {_render_engine_expr(value)}" # type: ignore[arg-type]
def repr_engine_value(value: Any) -> str:
@@ -68,22 +67,22 @@ def repr_engine_value(value: Any) -> str:
return repr(value)
-class TableEngine(SchemaEventTarget, Visitable):
+class TableEngine(SchemaItem):
"""
SqlAlchemy Schema element to support ClickHouse table engines. At the moment provides no real
functionality other than the CREATE TABLE argument string
"""
- arg_names = ()
- quoted_args = set()
- optional_args = set()
- eng_params = ()
+ arg_names: Sequence[str] = ()
+ quoted_args: set[str] = set()
+ optional_args: set[str] = set()
+ eng_params: Sequence[str] = ()
def __init_subclass__(cls, **kwargs):
engine_map[cls.__name__] = cls
def __init__(self, kwargs):
- Visitable.__init__(self)
+ super().__init__()
self.name = self.__class__.__name__
te_name = f"{self.name} Table Engine"
self._orig_kwargs = kwargs.copy()
@@ -167,21 +166,21 @@ class Set(TableEngine):
class Dictionary(TableEngine):
arg_names = ["dictionary"]
- def __init__(self, dictionary: str = None):
+ def __init__(self, dictionary: str | None = None):
super().__init__(locals())
class Merge(TableEngine):
arg_names = ["db_name, tables_regexp"]
- def __init__(self, db_name: str = None, tables_regexp: str = None):
+ def __init__(self, db_name: str | None = None, tables_regexp: str | None = None):
super().__init__(locals())
class File(TableEngine):
arg_names = ["fmt"]
- def __init__(self, fmt: str = None):
+ def __init__(self, fmt: str | None = None):
super().__init__(locals())
@@ -189,7 +188,14 @@ class Distributed(TableEngine):
arg_names = ["cluster", "database", "table", "sharding_key", "policy_name"]
optional_args = {"sharding_key", "policy_name"}
- def __init__(self, cluster: str = None, database: str = None, table=None, sharding_key: str = None, policy_name: str = None):
+ def __init__(
+ self,
+ cluster: str | None = None,
+ database: str | None = None,
+ table=None,
+ sharding_key: str | None = None,
+ policy_name: str | None = None,
+ ):
super().__init__(locals())
@@ -229,9 +235,9 @@ class ReplacingMergeTree(TableEngine):
def __init__(
self,
- ver: str = None,
- version: str = None,
- is_deleted: str = None,
+ ver: str | None = None,
+ version: str | None = None,
+ is_deleted: str | None = None,
order_by: EngineParam = None,
primary_key: EngineParam = None,
partition_by: EngineParam = None,
@@ -260,7 +266,7 @@ class CollapsingMergeTree(TableEngine):
def __init__(
self,
- sign: str = None,
+ sign: str | None = None,
order_by: EngineParam = None,
primary_key: EngineParam = None,
partition_by: EngineParam = None,
@@ -279,8 +285,8 @@ class VersionedCollapsingMergeTree(TableEngine):
def __init__(
self,
- sign: str = None,
- version: str = None,
+ sign: str | None = None,
+ version: str | None = None,
order_by: EngineParam = None,
primary_key: EngineParam = None,
partition_by: EngineParam = None,
@@ -300,8 +306,8 @@ class GraphiteMergeTree(TableEngine):
def __init__(
self,
- config_section: str = None,
- version: str = None,
+ config_section: str | None = None,
+ version: str | None = None,
order_by: EngineParam = None,
primary_key: EngineParam = None,
partition_by: EngineParam = None,
@@ -326,8 +332,8 @@ class ReplicatedMergeTree(TableEngine):
primary_key: EngineParam = None,
partition_by: EngineParam = None,
sample_by: EngineParam = None,
- zk_path: str = None,
- replica: str = None,
+ zk_path: str | None = None,
+ replica: str | None = None,
ttl: EngineExpr | None = None,
settings: dict[str, Any] | None = None,
):
@@ -352,13 +358,13 @@ class ReplicatedReplacingMergeTree(TableEngine):
def __init__(
self,
- ver: str = None,
+ ver: str | None = None,
order_by: EngineParam = None,
primary_key: EngineParam = None,
partition_by: EngineParam = None,
sample_by: EngineParam = None,
- zk_path: str = None,
- replica: str = None,
+ zk_path: str | None = None,
+ replica: str | None = None,
ttl: EngineExpr | None = None,
settings: dict[str, Any] | None = None,
):
@@ -375,13 +381,13 @@ class ReplicatedCollapsingMergeTree(TableEngine):
def __init__(
self,
- sign: str = None,
+ sign: str | None = None,
order_by: EngineParam = None,
primary_key: EngineParam = None,
partition_by: EngineParam = None,
sample_by: EngineParam = None,
- zk_path: str = None,
- replica: str = None,
+ zk_path: str | None = None,
+ replica: str | None = None,
ttl: EngineExpr | None = None,
settings: dict[str, Any] | None = None,
):
@@ -398,14 +404,14 @@ class ReplicatedVersionedCollapsingMergeTree(TableEngine):
def __init__(
self,
- sign: str = None,
- version: str = None,
+ sign: str | None = None,
+ version: str | None = None,
order_by: EngineParam = None,
primary_key: EngineParam = None,
partition_by: EngineParam = None,
sample_by: EngineParam = None,
- zk_path: str = None,
- replica: str = None,
+ zk_path: str | None = None,
+ replica: str | None = None,
ttl: EngineExpr | None = None,
settings: dict[str, Any] | None = None,
):
@@ -422,13 +428,13 @@ class ReplicatedGraphiteMergeTree(TableEngine):
def __init__(
self,
- config_section: str = None,
+ config_section: str | None = None,
order_by: EngineParam = None,
primary_key: EngineParam = None,
partition_by: EngineParam = None,
sample_by: EngineParam = None,
- zk_path: str = None,
- replica: str = None,
+ zk_path: str | None = None,
+ replica: str | None = None,
ttl: EngineExpr | None = None,
settings: dict[str, Any] | None = None,
):
diff --git a/contrib/python/clickhouse-connect/clickhouse_connect/cc_sqlalchemy/dialect.py b/contrib/python/clickhouse-connect/clickhouse_connect/cc_sqlalchemy/dialect.py
index 368e80422ba..bdfd529af8d 100644
--- a/contrib/python/clickhouse-connect/clickhouse_connect/cc_sqlalchemy/dialect.py
+++ b/contrib/python/clickhouse-connect/clickhouse_connect/cc_sqlalchemy/dialect.py
@@ -31,7 +31,7 @@ class ClickHouseDialect(DefaultDialect):
postfetch_lastrowid = False
ddl_compiler = ChDDLCompiler
statement_compiler = ChStatementCompiler
- preparer = ChIdentifierPreparer
+ preparer = ChIdentifierPreparer # type: ignore[assignment]
description_encoding = None
max_identifier_length = 127
ischema_names = ischema_names
diff --git a/contrib/python/clickhouse-connect/clickhouse_connect/cc_sqlalchemy/sql/__init__.py b/contrib/python/clickhouse-connect/clickhouse_connect/cc_sqlalchemy/sql/__init__.py
index ec2cee4c942..3352936603f 100644
--- a/contrib/python/clickhouse-connect/clickhouse_connect/cc_sqlalchemy/sql/__init__.py
+++ b/contrib/python/clickhouse-connect/clickhouse_connect/cc_sqlalchemy/sql/__init__.py
@@ -1,3 +1,5 @@
+from typing import Any
+
from sqlalchemy import Table, and_
from sqlalchemy.sql.selectable import FromClause, Select
@@ -16,7 +18,7 @@ def full_table(table_name: str, schema: str | None = None) -> str:
return f"{quote_identifier(schema)}.{quote_identifier(table_name)}"
-def format_table(table: Table):
+def format_table(table: Table) -> str:
return full_table(table.name, table.schema)
@@ -45,21 +47,21 @@ def _resolve_target(select_stmt: Select, table: FromClause | None, method_name:
def _target_cache_key(target: FromClause) -> str:
if hasattr(target, "fullname"):
- return target.fullname
- return target.name
+ return target.fullname # type: ignore[attr-defined]
+ return target.name # type: ignore[attr-defined]
def final(select_stmt: Select, table: FromClause | None = None) -> Select:
"""Apply the ClickHouse FINAL modifier. For ReplacingMergeTree-family engines."""
target = _resolve_target(select_stmt, table, "final")
- ch_final = getattr(select_stmt, "_ch_final", set())
+ ch_final: set[FromClause] = getattr(select_stmt, "_ch_final", set())
if target in ch_final:
return select_stmt
hint_key = _target_cache_key(target)
new_stmt = select_stmt.with_statement_hint(f"FINAL:{hint_key}", dialect_name=_CH_MODIFIER_DIALECT)
- new_stmt._ch_final = ch_final | {target}
+ new_stmt._ch_final = ch_final | {target} # type: ignore[attr-defined]
return new_stmt
@@ -75,7 +77,7 @@ def sample(select_stmt: Select, sample_value: str | int | float, table: FromClau
new_stmt = select_stmt.with_statement_hint(f"SAMPLE:{hint_key}:{sample_value}", dialect_name=_CH_MODIFIER_DIALECT)
ch_sample = dict(getattr(select_stmt, "_ch_sample", {}))
ch_sample[target] = sample_value
- new_stmt._ch_sample = ch_sample
+ new_stmt._ch_sample = ch_sample # type: ignore[attr-defined]
return new_stmt
@@ -83,7 +85,7 @@ def _select_sample(self: Select, sample_value: str | int | float, table: FromCla
return sample(self, sample_value=sample_value, table=table)
-def _apply_array_join(select_stmt: Select, cols, alias, is_left: bool) -> Select:
+def _apply_array_join(select_stmt: Select, cols: Any, alias: Any, is_left: bool) -> Select:
if not isinstance(select_stmt, Select):
raise TypeError("array_join() expects a SQLAlchemy Select instance")
@@ -125,7 +127,7 @@ def _select_left_array_join(self: Select, *cols, alias=None) -> Select:
return _apply_array_join(self, cols, alias, is_left=True)
-def prewhere(select_stmt, whereclause):
+def prewhere(select_stmt: Select, whereclause: Any) -> Select:
"""Apply ClickHouse PREWHERE. Multiple calls compose with AND."""
if not isinstance(select_stmt, Select):
raise TypeError("prewhere() expects a SQLAlchemy Select instance")
@@ -136,11 +138,11 @@ def prewhere(select_stmt, whereclause):
# Hint key is str(combined) (structural, with bind placeholders) rather
# than id() so equivalent statements share a compiled-statement cache entry.
new_stmt = select_stmt.with_statement_hint(f"PREWHERE:{str(combined)}", dialect_name=_CH_MODIFIER_DIALECT)
- new_stmt._ch_prewhere = PreWhereClause(combined)
+ new_stmt._ch_prewhere = PreWhereClause(combined) # type: ignore[attr-defined]
return new_stmt
-def limit_by(select_stmt, by_clauses, limit, offset=None):
+def limit_by(select_stmt: Select, by_clauses: Any, limit: int, offset: int | None = None) -> Select:
"""Apply ClickHouse LIMIT BY (top-N per group). Renders `LIMIT [offset,] limit BY by_clauses`."""
if not isinstance(select_stmt, Select):
raise TypeError("limit_by() expects a SQLAlchemy Select instance")
@@ -151,21 +153,21 @@ def limit_by(select_stmt, by_clauses, limit, offset=None):
by_key = ",".join(str(c) for c in by_tuple)
new_stmt = select_stmt.with_statement_hint(f"LIMIT_BY:{limit}:{offset}:{by_key}", dialect_name=_CH_MODIFIER_DIALECT)
- new_stmt._ch_limit_by = LimitByClause(by_tuple, limit, offset)
+ new_stmt._ch_limit_by = LimitByClause(by_tuple, limit, offset) # type: ignore[attr-defined]
return new_stmt
-def _select_prewhere(self, whereclause):
+def _select_prewhere(self: Select, whereclause: Any) -> Select:
return prewhere(self, whereclause)
-def _select_limit_by(self, by_clauses, limit, offset=None):
+def _select_limit_by(self: Select, by_clauses: Any, limit: int, offset: int | None = None) -> Select:
return limit_by(self, by_clauses, limit, offset)
-Select.sample = _select_sample
-Select.final = _select_final
-Select.array_join = _select_array_join
-Select.left_array_join = _select_left_array_join
-Select.prewhere = _select_prewhere
-Select.limit_by = _select_limit_by
+Select.sample = _select_sample # type: ignore[attr-defined]
+Select.final = _select_final # type: ignore[attr-defined]
+Select.array_join = _select_array_join # type: ignore[attr-defined]
+Select.left_array_join = _select_left_array_join # type: ignore[attr-defined]
+Select.prewhere = _select_prewhere # type: ignore[attr-defined]
+Select.limit_by = _select_limit_by # type: ignore[attr-defined]
diff --git a/contrib/python/clickhouse-connect/clickhouse_connect/cc_sqlalchemy/sql/preparer.py b/contrib/python/clickhouse-connect/clickhouse_connect/cc_sqlalchemy/sql/preparer.py
index 4521eb97dfa..cda0f5a2ed8 100644
--- a/contrib/python/clickhouse-connect/clickhouse_connect/cc_sqlalchemy/sql/preparer.py
+++ b/contrib/python/clickhouse-connect/clickhouse_connect/cc_sqlalchemy/sql/preparer.py
@@ -4,7 +4,7 @@ from clickhouse_connect.driver.binding import quote_identifier
class ChIdentifierPreparer(IdentifierPreparer):
- quote_identifier = staticmethod(quote_identifier)
+ quote_identifier = staticmethod(quote_identifier) # type: ignore[assignment]
def __init__(self, dialect, **kwargs):
super().__init__(dialect, **kwargs)
diff --git a/contrib/python/clickhouse-connect/clickhouse_connect/datatypes/base.py b/contrib/python/clickhouse-connect/clickhouse_connect/datatypes/base.py
index 220a2dda579..ab5e17740c4 100644
--- a/contrib/python/clickhouse-connect/clickhouse_connect/datatypes/base.py
+++ b/contrib/python/clickhouse-connect/clickhouse_connect/datatypes/base.py
@@ -16,8 +16,8 @@ from clickhouse_connect.driver.query import QueryContext
from clickhouse_connect.driver.types import ByteSource
logger = logging.getLogger(__name__)
-ch_read_formats = {}
-ch_write_formats = {}
+ch_read_formats: dict[type, str] = {}
+ch_write_formats: dict[type, str] = {}
class TypeDef(NamedTuple):
@@ -45,10 +45,10 @@ class ClickHouseType(ABC): # noqa: B024
np_type = "O" # Default to Numpy Object type
nano_divisor = 0 # Only relevant for date like objects
byte_size = 0
- valid_formats = "native"
+ valid_formats: str | tuple[str, ...] = "native"
- python_type = None
- base_type = None
+ python_type: type | None = None
+ base_type: str | None = None
@property
def _null_time_unit(self):
@@ -110,7 +110,7 @@ class ClickHouseType(ABC): # noqa: B024
def insert_name(self):
return self.name
- def data_size(self, sample: Sequence) -> int:
+ def data_size(self, sample: Collection) -> int:
if self.low_card:
values = set(sample)
d_size = self._data_size(values) + 2
@@ -126,7 +126,7 @@ class ClickHouseType(ABC): # noqa: B024
total = 0
for x in sample:
total += len(str(x))
- return total / len(sample) + 1
+ return total // len(sample) + 1
def write_column_prefix(self, dest: bytearray):
"""
@@ -264,9 +264,9 @@ class ClickHouseType(ABC): # noqa: B024
def _write_column_low_card(self, column: Sequence, dest: bytearray, ctx: InsertContext):
if len(column) == 0:
return
- keys = []
- index = []
- rev_map = {}
+ keys: list[int] = []
+ index: list[Any] = []
+ rev_map: dict[Any, int] = {}
rmg = rev_map.get
if self.nullable:
index.append(None)
@@ -319,15 +319,16 @@ class ArrayType(ClickHouseType, ABC, registered=False):
"""
_signed = True
- _array_type = None
- _struct_type = None
+ _array_type: str | None = None
+ _struct_type: str | None = None
valid_formats = "string", "native"
- python_type = int
+ python_type: type = int
def __init_subclass__(cls, registered: bool = True):
super().__init_subclass__(registered)
if cls._array_type in ("i", "I") and int_size == 2:
- cls._array_type = "L" if cls._array_type.isupper() else "l"
+ array_type_char = cls._array_type
+ cls._array_type = "L" if array_type_char.isupper() else "l"
if isinstance(cls._array_type, str) and cls._array_type:
cls._struct_type = "<" + cls._array_type
cls.byte_size = array.array(cls._array_type).itemsize
@@ -335,14 +336,17 @@ class ArrayType(ClickHouseType, ABC, registered=False):
def _read_column_binary(self, source: ByteSource, num_rows: int, ctx: QueryContext, _read_state: Any):
if ctx.use_numpy:
return driver_ctypes.numpy_conv.read_numpy_array(source, self.np_type, num_rows)
+ assert self._array_type is not None
return source.read_array(self._array_type, num_rows)
def _read_nullable_column(self, source: ByteSource, num_rows: int, ctx: QueryContext, _read_state: Any) -> Sequence:
+ assert self._array_type is not None
return data_conv.read_nullable_array(source, self._array_type, num_rows, self._active_null(ctx))
def _build_lc_column(self, index: Sequence, keys: array.array, ctx: QueryContext):
if ctx.use_numpy:
- return options.np.fromiter((index[key] for key in keys), dtype=index.dtype, count=len(index))
+ # index is a numpy array when ctx.use_numpy is True
+ return options.np.fromiter((index[key] for key in keys), dtype=index.dtype, count=len(index)) # type: ignore[attr-defined]
return super()._build_lc_column(index, keys, ctx)
def _finalize_column(self, column: Sequence, ctx: QueryContext) -> Sequence:
@@ -357,6 +361,7 @@ class ArrayType(ClickHouseType, ABC, registered=False):
def _write_column_binary(self, column: Sequence | MutableSequence, dest: bytearray, ctx: InsertContext):
if len(column) and self.nullable:
column = [0 if x is None else x for x in column]
+ assert self._array_type is not None
write_array(self._array_type, column, dest, ctx.column_name)
def _active_null(self, ctx: QueryContext):
@@ -377,7 +382,7 @@ class UnsupportedType(ClickHouseType, ABC, registered=False):
super().__init__(type_def)
self._name_suffix = type_def.arg_str
- def _read_column_binary(self, source: Sequence, num_rows: int, ctx: QueryContext, read_state: Any):
+ def _read_column_binary(self, _source: ByteSource, _num_rows: int, _ctx: QueryContext, _read_state: Any):
raise NotSupportedError(f"{self.name} deserialization not supported")
def _write_column_binary(self, column: Sequence | MutableSequence, dest: bytearray, ctx: InsertContext):
diff --git a/contrib/python/clickhouse-connect/clickhouse_connect/datatypes/container.py b/contrib/python/clickhouse-connect/clickhouse_connect/datatypes/container.py
index 88e87540695..951e618d623 100644
--- a/contrib/python/clickhouse-connect/clickhouse_connect/datatypes/container.py
+++ b/contrib/python/clickhouse-connect/clickhouse_connect/datatypes/container.py
@@ -33,7 +33,7 @@ class Array(ClickHouseType):
def read_column_prefix(self, source: ByteSource, ctx: QueryContext):
return self.element_type.read_column_prefix(source, ctx)
- def _data_size(self, sample: Sequence) -> int:
+ def _data_size(self, sample: Collection[Any]) -> int:
if len(sample) == 0:
return 8
total = 0
@@ -139,7 +139,7 @@ class Tuple(ClickHouseType):
column = e_type.read_column_data(source, num_rows, ctx, read_state[ix])
columns.append(column)
if e_names and self.read_format(ctx) != "tuple":
- dicts = [{} for _ in range(num_rows)]
+ dicts: list[dict[str, Any]] = [{} for _ in range(num_rows)]
for ix, x in enumerate(dicts):
for y, key in enumerate(e_names):
x[key] = columns[y][ix]
@@ -163,7 +163,7 @@ class Tuple(ClickHouseType):
def convert_dict_insert(self, column: Sequence) -> Sequence:
names = self.element_names
- col = [[] for _ in names]
+ col: list[list[Any]] = [[] for _ in names]
for x in column:
for ix, name in enumerate(names):
col[ix].append(x.get(name))
diff --git a/contrib/python/clickhouse-connect/clickhouse_connect/datatypes/dynamic.py b/contrib/python/clickhouse-connect/clickhouse_connect/datatypes/dynamic.py
index 150fa1ced1f..4732ff1b957 100644
--- a/contrib/python/clickhouse-connect/clickhouse_connect/datatypes/dynamic.py
+++ b/contrib/python/clickhouse-connect/clickhouse_connect/datatypes/dynamic.py
@@ -156,7 +156,7 @@ class Variant(ClickHouseType):
v_count = len(self.element_types)
if v_count == 0:
return 1
- sub_samples = [[] for _ in range(v_count)]
+ sub_samples: list[list[Any]] = [[] for _ in range(v_count)]
for v in sample:
if v is None:
continue
@@ -197,7 +197,7 @@ def read_variant_column(
# Now we have to walk through each of the discriminators again to assign the correct value from
# the sub-column to the final result column
sub_indexes = [0] * v_count
- col = []
+ col: list[Any] = []
app_col = col.append
for disc in discriminators:
if disc == 255:
@@ -230,7 +230,9 @@ def read_dynamic_prefix(_, source: ByteSource, ctx: QueryContext) -> DynamicStat
class Dynamic(ClickHouseType):
python_type = object
- read_column_prefix = read_dynamic_prefix
+
+ def read_column_prefix(self, source: ByteSource, ctx: QueryContext) -> DynamicState:
+ return read_dynamic_prefix(self, source, ctx)
@property
def insert_name(self):
@@ -272,7 +274,7 @@ def write_json(ch_type: ClickHouseType, column: Sequence, dest: bytearray, ctx:
first = first_value(column, ch_type.nullable)
write_col = column
- encoding = ctx.encoding or ch_type.encoding
+ encoding: str | None = ctx.encoding or ch_type.encoding
if not isinstance(first, str) and ch_type.write_format(ctx) != "string":
to_json = any_to_json
if ch_type.nullable:
@@ -576,7 +578,7 @@ class JSON(ClickHouseType):
shared_columns = SHARED_DATA_TYPE.read_column_data(source, num_rows, ctx, read_state.shared_state) # noqa: F821 (undefined-name)
col = []
for row_num in range(num_rows):
- top = {}
+ top: dict[str, Any] = {}
for ix, field in enumerate(self.typed_paths):
_nest_value(top, field, typed_columns[ix][row_num])
for ix, field in enumerate(read_state.dynamic_paths):
diff --git a/contrib/python/clickhouse-connect/clickhouse_connect/datatypes/format.py b/contrib/python/clickhouse-connect/clickhouse_connect/datatypes/format.py
index fa09e2c9fdd..9bb02e72b7d 100644
--- a/contrib/python/clickhouse-connect/clickhouse_connect/datatypes/format.py
+++ b/contrib/python/clickhouse-connect/clickhouse_connect/datatypes/format.py
@@ -63,7 +63,7 @@ def _convert_arguments(*args, **kwargs) -> dict[str, str]:
return fmt_map
-def _matching_types(pattern: str, fmt: str = None) -> Sequence[type[ClickHouseType]]:
+def _matching_types(pattern: str, fmt: str | None = None) -> Sequence[type[ClickHouseType]]:
re_pattern = re.compile(pattern.replace("*", ".*"), re.IGNORECASE)
matches = [ch_type for type_name, ch_type in type_map.items() if re_pattern.match(type_name)]
if not matches:
diff --git a/contrib/python/clickhouse-connect/clickhouse_connect/datatypes/network.py b/contrib/python/clickhouse-connect/clickhouse_connect/datatypes/network.py
index e0e751cf390..69f200dd7b9 100644
--- a/contrib/python/clickhouse-connect/clickhouse_connect/datatypes/network.py
+++ b/contrib/python/clickhouse-connect/clickhouse_connect/datatypes/network.py
@@ -67,22 +67,24 @@ class IPv6(ClickHouseType):
"""Read IPv6 addresses in native format, always returning IPv6Address objects."""
fast_ip_v6 = IPv6Address.__new__
with_scope_id = "_scope_id" in IPv6Address.__slots__
- new_col = []
+ new_col: list[IPv6Address] = []
app = new_col.append
ifb = int.from_bytes
for _ in range(num_rows):
int_value = ifb(source.read_bytes(16), "big")
ipv6 = fast_ip_v6(IPv6Address)
- ipv6._ip = int_value
+ # Bypass IPv6Address.__init__ for performance; _ip and _scope_id are
+ # the internal representation used by CPython's ipaddress module.
+ ipv6._ip = int_value # type: ignore[attr-defined]
if with_scope_id:
- ipv6._scope_id = None
+ ipv6._scope_id = None # type: ignore[attr-defined]
app(ipv6)
return new_col
@staticmethod
def _read_binary_str(source: ByteSource, num_rows: int) -> list[str]:
"""Read IPv6 addresses in string format, always returning IPv6Address strings."""
- new_col = []
+ new_col: list[str] = []
app = new_col.append
tov6 = socket.inet_ntop
af6 = socket.AF_INET6
diff --git a/contrib/python/clickhouse-connect/clickhouse_connect/datatypes/numeric.py b/contrib/python/clickhouse-connect/clickhouse_connect/datatypes/numeric.py
index 37b0d83ed4c..90d2abea7ab 100644
--- a/contrib/python/clickhouse-connect/clickhouse_connect/datatypes/numeric.py
+++ b/contrib/python/clickhouse-connect/clickhouse_connect/datatypes/numeric.py
@@ -16,6 +16,8 @@ from clickhouse_connect.driver.types import ByteSource
class IntBase(ArrayType, registered=False):
+ _array_type: str
+
def _write_column_binary(self, column: Sequence | MutableSequence, dest: bytearray, ctx: InsertContext):
if len(column) == 0:
return
@@ -109,7 +111,7 @@ class BigInt(ClickHouseType, registered=False):
def _read_column_binary(self, source: ByteSource, num_rows: int, ctx: QueryContext, _read_state: Any):
signed = self._signed
sz = self.byte_size
- column = []
+ column: list[Any] = []
app = column.append
ifb = int.from_bytes
if self.read_format(ctx) == "string":
@@ -407,7 +409,7 @@ class BigDecimal(Decimal, registered=False):
def _read_column_binary(self, source: ByteSource, num_rows: int, _ctx: QueryContext, _read_state: Any):
dec = decimal.Decimal
scale = self.scale
- column = []
+ column: list[Any] = []
app = column.append
sz = self.byte_size
ifb = int.from_bytes
diff --git a/contrib/python/clickhouse-connect/clickhouse_connect/datatypes/registry.py b/contrib/python/clickhouse-connect/clickhouse_connect/datatypes/registry.py
index 3f5e45866c8..cb035782c85 100644
--- a/contrib/python/clickhouse-connect/clickhouse_connect/datatypes/registry.py
+++ b/contrib/python/clickhouse-connect/clickhouse_connect/datatypes/registry.py
@@ -1,4 +1,5 @@
import logging
+from typing import Any
from clickhouse_connect.datatypes.base import ClickHouseType, TypeDef, type_map
from clickhouse_connect.driver.exceptions import InternalError
@@ -18,7 +19,8 @@ def parse_name(name: str) -> tuple[str, str, TypeDef]:
"""
base = name
wrappers = []
- keys = tuple()
+ keys: tuple[Any, ...] = ()
+ values: tuple[Any, ...] = ()
if base.startswith("LowCardinality"):
wrappers.append("LowCardinality")
base = base[15:-1]
diff --git a/contrib/python/clickhouse-connect/clickhouse_connect/datatypes/special.py b/contrib/python/clickhouse-connect/clickhouse_connect/datatypes/special.py
index 7b030fb3662..f5f5df9f45d 100644
--- a/contrib/python/clickhouse-connect/clickhouse_connect/datatypes/special.py
+++ b/contrib/python/clickhouse-connect/clickhouse_connect/datatypes/special.py
@@ -1,4 +1,4 @@
-from collections.abc import MutableSequence, Sequence
+from collections.abc import Collection, MutableSequence, Sequence
from typing import Any
from uuid import UUID as PYUUID
@@ -30,7 +30,7 @@ class UUID(ClickHouseType):
@staticmethod
def _read_binary_str(source: ByteSource, num_rows: int):
v = source.read_array("Q", num_rows * 2)
- column = []
+ column: list[str] = []
app = column.append
for i in range(num_rows):
ix = i << 1
@@ -94,7 +94,7 @@ class SimpleAggregateFunction(ClickHouseType):
self.python_type = self.element_type.python_type
self.nano_divisor = self.element_type.nano_divisor
- def _data_size(self, sample: Sequence) -> int:
+ def _data_size(self, sample: Collection[Any]) -> int:
return self.element_type.data_size(sample)
def read_column_prefix(self, source: ByteSource, ctx: QueryContext):
diff --git a/contrib/python/clickhouse-connect/clickhouse_connect/datatypes/temporal.py b/contrib/python/clickhouse-connect/clickhouse_connect/datatypes/temporal.py
index 0f8ed6ad43f..c2105643afb 100644
--- a/contrib/python/clickhouse-connect/clickhouse_connect/datatypes/temporal.py
+++ b/contrib/python/clickhouse-connect/clickhouse_connect/datatypes/temporal.py
@@ -4,9 +4,9 @@ import array
import re
import zoneinfo
from abc import abstractmethod
-from collections.abc import MutableSequence, Sequence
+from collections.abc import Callable, MutableSequence, Sequence
from datetime import date, datetime, time, timedelta, tzinfo
-from typing import TYPE_CHECKING, Any, NamedTuple
+from typing import TYPE_CHECKING, Any, NamedTuple, cast
if TYPE_CHECKING:
import numpy
@@ -50,6 +50,7 @@ class Date(ClickHouseType):
if self.nullable:
column = [x if x else 0 for x in column]
else:
+ esd: date
if isinstance(first, datetime):
esd = epoch_start_datetime
else:
@@ -112,6 +113,7 @@ class Date32(Date):
class DateTimeBase(ClickHouseType, registered=False):
__slots__ = ("tzinfo",)
+ tzinfo: tzinfo | None
valid_formats = "native", "int"
python_type = datetime
@@ -143,12 +145,13 @@ class DateTimeBase(ClickHouseType, registered=False):
if isinstance(column, list):
column = options.pd.DatetimeIndex(column)
- if column.tz is None:
- result = column.astype(self.pandas_dtype)
+ dti = cast(Any, column)
+ if dti.tz is None:
+ result = dti.astype(self.pandas_dtype)
return options.pd.array(result) if self.nullable else result
- naive_ns = column.tz_convert("UTC").tz_localize(None).astype(self.pandas_dtype)
- tz_aware_result = naive_ns.tz_localize("UTC").tz_convert(column.tz)
+ naive_ns = dti.tz_convert("UTC").tz_localize(None).astype(self.pandas_dtype)
+ tz_aware_result = naive_ns.tz_localize("UTC").tz_convert(dti.tz)
return options.pd.array(tz_aware_result) if self.nullable else tz_aware_result
if self.nullable:
@@ -329,7 +332,6 @@ class TimeBase(ClickHouseType, registered=False):
_array_type: str
byte_size: int
- np_type: str
valid_formats = ("native", "string", "int", "time")
python_type = timedelta
@@ -403,7 +405,7 @@ class TimeBase(ClickHouseType, registered=False):
return [0] * len(column)
return []
- converter_map = {
+ converter_map: dict[type, Callable[..., int]] = {
timedelta: self._timedelta_to_ticks,
time: self._time_to_ticks,
float: self._numerical_to_ticks,
@@ -509,7 +511,7 @@ class TimeBase(ClickHouseType, registered=False):
raise NotImplementedError
@abstractmethod
- def _ticks_to_np_timedelta(self, ticks: int) -> timedelta:
+ def _ticks_to_np_timedelta(self, ticks: int) -> timedelta | numpy.timedelta64:
"""Convert integer ticks into an np.timedelta."""
raise NotImplementedError
@@ -638,7 +640,7 @@ class Time64(TimeBase):
return self._SECONDS_PER_DAY * self.precision - 1
@property
- def np_type(self) -> str:
+ def np_type(self):
return f"timedelta64{self.unit}"
@property
diff --git a/contrib/python/clickhouse-connect/clickhouse_connect/datatypes/vector.py b/contrib/python/clickhouse-connect/clickhouse_connect/datatypes/vector.py
index 0d666941f74..e0dff1b56c7 100644
--- a/contrib/python/clickhouse-connect/clickhouse_connect/datatypes/vector.py
+++ b/contrib/python/clickhouse-connect/clickhouse_connect/datatypes/vector.py
@@ -4,7 +4,7 @@ import logging
from collections.abc import Sequence
from math import ceil, nan
from struct import pack, unpack
-from typing import TYPE_CHECKING, Any
+from typing import TYPE_CHECKING, Any, cast
if TYPE_CHECKING:
import numpy
@@ -89,7 +89,7 @@ class QBit(ClickHouseType):
tuple_data = self._tuple_type.read_column_data(source, num_rows, ctx, read_state)
vectors = [self._untranspose_row(t) for t in tuple_data]
if self.nullable:
- return data_conv.build_nullable_column(vectors, null_map, self._active_null(ctx))
+ return data_conv.build_nullable_column(vectors, cast(bytes, null_map), self._active_null(ctx))
return vectors
def write_column_prefix(self, dest: bytearray):
diff --git a/contrib/python/clickhouse-connect/clickhouse_connect/dbapi/__init__.py b/contrib/python/clickhouse-connect/clickhouse_connect/dbapi/__init__.py
index 84a9a8c3fcc..99a298d4f0d 100644
--- a/contrib/python/clickhouse-connect/clickhouse_connect/dbapi/__init__.py
+++ b/contrib/python/clickhouse-connect/clickhouse_connect/dbapi/__init__.py
@@ -1,3 +1,5 @@
+from typing import Any
+
from clickhouse_connect.dbapi.connection import Connection
apilevel = "2.0" # PEP 249 DB API level
@@ -12,11 +14,11 @@ class Error(Exception):
def connect(
host: str | None = None,
database: str | None = None,
- username: str | None = "",
- password: str | None = "",
+ username: str = "",
+ password: str = "",
port: int | None = None,
- **kwargs,
-):
+ **kwargs: Any,
+) -> Connection:
secure = kwargs.pop("secure", False)
return Connection(
host=host,
diff --git a/contrib/python/clickhouse-connect/clickhouse_connect/dbapi/connection.py b/contrib/python/clickhouse-connect/clickhouse_connect/dbapi/connection.py
index 3dba3c54aef..7b0cafaf121 100644
--- a/contrib/python/clickhouse-connect/clickhouse_connect/dbapi/connection.py
+++ b/contrib/python/clickhouse-connect/clickhouse_connect/dbapi/connection.py
@@ -1,3 +1,5 @@
+from typing import Any
+
from clickhouse_connect.dbapi.cursor import Cursor
from clickhouse_connect.driver import create_client
from clickhouse_connect.driver.query import QueryResult
@@ -10,15 +12,15 @@ class Connection:
def __init__(
self,
- dsn: str = None,
+ dsn: str | None = None,
username: str = "",
password: str = "",
- host: str = None,
- database: str = None,
- interface: str = None,
- port: int = 0,
+ host: str | None = None,
+ database: str | None = None,
+ interface: str | None = None,
+ port: int | None = None,
secure: bool | str = False,
- **kwargs,
+ **kwargs: Any,
):
self.client = create_client(
host=host,
@@ -35,20 +37,20 @@ class Connection:
self.client._add_integration_tag("sqlalchemy")
self.timezone = self.client.server_tz
- def close(self):
+ def close(self) -> None:
self.client.close()
- def commit(self):
+ def commit(self) -> None:
pass
- def rollback(self):
+ def rollback(self) -> None:
pass
- def command(self, cmd: str):
+ def command(self, cmd: str) -> Any:
return self.client.command(cmd)
def raw_query(self, query: str) -> QueryResult:
return self.client.query(query)
- def cursor(self):
+ def cursor(self) -> Cursor:
return Cursor(self.client)
diff --git a/contrib/python/clickhouse-connect/clickhouse_connect/dbapi/cursor.py b/contrib/python/clickhouse-connect/clickhouse_connect/dbapi/cursor.py
index af7f36f67b6..9b200aba3a3 100644
--- a/contrib/python/clickhouse-connect/clickhouse_connect/dbapi/cursor.py
+++ b/contrib/python/clickhouse-connect/clickhouse_connect/dbapi/cursor.py
@@ -1,6 +1,7 @@
import logging
import re
from collections.abc import Mapping, Sequence
+from typing import Any, cast
from clickhouse_connect.datatypes.registry import get_from_name
from clickhouse_connect.driver import Client
@@ -23,34 +24,34 @@ class Cursor:
def __init__(self, client: Client):
self.client = client
- self.arraysize = 1
+ self.arraysize: int = 1
self.data: Sequence | None = None
- self.names = []
- self.types = []
- self._rowcount = 0
- self._summary: list[dict[str, str]] = []
- self._ix = 0
+ self.names: Sequence[str] = []
+ self.types: Sequence[Any] = []
+ self._rowcount: int = 0
+ self._summary: list[dict[str, Any]] = []
+ self._ix: int = 0
- def check_valid(self):
+ def check_valid(self) -> None:
if self.data is None:
raise ProgrammingError("Cursor is not valid")
@property
- def description(self):
+ def description(self) -> list[tuple[str, Any, None, None, None, None, bool]]:
return [(n, t, None, None, None, None, True) for n, t in zip(self.names, self.types)]
@property
- def rowcount(self):
+ def rowcount(self) -> int:
return self._rowcount
@property
- def summary(self) -> list[dict[str, str]]:
+ def summary(self) -> list[dict[str, Any]]:
return self._summary
- def close(self):
+ def close(self) -> None:
self.data = None
- def execute(self, operation: str, parameters=None):
+ def execute(self, operation: str, parameters: Any = None) -> None:
if not parameters and isinstance(operation, str):
# Per PEP 249 pyformat paramstyle, callers (e.g. SQLAlchemy) escape
# literal percent signs as %% in operation strings. When there are
@@ -80,7 +81,7 @@ class Cursor:
self.names = meta_result.column_names
self.types = [x.name for x in meta_result.column_types]
- def _try_bulk_insert(self, operation: str, data):
+ def _try_bulk_insert(self, operation: str, data: Any) -> bool:
match = insert_re.match(remove_sql_comments(operation))
if not match:
return False
@@ -94,24 +95,31 @@ class Cursor:
op_columns = None
if "VALUES" not in temp.upper():
return False
+ if not isinstance(data, Sequence) or len(data) == 0:
+ return False
first_row = data[0]
+ col_names: list[str] | str
+ data_values: Sequence[Sequence[Any]]
if isinstance(first_row, Mapping):
- col_names = list(first_row.keys())
- if op_columns and {unescape_identifier(x) for x in op_columns} != set(col_names):
+ col_names = [str(k) for k in first_row.keys()]
+ if op_columns and {unescape_identifier(str(x)) for x in op_columns} != set(col_names):
return False # Data sent in doesn't match the columns in the insert statement
data_values = [list(row.values()) for row in data]
elif isinstance(first_row, Sequence) and not isinstance(first_row, (str, bytes)):
# PEP 249 also allows rows as sequences; take column names from the
# insert statement if present, otherwise insert into all columns
- col_names = [unescape_identifier(x) for x in op_columns] if op_columns else "*"
+ col_names = [unescape_identifier(str(x)) for x in op_columns] if op_columns else "*"
data_values = data
else:
return False
- self.client.insert(table, data_values, col_names)
+ insert_summary = self.client.insert(table, data_values, col_names)
self.data = []
+ self._rowcount = insert_summary.written_rows
+ self._ix = 0
+ self._summary.append(insert_summary.summary)
return True
- def executemany(self, operation, parameters):
+ def executemany(self, operation: str, parameters: Any) -> None:
if not parameters or self._try_bulk_insert(operation, parameters):
return
self.data = []
@@ -138,22 +146,25 @@ class Cursor:
# Need to reset cursor _ix after performing an execute
self._ix = 0
- def fetchall(self):
+ def fetchall(self) -> Sequence:
self.check_valid()
- ret = self.data[self._ix :]
+ data = cast(Sequence, self.data)
+ ret = data[self._ix :]
self._ix = self._rowcount
return ret
- def fetchone(self):
+ def fetchone(self) -> Any:
self.check_valid()
if self._ix >= self._rowcount:
return None
- val = self.data[self._ix]
+ data = cast(Sequence, self.data)
+ val = data[self._ix]
self._ix += 1
return val
- def fetchmany(self, size: int = -1):
+ def fetchmany(self, size: int = -1) -> Sequence:
self.check_valid()
+ data = cast(Sequence, self.data)
if size < 0:
# Fetch all remaining rows
@@ -163,12 +174,12 @@ class Cursor:
return []
end = min(self._ix + size, self._rowcount)
- ret = self.data[self._ix : end]
+ ret = data[self._ix : end]
self._ix = end
return ret
- def nextset(self):
+ def nextset(self) -> None:
raise NotImplementedError
- def callproc(self, *args, **kwargs):
+ def callproc(self, *args, **kwargs) -> None:
raise NotImplementedError
diff --git a/contrib/python/clickhouse-connect/clickhouse_connect/driver/__init__.py b/contrib/python/clickhouse-connect/clickhouse_connect/driver/__init__.py
index 811286ad24c..1775a420230 100644
--- a/contrib/python/clickhouse-connect/clickhouse_connect/driver/__init__.py
+++ b/contrib/python/clickhouse-connect/clickhouse_connect/driver/__init__.py
@@ -44,23 +44,24 @@ def _parse_connection_params(
host: str | None,
username: str | None,
password: str,
- port: int,
- database: str,
+ port: int | None,
+ database: str | None,
interface: str | None,
secure: bool | str,
dsn: str | None,
kwargs: dict[str, Any],
-) -> tuple[str, str | None, str, int, str, str]:
+) -> tuple[str, str | None, str, int, str | None, str]:
"""Parse and normalize connection parameters including DSN parsing."""
+ if database == "__default__": # legacy sentinel for "not specified"
+ database = None
if dsn:
parsed = urlparse(dsn)
username = username or _unquote(parsed.username)
password = password or _unquote(parsed.password) or ""
host = host or parsed.hostname
port = port or parsed.port
- if parsed.path and (not database or database == "__default__"):
- database = unquote(parsed.path[1:].split("/")[0])
- database = database or parsed.path
+ if not database and parsed.path:
+ database = unquote(parsed.path[1:].split("/")[0]) or None
for k, v in parse_qs(parsed.query).items():
kwargs[k] = v[0]
use_tls = str(secure).lower() == "true" or interface == "https" or (not interface and str(port) in ("443", "8443"))
@@ -81,7 +82,12 @@ def _parse_connection_params(
return host, username, password, port, database, interface
-def _validate_access_token(access_token: str | None, token_provider: Callable[[], str] | None, username: str | None, password: str) -> None:
+def _validate_access_token(
+ access_token: str | None,
+ token_provider: Callable[[], str | Awaitable[str]] | None,
+ username: str | None,
+ password: str,
+) -> None:
"""Validate that token-based and username/password auth are not mixed."""
if (access_token or token_provider) and (username or password):
raise ProgrammingError("Cannot use both token authentication and username/password")
@@ -110,9 +116,9 @@ def create_client(
password: str = "",
access_token: str | None = None,
token_provider: Callable[[], str] | None = None,
- database: str = "__default__",
+ database: str | None = None,
interface: str | None = None,
- port: int = 0,
+ port: int | None = None,
secure: bool | str = False,
dsn: str | None = None,
settings: dict[str, Any] | None = None,
@@ -224,7 +230,7 @@ def create_client(
interface,
host,
port,
- username,
+ username or "",
password,
database,
access_token,
@@ -243,9 +249,9 @@ async def create_async_client(
password: str = "",
access_token: str | None = None,
token_provider: Callable[[], str | Awaitable[str]] | None = None,
- database: str = "__default__",
+ database: str | None = None,
interface: str | None = None,
- port: int = 0,
+ port: int | None = None,
secure: bool | str = False,
dsn: str | None = None,
settings: dict[str, Any] | None = None,
diff --git a/contrib/python/clickhouse-connect/clickhouse_connect/driver/asyncclient.py b/contrib/python/clickhouse-connect/clickhouse_connect/driver/asyncclient.py
index b0301df84ca..d7bc03884d4 100644
--- a/contrib/python/clickhouse-connect/clickhouse_connect/driver/asyncclient.py
+++ b/contrib/python/clickhouse-connect/clickhouse_connect/driver/asyncclient.py
@@ -14,17 +14,18 @@ import uuid
import zlib
import zoneinfo
from base64 import b64encode
-from collections.abc import Awaitable, Callable, Generator, Iterable, Sequence
+from collections.abc import Awaitable, Callable, Generator, Sequence
from datetime import timezone, tzinfo
from importlib import import_module
from importlib.metadata import version as dist_version
-from typing import TYPE_CHECKING, Any, BinaryIO
+from typing import TYPE_CHECKING, Any, BinaryIO, cast
import aiohttp
import lz4.frame
import zstandard
if TYPE_CHECKING:
+ import numpy
import pandas
import polars
import pyarrow
@@ -64,6 +65,7 @@ from clickhouse_connect.driver.query import (
from clickhouse_connect.driver.streaming import StreamingFileAdapter, StreamingInsertSource, StreamingResponseSource
from clickhouse_connect.driver.summary import QuerySummary
from clickhouse_connect.driver.transform import NativeTransform
+from clickhouse_connect.driver.types import Closable
logger = logging.getLogger(__name__)
columns_only_re = re.compile(r"LIMIT 0\s*$", re.IGNORECASE)
@@ -319,7 +321,7 @@ class AsyncClient(Client):
self._ssl_context = ssl_context
self._proxy_url = proxy_url
- self._connector_kwargs = {
+ self._connector_kwargs: dict[str, Any] = {
"limit": connector_limit,
"limit_per_host": connector_limit_per_host,
"keepalive_timeout": keepalive_timeout,
@@ -337,10 +339,10 @@ class AsyncClient(Client):
self._read_format = "Native"
self._write_format = "Native"
self._transform = NativeTransform()
- self._client_settings = {}
+ self._client_settings: dict[str, str] = {}
self._initialized = False
- self._reported_libs = set()
- self._last_pool_reset = None
+ self._reported_libs: set[str] = set()
+ self._last_pool_reset: float | None = None
self.headers["User-Agent"] = self.headers["User-Agent"].replace("mode:sync;", "mode:async;")
if headers:
self.headers.update(headers)
@@ -505,18 +507,18 @@ class AsyncClient(Client):
self._session = None
raise
- async def __aenter__(self):
+ async def __aenter__(self) -> AsyncClient:
"""Async context manager entry."""
if not self._initialized:
await self._initialize()
return self
- async def __aexit__(self, exc_type, exc_val, exc_tb):
+ async def __aexit__(self, exc_type, exc_val, exc_tb) -> bool:
"""Async context manager exit."""
await self.close()
return False
- async def close(self): # type: ignore[override]
+ async def close(self) -> None: # type: ignore[override]
async with self._session_lock:
old_lease = self._session_lease
self._session_lease = None
@@ -524,7 +526,7 @@ class AsyncClient(Client):
await old_lease.wait_drained()
await old_lease.session.close()
- async def close_connections(self): # type: ignore[override]
+ async def close_connections(self) -> None: # type: ignore[override]
"""Rotate the connection pool: new requests use a fresh session; in-flight
requests keep using the old session until they complete, then it's closed."""
async with self._session_lock:
@@ -543,7 +545,7 @@ class AsyncClient(Client):
await old_lease.wait_drained()
await old_lease.session.close()
- def set_client_setting(self, key, value):
+ def set_client_setting(self, key: str, value: Any) -> None:
str_value = self._validate_setting(key, value, common.get_setting("invalid_setting_action"))
if str_value is not None:
self._client_settings[key] = str_value
@@ -555,12 +557,12 @@ class AsyncClient(Client):
# Run sync providers off the event loop; await async providers.
# The provider may be called concurrently if multiple requests get a 516 at the same time;
# it must be safe to invoke in parallel (e.g. if it hits an IdP, consider rate limiting).
- result = await asyncio.get_running_loop().run_in_executor(None, self._token_provider)
+ result = await asyncio.get_running_loop().run_in_executor(None, cast(Callable[[], str | Awaitable[str]], self._token_provider))
if inspect.isawaitable(result):
result = await result
return result
- def set_access_token(self, access_token: str):
+ def set_access_token(self, access_token: str) -> None:
auth_header = self.headers.get("Authorization")
if auth_header and not auth_header.startswith("Bearer"):
raise ProgrammingError("Cannot set access token when a different auth type is used")
@@ -578,17 +580,18 @@ class AsyncClient(Client):
return final_query + fmt
async def _query_with_context(self, context: QueryContext) -> QueryResult: # type: ignore[override]
- headers = {}
- params = {}
+ headers: dict[str, Any] = {}
+ params: dict[str, str] = {}
if self.database:
params["database"] = self.database
if self.protocol_version:
- params["client_protocol_version"] = self.protocol_version
+ params["client_protocol_version"] = str(self.protocol_version)
context.block_info = True
params.update(self._validate_settings(context.settings))
context.rename_response_column = self._rename_response_column
use_form = use_form_encoding(context.final_query, context.bind_params, self.form_encode_query_params)
+ files: dict[str, Any] | None = None
if not context.is_insert and columns_only_re.search(context.uncommented_query):
fmt_json_query = f"{context.final_query}\n FORMAT JSON"
fields = {"query": fmt_json_query}
@@ -641,7 +644,7 @@ class AsyncClient(Client):
logger.debug("Failed to rename col '%s'. Skipping rename. Error: %s", name, e)
names.append(name)
types.append(get_from_name(col["type"]))
- return QueryResult([], None, tuple(names), tuple(types))
+ return QueryResult([], None, tuple(names), tuple(types)) # type: ignore[arg-type]
if self.compression:
headers["Accept-Encoding"] = self.compression
@@ -651,7 +654,7 @@ class AsyncClient(Client):
final_query = self._prep_query(context)
files = None
- data = None
+ data: Any = None
if use_form:
fields = {"query": final_query}
@@ -755,7 +758,10 @@ class AsyncClient(Client):
"""
if query and query.lower().strip().startswith("select __connect_version__"):
return QueryResult(
- [[f"ClickHouse Connect v.{common.version()} â“’ ClickHouse Inc."]], None, ("connect_version",), (get_from_name("String"),)
+ [[f"ClickHouse Connect v.{common.version()} â“’ ClickHouse Inc."]],
+ None, # type: ignore[arg-type] # QueryContext.generator not yet Optional; widen after #805 merges
+ ("connect_version",),
+ (get_from_name("String"),), # type: ignore[arg-type]
)
if not context:
context = self.create_query_context(
@@ -856,7 +862,7 @@ class AsyncClient(Client):
"""
return (await self._context_query(locals(), use_numpy=False, streaming=True)).rows_stream
- async def query_np(
+ async def query_np( # type: ignore[override]
self,
query: str | None = None,
parameters: Sequence | dict[str, Any] | None = None,
@@ -869,7 +875,7 @@ class AsyncClient(Client):
context: QueryContext | None = None,
external_data: ExternalData | None = None,
transport_settings: dict[str, str] | None = None,
- ):
+ ) -> numpy.ndarray:
check_numpy()
self._add_integration_tag("numpy")
return (await self._context_query(locals(), use_numpy=True)).np_result
@@ -910,7 +916,7 @@ class AsyncClient(Client):
use_extended_dtypes: bool | None = None,
transport_settings: dict[str, str] | None = None,
tz_mode: TzMode | None = None,
- ):
+ ) -> pandas.DataFrame:
check_pandas()
self._add_integration_tag("pandas")
return (await self._context_query(locals(), use_numpy=True, as_pandas=True)).df_result
@@ -938,7 +944,7 @@ class AsyncClient(Client):
self._add_integration_tag("pandas")
return (await self._context_query(locals(), use_numpy=True, as_pandas=True, streaming=True)).df_stream
- async def _context_query(self, lcls: dict, **overrides): # type: ignore[override]
+ async def _context_query(self, lcls: dict, **overrides):
"""
Helper method to create query context and execute query.
Matches sync client pattern for consistency.
@@ -983,6 +989,8 @@ class AsyncClient(Client):
raise ProgrammingError("Command sent without query or recognized data") from None
if payload or files:
+ if isinstance(cmd, bytes):
+ raise ProgrammingError("Binary parameter bind cannot be combined with command data or external data") from None
params["query"] = cmd
else:
payload = cmd
@@ -1104,7 +1112,7 @@ class AsyncClient(Client):
async for chunk in response.content.iter_any():
yield chunk
- class _RawStreamSource:
+ class _RawStreamSource(Closable):
def close(self):
try:
response.close()
@@ -1164,7 +1172,7 @@ class AsyncClient(Client):
self,
table: str | None = None,
data: Sequence[Sequence[Any]] | None = None,
- column_names: str | Iterable[str] = "*",
+ column_names: str | Sequence[str] | None = "*",
database: str | None = None,
column_types: Sequence[ClickHouseType] | None = None,
column_type_names: Sequence[str] | None = None,
@@ -1195,6 +1203,8 @@ class AsyncClient(Client):
if (context is None or context.empty) and data is None:
raise ProgrammingError("No data specified for insert") from None
if context is None:
+ if table is None:
+ raise ProgrammingError("No table specified for insert") from None
context = await self.create_insert_context(
table,
column_names,
@@ -1219,7 +1229,7 @@ class AsyncClient(Client):
use_strings: bool | None = None,
external_data: ExternalData | None = None,
transport_settings: dict[str, str] | None = None,
- ):
+ ) -> pyarrow.Table:
"""
Query method using the ClickHouse Arrow format to return a PyArrow table
:param query: Query statement/format string
@@ -1312,9 +1322,9 @@ class AsyncClient(Client):
streaming_source = StreamingResponseSource(response, encoding=encoding, exception_tag=exception_tag)
await streaming_source.start_producer(loop)
- queue = AsyncSyncQueue(maxsize=10)
+ queue: AsyncSyncQueue = AsyncSyncQueue(maxsize=10)
- class _ArrowStreamSource:
+ class _ArrowStreamSource(Closable):
def __init__(self, source, q):
self._source = source
self._queue = q
@@ -1404,7 +1414,7 @@ class AsyncClient(Client):
check_polars()
self._add_integration_tag("polars")
- def converter(table: pyarrow.Table) -> polars.DataFrame:
+ def converter(table: pyarrow.Table) -> polars.DataFrame: # type: ignore[misc]
table = _apply_arrow_tz_policy(table, self.tz_mode)
return options.pl.from_arrow(table)
@@ -1458,7 +1468,7 @@ class AsyncClient(Client):
check_polars()
self._add_integration_tag("polars")
- def converter(table: pyarrow.Table) -> polars.DataFrame:
+ def converter(table: pyarrow.Table) -> polars.DataFrame: # type: ignore[misc]
table = _apply_arrow_tz_policy(table, self.tz_mode)
return options.pl.from_arrow(table)
@@ -1482,9 +1492,9 @@ class AsyncClient(Client):
streaming_source = StreamingResponseSource(response, encoding=encoding, exception_tag=exception_tag)
await streaming_source.start_producer(loop)
- queue = AsyncSyncQueue(maxsize=10)
+ queue: AsyncSyncQueue = AsyncSyncQueue(maxsize=10)
- class _ArrowDFStreamSource:
+ class _ArrowDFStreamSource(Closable):
def __init__(self, source, q):
self._source = source
self._queue = q
@@ -1560,7 +1570,7 @@ class AsyncClient(Client):
column_names, insert_block = arrow_buffer(arrow_table, compression)
if hasattr(insert_block, "to_pybytes"):
insert_block = insert_block.to_pybytes()
- return await self.raw_insert(full_table, column_names, insert_block, settings, "Arrow", transport_settings)
+ return await self.raw_insert(full_table, column_names, insert_block, settings, "Arrow", transport_settings=transport_settings)
async def insert_df_arrow( # type: ignore[override]
self,
@@ -1719,8 +1729,8 @@ class AsyncClient(Client):
active_source.start_producer()
return active_source.async_generator()
- headers = {"Content-Type": "application/octet-stream"}
- if context.compression:
+ headers: dict[str, Any] = {"Content-Type": "application/octet-stream"}
+ if isinstance(context.compression, str):
headers["Content-Encoding"] = context.compression
params = {}
@@ -1829,9 +1839,11 @@ class AsyncClient(Client):
query = f"INSERT INTO {table}{cols} FORMAT {fmt_str}"
if not compression and isinstance(insert_block, str):
insert_block = query + "\n" + insert_block
- elif not compression and isinstance(insert_block, (bytes, bytearray, BinaryIO)):
+ elif not compression and isinstance(insert_block, (bytes, bytearray)):
insert_block = (query + "\n").encode() + insert_block
else:
+ # Generators, file-like objects, and compressed data: send the
+ # INSERT query as a URL param and stream the body as-is.
params["query"] = query
if self.database:
@@ -2040,7 +2052,7 @@ class AsyncClient(Client):
response = await session.request(**request_kwargs)
if 200 <= response.status < 300 and not response.headers.get(ex_header):
# Caller releases lease after consuming the body.
- response._lease_release = _one_shot(lease.release)
+ response._lease_release = _one_shot(lease.release) # type: ignore[attr-defined]
lease_released = True
return response
diff --git a/contrib/python/clickhouse-connect/clickhouse_connect/driver/asyncqueue.py b/contrib/python/clickhouse-connect/clickhouse_connect/driver/asyncqueue.py
index a318f59cea1..3d1c277ebd6 100644
--- a/contrib/python/clickhouse-connect/clickhouse_connect/driver/asyncqueue.py
+++ b/contrib/python/clickhouse-connect/clickhouse_connect/driver/asyncqueue.py
@@ -1,7 +1,7 @@
import asyncio
import threading
from collections import deque
-from typing import Generic, TypeVar
+from typing import Any, Generic, TypeVar
from clickhouse_connect.driver.exceptions import ProgrammingError
@@ -9,7 +9,8 @@ __all__ = ["AsyncSyncQueue", "Empty", "Full", "EOF_SENTINEL"]
T = TypeVar("T")
-EOF_SENTINEL = object()
+# Typed Any so it can stand in for a queued T value on EOF without a cast in every reader.
+EOF_SENTINEL: Any = object()
class AsyncSyncQueue(Generic[T]):
@@ -73,7 +74,8 @@ class AsyncSyncQueue(Generic[T]):
while waiter_queue:
fut = waiter_queue.popleft()
if not fut.done():
- self._loop.call_soon_threadsafe(self._safe_set_result, fut)
+ # _bind_loop() runs before any Future is created, so _loop is always set here
+ self._loop.call_soon_threadsafe(self._safe_set_result, fut) # type: ignore[union-attr]
break
def shutdown(self):
@@ -161,7 +163,8 @@ class _AsyncQueueInterface(Generic[T]):
if self._p._shutdown:
return EOF_SENTINEL
- fut = self._p._loop.create_future()
+ # _bind_loop() is called at the top of get(), so _loop is always set here
+ fut = self._p._loop.create_future() # type: ignore[union-attr]
self._p._async_getters.append(fut)
try:
@@ -185,7 +188,8 @@ class _AsyncQueueInterface(Generic[T]):
self._p._wakeup_async_waiter(self._p._async_getters)
return
- fut = self._p._loop.create_future()
+ # _bind_loop() is called at the top of put(), so _loop is always set here
+ fut = self._p._loop.create_future() # type: ignore[union-attr]
self._p._async_putters.append(fut)
try:
diff --git a/contrib/python/clickhouse-connect/clickhouse_connect/driver/binding.py b/contrib/python/clickhouse-connect/clickhouse_connect/driver/binding.py
index b4f848dc77a..52fe6333ce7 100644
--- a/contrib/python/clickhouse-connect/clickhouse_connect/driver/binding.py
+++ b/contrib/python/clickhouse-connect/clickhouse_connect/driver/binding.py
@@ -23,7 +23,7 @@ class DT64Param:
def __init__(self, value: datetime):
self.value = value
- def format(self, tz: tzinfo, top_level: bool) -> str:
+ def format(self, tz: tzinfo | None, top_level: bool) -> str:
value = self.value
if tz:
value = value.astimezone(tz)
@@ -128,12 +128,13 @@ def bind_query(
query: str,
parameters: Sequence | dict[str, Any] | None,
server_tz: tzinfo | None = None,
-) -> tuple[str, dict[str, str]]:
+) -> tuple[str | bytes, dict[str, str]]:
query = query.rstrip(";")
if not parameters:
return query, {}
binary_binds = None
+ bound_params: dict[str, str] = {}
if isinstance(parameters, dict):
params_copy = dict_copy(parameters)
@@ -186,13 +187,14 @@ def bind_query(
break
binary_indexes[item_index + len(key)] = key, v
item_index += len(key)
- query = b""
+ binary_out = b""
start = 0
for loc in sorted(binary_indexes.keys()):
key, value = binary_indexes[loc]
- query += binary_query[start:loc] + value + key
+ binary_out += binary_query[start:loc] + value + key
start = loc
- query += binary_query[start:]
+ binary_out += binary_query[start:]
+ return binary_out, bound_params
return query, bound_params
@@ -232,7 +234,7 @@ def escape_bytes(value):
return "".join(f"{BS}x{b:02x}" for b in value)
-def format_query_value(value: Any, server_tz: tzinfo = timezone.utc):
+def format_query_value(value: Any, server_tz: tzinfo | None = timezone.utc):
"""
Format Python values in a ClickHouse query
:param value: Python object
@@ -269,11 +271,11 @@ def format_query_value(value: Any, server_tz: tzinfo = timezone.utc):
return value
-def str_query_value(value: Any, server_tz: tzinfo = timezone.utc):
+def str_query_value(value: Any, server_tz: tzinfo | None = timezone.utc):
return str(format_query_value(value, server_tz))
-def format_bind_value(value: Any, server_tz: tzinfo = timezone.utc, top_level: bool = True):
+def format_bind_value(value: Any, server_tz: tzinfo | None = timezone.utc, top_level: bool = True):
"""
Format Python values in a ClickHouse query
:param value: Python object
diff --git a/contrib/python/clickhouse-connect/clickhouse_connect/driver/buffer.py b/contrib/python/clickhouse-connect/clickhouse_connect/driver/buffer.py
index 97c01670b09..c0da348660f 100644
--- a/contrib/python/clickhouse-connect/clickhouse_connect/driver/buffer.py
+++ b/contrib/python/clickhouse-connect/clickhouse_connect/driver/buffer.py
@@ -114,11 +114,11 @@ class ResponseBuffer(ByteSource):
def read_str_col(
self,
num_rows: int,
- encoding: str,
+ encoding: str | None,
nullable: bool = False,
null_obj: Any = None,
) -> Iterable[str]:
- column = []
+ column: list[Any] = []
app = column.append
null_map = self.read_bytes(num_rows) if nullable else None
for ix in range(num_rows):
@@ -148,7 +148,7 @@ class ResponseBuffer(ByteSource):
def read_fixed_str_col(self, sz: int, num_rows: int, encoding: str) -> Iterable[str]:
source = self.read_bytes(sz * num_rows)
- column = []
+ column: list[str] = []
app = column.append
for ix in range(0, sz * num_rows, sz):
try:
@@ -167,7 +167,7 @@ class ResponseBuffer(ByteSource):
return column
@property
- def last_message(self) -> bytes:
+ def last_message(self) -> bytes | None: # type: ignore[override] # overrides writable attr with property
return self.buffer
def close(self):
diff --git a/contrib/python/clickhouse-connect/clickhouse_connect/driver/client.py b/contrib/python/clickhouse-connect/clickhouse_connect/driver/client.py
index e5da4ec5736..46ce2b4b76a 100644
--- a/contrib/python/clickhouse-connect/clickhouse_connect/driver/client.py
+++ b/contrib/python/clickhouse-connect/clickhouse_connect/driver/client.py
@@ -3,12 +3,13 @@ from __future__ import annotations
import io
import logging
from abc import ABC, abstractmethod
-from collections.abc import Generator, Iterable, Sequence
+from collections.abc import Generator, Sequence
from datetime import timezone, tzinfo
from typing import (
TYPE_CHECKING,
Any,
BinaryIO,
+ cast,
)
from zoneinfo import ZoneInfoNotFoundError
@@ -55,6 +56,7 @@ from clickhouse_connect.driver.query import (
to_arrow_batches,
)
from clickhouse_connect.driver.summary import QuerySummary
+from clickhouse_connect.driver.types import Closable
if TYPE_CHECKING:
import numpy
@@ -62,7 +64,7 @@ if TYPE_CHECKING:
import polars
import pyarrow
-io.DEFAULT_BUFFER_SIZE = 1024 * 256
+io.DEFAULT_BUFFER_SIZE = 1024 * 256 # type: ignore[misc] # override module default buffer size
logger = logging.getLogger(__name__)
arrow_str_setting = "output_format_arrow_string_as_string"
@@ -117,8 +119,8 @@ class Client(ABC):
compression: str | None = None
write_compression: str | None = None
protocol_version = 0
- valid_transport_settings = set()
- optional_transport_settings = set()
+ valid_transport_settings: set[str] = set()
+ optional_transport_settings: set[str] = set()
database = None
max_error_message = 0
_tz_source: TzSource = "auto"
@@ -169,7 +171,7 @@ class Client(ABC):
"""
self.query_limit = coerce_int(query_limit)
self.query_retries = coerce_int(query_retries)
- if database and not database == "__default__":
+ if database and database != "__default__":
self.database = database
if show_clickhouse_errors is not None:
self.show_clickhouse_errors = coerce_bool(show_clickhouse_errors)
@@ -184,9 +186,9 @@ class Client(ABC):
self._tz_source = resolved_tz_source
# Initialize attributes that will be set during connection
- self.server_version = None
- self.server_tz = timezone.utc
- self.server_settings = {}
+ self.server_version: str | None = None
+ self.server_tz: tzinfo = timezone.utc
+ self.server_settings: dict[str, SettingDef] = {}
if autoconnect:
self._init_common_settings(resolved_tz_source)
@@ -196,7 +198,10 @@ class Client(ABC):
def _init_common_settings(self, tz_source: TzSource):
self.server_tz, self._dst_safe = timezone.utc, True
- self.server_version, server_tz = tuple(self.command("SELECT version(), timezone()", use_database=False))
+ version_result = self.command("SELECT version(), timezone()", use_database=False)
+ if not isinstance(version_result, Sequence) or isinstance(version_result, str):
+ raise OperationalError(f"Unexpected response to server version query: {version_result!r}")
+ self.server_version, server_tz = version_result[0], version_result[1]
try:
server_tz_info = tzutil.resolve_zone(server_tz)
server_tz_info, self._dst_safe = tzutil.normalize_timezone(server_tz_info, trust_fixed_offset=True)
@@ -247,8 +252,10 @@ class Client(ABC):
:param settings: Dictionary of setting name and values
:return: A filtered dictionary of settings with values rendered as strings
"""
- validated = {}
+ validated: dict[str, str] = {}
invalid_action = common.get_setting("invalid_setting_action")
+ if not settings:
+ return validated
for key, value in settings.items():
str_value = self._validate_setting(key, value, invalid_action)
if str_value is not None:
@@ -349,7 +356,7 @@ class Client(ABC):
column_oriented: bool | None = None,
use_numpy: bool | None = None,
max_str_len: int | None = None,
- context: QueryContext = None,
+ context: QueryContext | None = None,
query_tz: str | tzinfo | None = None,
column_tzs: dict[str, str | tzinfo] | None = None,
external_data: ExternalData | None = None,
@@ -363,14 +370,17 @@ class Client(ABC):
"""
if query and query.lower().strip().startswith("select __connect_version__"):
return QueryResult(
- [[f"ClickHouse Connect v.{version()} â“’ ClickHouse Inc."]], None, ("connect_version",), (get_from_name("String"),)
+ [[f"ClickHouse Connect v.{version()} â“’ ClickHouse Inc."]],
+ None,
+ ("connect_version",),
+ (get_from_name("String"),),
)
kwargs = locals().copy()
del kwargs["self"]
query_context = self.create_query_context(**kwargs)
if query_context.is_command:
response = self.command(
- query,
+ cast(str, query),
parameters=query_context.parameters,
settings=query_context.settings,
external_data=query_context.external_data,
@@ -390,7 +400,7 @@ class Client(ABC):
column_formats: dict[str, str | dict[str, str]] | None = None,
encoding: str | None = None,
use_none: bool | None = None,
- context: QueryContext = None,
+ context: QueryContext | None = None,
query_tz: str | tzinfo | None = None,
column_tzs: dict[str, str | tzinfo] | None = None,
external_data: ExternalData | None = None,
@@ -413,7 +423,7 @@ class Client(ABC):
column_formats: dict[str, str | dict[str, str]] | None = None,
encoding: str | None = None,
use_none: bool | None = None,
- context: QueryContext = None,
+ context: QueryContext | None = None,
query_tz: str | tzinfo | None = None,
column_tzs: dict[str, str | tzinfo] | None = None,
external_data: ExternalData | None = None,
@@ -436,7 +446,7 @@ class Client(ABC):
column_formats: dict[str, str | dict[str, str]] | None = None,
encoding: str | None = None,
use_none: bool | None = None,
- context: QueryContext = None,
+ context: QueryContext | None = None,
query_tz: str | tzinfo | None = None,
column_tzs: dict[str, str | tzinfo] | None = None,
external_data: ExternalData | None = None,
@@ -456,7 +466,7 @@ class Client(ABC):
query: str,
parameters: Sequence | dict[str, Any] | None = None,
settings: dict[str, Any] | None = None,
- fmt: str = None,
+ fmt: str | None = None,
use_database: bool = True,
external_data: ExternalData | None = None,
transport_settings: dict[str, str] | None = None,
@@ -480,7 +490,7 @@ class Client(ABC):
query: str,
parameters: Sequence | dict[str, Any] | None = None,
settings: dict[str, Any] | None = None,
- fmt: str = None,
+ fmt: str | None = None,
use_database: bool = True,
external_data: ExternalData | None = None,
transport_settings: dict[str, str] | None = None,
@@ -508,7 +518,7 @@ class Client(ABC):
encoding: str | None = None,
use_none: bool | None = None,
max_str_len: int | None = None,
- context: QueryContext = None,
+ context: QueryContext | None = None,
external_data: ExternalData | None = None,
transport_settings: dict[str, str] | None = None,
) -> numpy.ndarray:
@@ -531,7 +541,7 @@ class Client(ABC):
encoding: str | None = None,
use_none: bool | None = None,
max_str_len: int | None = None,
- context: QueryContext = None,
+ context: QueryContext | None = None,
external_data: ExternalData | None = None,
transport_settings: dict[str, str] | None = None,
) -> StreamContext:
@@ -557,7 +567,7 @@ class Client(ABC):
use_na_values: bool | None = None,
query_tz: str | None = None,
column_tzs: dict[str, str | tzinfo] | None = None,
- context: QueryContext = None,
+ context: QueryContext | None = None,
external_data: ExternalData | None = None,
use_extended_dtypes: bool | None = None,
transport_settings: dict[str, str] | None = None,
@@ -585,7 +595,7 @@ class Client(ABC):
use_na_values: bool | None = None,
query_tz: str | None = None,
column_tzs: dict[str, str | tzinfo] | None = None,
- context: QueryContext = None,
+ context: QueryContext | None = None,
external_data: ExternalData | None = None,
use_extended_dtypes: bool | None = None,
transport_settings: dict[str, str] | None = None,
@@ -688,7 +698,7 @@ class Client(ABC):
if as_pandas and use_extended_dtypes is None:
use_extended_dtypes = True
return QueryContext(
- query=query,
+ query=cast(str | bytes, query),
parameters=parameters,
settings=settings,
query_formats=query_formats,
@@ -766,13 +776,16 @@ class Client(ABC):
self._add_integration_tag("arrow")
settings = self._update_arrow_settings(settings, use_strings)
return to_arrow_batches(
- self.raw_stream(
- query,
- parameters,
- settings,
- fmt="ArrowStream",
- external_data=external_data,
- transport_settings=transport_settings,
+ cast(
+ io.IOBase,
+ self.raw_stream(
+ query,
+ parameters,
+ settings,
+ fmt="ArrowStream",
+ external_data=external_data,
+ transport_settings=transport_settings,
+ ),
)
)
@@ -814,7 +827,7 @@ class Client(ABC):
check_polars()
self._add_integration_tag("polars")
- def converter(table: pyarrow.Table) -> polars.DataFrame:
+ def converter(table: pyarrow.Table) -> polars.DataFrame: # type: ignore[misc]
table = _apply_arrow_tz_policy(table, self.tz_mode)
return options.pl.from_arrow(table)
@@ -867,7 +880,7 @@ class Client(ABC):
check_polars()
self._add_integration_tag("polars")
- def converter(table: pyarrow.Table) -> polars.DataFrame:
+ def converter(table: pyarrow.Table) -> polars.DataFrame: # type: ignore[misc]
table = _apply_arrow_tz_policy(table, self.tz_mode)
return options.pl.from_arrow(table)
else:
@@ -882,7 +895,7 @@ class Client(ABC):
for batch in reader:
yield converter(batch)
- return StreamContext(raw_stream, df_generator())
+ return StreamContext(cast(Closable, raw_stream), df_generator())
def _update_arrow_settings(self, settings: dict[str, Any] | None, use_strings: bool | None) -> dict[str, Any]:
settings = dict_copy(settings)
@@ -903,8 +916,8 @@ class Client(ABC):
self,
cmd: str,
parameters: Sequence | dict[str, Any] | None = None,
- data: str | bytes = None,
- settings: dict[str, Any] = None,
+ data: str | bytes | None = None,
+ settings: dict[str, Any] | None = None,
use_database: bool = True,
external_data: ExternalData | None = None,
transport_settings: dict[str, str] | None = None,
@@ -934,14 +947,14 @@ class Client(ABC):
def insert(
self,
table: str | None = None,
- data: Sequence[Sequence[Any]] = None,
- column_names: str | Iterable[str] = "*",
+ data: Sequence[Sequence[Any]] | None = None,
+ column_names: str | Sequence[str] | None = "*",
database: str | None = None,
- column_types: Sequence[ClickHouseType] = None,
- column_type_names: Sequence[str] = None,
+ column_types: Sequence[ClickHouseType] | None = None,
+ column_type_names: Sequence[str] | None = None,
column_oriented: bool = False,
settings: dict[str, Any] | None = None,
- context: InsertContext = None,
+ context: InsertContext | None = None,
transport_settings: dict[str, str] | None = None,
) -> QuerySummary:
"""
@@ -966,6 +979,8 @@ class Client(ABC):
if (context is None or context.empty) and data is None:
raise ProgrammingError("No data specified for insert") from None
if context is None:
+ if table is None:
+ raise ProgrammingError("No table specified for insert") from None
context = self.create_insert_context(
table,
column_names,
@@ -984,14 +999,14 @@ class Client(ABC):
def insert_df(
self,
- table: str = None,
+ table: str | None = None,
df=None,
database: str | None = None,
settings: dict | None = None,
column_names: Sequence[str] | None = None,
- column_types: Sequence[ClickHouseType] = None,
- column_type_names: Sequence[str] = None,
- context: InsertContext = None,
+ column_types: Sequence[ClickHouseType] | None = None,
+ column_type_names: Sequence[str] | None = None,
+ context: InsertContext | None = None,
transport_settings: dict[str, str] | None = None,
) -> QuerySummary:
"""
@@ -1034,7 +1049,7 @@ class Client(ABC):
self,
table: str,
arrow_table,
- database: str = None,
+ database: str | None = None,
settings: dict | None = None,
transport_settings: dict[str, str] | None = None,
) -> QuerySummary:
@@ -1051,7 +1066,7 @@ class Client(ABC):
full_table = table if "." in table or not database else f"{database}.{table}"
compression = self.write_compression if self.write_compression in ("zstd", "lz4") else None
column_names, insert_block = arrow_buffer(arrow_table, compression)
- return self.raw_insert(full_table, column_names, insert_block, settings, "Arrow", transport_settings)
+ return self.raw_insert(full_table, column_names, insert_block, settings, "Arrow", transport_settings=transport_settings)
def insert_df_arrow(
self,
@@ -1117,8 +1132,8 @@ class Client(ABC):
table: str,
column_names: str | Sequence[str] | None = None,
database: str | None = None,
- column_types: Sequence[ClickHouseType] = None,
- column_type_names: Sequence[str] = None,
+ column_types: Sequence[ClickHouseType] | None = None,
+ column_type_names: Sequence[str] | None = None,
column_oriented: bool = False,
settings: dict[str, Any] | None = None,
data: Sequence[Sequence[Any]] | None = None,
@@ -1146,7 +1161,7 @@ class Client(ABC):
full_table = f"{quote_identifier(database)}.{quote_identifier(table)}"
else:
full_table = quote_identifier(table)
- column_defs = []
+ column_defs: list[ColumnDef] = []
if column_types is None and column_type_names is None:
describe_result = self.query(f"DESCRIBE TABLE {full_table}", settings=settings)
column_defs = [
@@ -1189,7 +1204,7 @@ class Client(ABC):
:return: True if version_str is greater than the server_version, False if less than
"""
try:
- server_parts = [int(x) for x in self.server_version.split(".") if x.isnumeric()]
+ server_parts = [int(x) for x in (self.server_version or "").split(".") if x.isnumeric()]
server_parts.extend([0] * (4 - len(server_parts)))
version_parts = [int(x) for x in version_str.split(".")]
version_parts.extend([0] * (4 - len(version_parts)))
@@ -1222,7 +1237,7 @@ class Client(ABC):
self,
table: str,
column_names: Sequence[str] | None = None,
- insert_block: str | bytes | Generator[bytes, None, None] | BinaryIO = None,
+ insert_block: str | bytes | Generator[bytes, None, None] | BinaryIO | None = None,
settings: dict | None = None,
fmt: str | None = None,
compression: str | None = None,
diff --git a/contrib/python/clickhouse-connect/clickhouse_connect/driver/common.py b/contrib/python/clickhouse-connect/clickhouse_connect/driver/common.py
index a4acbd292df..fc609bfd84b 100644
--- a/contrib/python/clickhouse-connect/clickhouse_connect/driver/common.py
+++ b/contrib/python/clickhouse-connect/clickhouse_connect/driver/common.py
@@ -3,6 +3,7 @@ import asyncio
import struct
import sys
from collections.abc import Callable, Generator, MutableSequence, Sequence
+from io import IOBase
from typing import Any
from clickhouse_connect.driver.exceptions import DataError, ProgrammingError, StreamClosedError
@@ -107,7 +108,7 @@ def unescape_identifier(x: str) -> str:
return x
-def dict_copy(source: dict = None, update: dict | None = None) -> dict:
+def dict_copy(source: dict | None = None, update: dict | None = None) -> dict:
copy = source.copy() if source else {}
if update:
copy.update(update)
@@ -153,10 +154,13 @@ class SliceView(Sequence):
slots = ("_source", "_range")
+ _source: Sequence
+ _range: range
+
def __init__(self, source: Sequence, source_slice: slice | None = None):
if isinstance(source, SliceView):
self._source = source._source
- self._range = source._range[source_slice]
+ self._range = source._range if source_slice is None else source._range[source_slice]
else:
self._source = source
if source_slice is None:
@@ -200,7 +204,7 @@ class StreamContext:
__slots__ = "source", "gen", "_in_context"
- def __init__(self, source: Closable, gen: Generator):
+ def __init__(self, source: Closable | IOBase, gen: Generator):
self.source = source
self.gen = gen
self._in_context = False
diff --git a/contrib/python/clickhouse-connect/clickhouse_connect/driver/compression.py b/contrib/python/clickhouse-connect/clickhouse_connect/driver/compression.py
index 802b6e815c2..b6533a95711 100644
--- a/contrib/python/clickhouse-connect/clickhouse_connect/driver/compression.py
+++ b/contrib/python/clickhouse-connect/clickhouse_connect/driver/compression.py
@@ -1,5 +1,4 @@
import zlib
-from abc import abstractmethod
import lz4
import lz4.frame
@@ -17,14 +16,13 @@ if brotli:
available_compression.append("br")
available_compression.extend(["gzip", "deflate"])
-comp_map = {}
+comp_map: dict[str, "Compressor | type[Compressor]"] = {}
class Compressor:
def __init_subclass__(cls, tag: str, thread_safe: bool = True):
comp_map[tag] = cls() if thread_safe else cls
- @abstractmethod
def compress_block(self, block) -> bytes | bytearray:
return block
@@ -66,11 +64,10 @@ class BrotliCompressor(Compressor, tag="br"):
null_compressor = Compressor()
-def get_compressor(compression: str) -> Compressor:
+def get_compressor(compression: str | None) -> Compressor:
if not compression:
return null_compressor
comp = comp_map[compression]
- try:
- return comp()
- except TypeError:
+ if isinstance(comp, Compressor):
return comp
+ return comp()
diff --git a/contrib/python/clickhouse-connect/clickhouse_connect/driver/context.py b/contrib/python/clickhouse-connect/clickhouse_connect/driver/context.py
index 50b8315a0db..eec0ed517ed 100644
--- a/contrib/python/clickhouse-connect/clickhouse_connect/driver/context.py
+++ b/contrib/python/clickhouse-connect/clickhouse_connect/driver/context.py
@@ -5,7 +5,7 @@ from typing import Any
logger = logging.getLogger(__name__)
-_empty_map = {}
+_empty_map: dict[Any, Any] = {}
class BaseQueryContext:
@@ -38,7 +38,7 @@ class BaseQueryContext:
self.query_formats = query_formats or {}
self.column_formats = column_formats or {}
self.transport_settings = transport_settings
- self.column_name = None
+ self.column_name: str | None = None
self.encoding = encoding
self.use_numpy = use_numpy
self.use_extended_dtypes = use_extended_dtypes
diff --git a/contrib/python/clickhouse-connect/clickhouse_connect/driver/dataconv.py b/contrib/python/clickhouse-connect/clickhouse_connect/driver/dataconv.py
index 5a6d1786f8a..9de76e3fbdd 100644
--- a/contrib/python/clickhouse-connect/clickhouse_connect/driver/dataconv.py
+++ b/contrib/python/clickhouse-connect/clickhouse_connect/driver/dataconv.py
@@ -17,11 +17,14 @@ MONTH_DAYS_LEAP = (0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366)
def read_ipv4_col(source: ByteSource, num_rows: int):
column = source.read_array("I", num_rows)
fast_ip_v4 = IPv4Address.__new__
- new_col = []
+ new_col: list[IPv4Address] = []
app = new_col.append
for x in column:
ipv4 = fast_ip_v4(IPv4Address)
- ipv4._ip = x
+ # _ip is CPython's private backing int for the address.
+ # It's directly set to bypass IPv4Address.__init__
+ # for speed when bulk-decoding a column
+ ipv4._ip = x # type: ignore[attr-defined]
app(ipv4)
return new_col
@@ -103,7 +106,7 @@ def read_uuid_col(source: ByteSource, num_rows: int):
new_uuid = UUID.__new__
unsafe = SafeUUID.unsafe
oset = object.__setattr__
- column = []
+ column: list[UUID] = []
app = column.append
for i in range(num_rows):
ix = i << 1
@@ -173,7 +176,7 @@ def write_str_col(column: Sequence, nullable: bool, encoding: str | None, dest:
return 0
-def write_native_col(code: str, column: Sequence, dest: bytearray, col_name: object = None) -> int:
+def write_native_col(code: str, column: Sequence, dest: bytearray, col_name: str | None = None) -> int:
"""
Pure Python fallback for write_native_col.
Delegates to write_array which uses struct.pack.
diff --git a/contrib/python/clickhouse-connect/clickhouse_connect/driver/ddl.py b/contrib/python/clickhouse-connect/clickhouse_connect/driver/ddl.py
index d6137d124b4..b682fbfa4e6 100644
--- a/contrib/python/clickhouse-connect/clickhouse_connect/driver/ddl.py
+++ b/contrib/python/clickhouse-connect/clickhouse_connect/driver/ddl.py
@@ -1,24 +1,30 @@
from __future__ import annotations
from collections.abc import Sequence
-from typing import TYPE_CHECKING, NamedTuple
+from typing import TYPE_CHECKING, NamedTuple, Protocol
-from clickhouse_connect.datatypes.base import ClickHouseType
from clickhouse_connect.driver.options import check_arrow
if TYPE_CHECKING:
import pyarrow
+class _NamedType(Protocol):
+ """Structural type for anything exposing a ClickHouse type ``name``."""
+
+ @property
+ def name(self) -> str: ...
+
+
class TableColumnDef(NamedTuple):
"""
Simplified ClickHouse Table Column definition for DDL
"""
name: str
- ch_type: ClickHouseType
- expr_type: str = None
- expr: str = None
+ ch_type: _NamedType
+ expr_type: str | None = None
+ expr: str | None = None
@property
def col_expr(self):
diff --git a/contrib/python/clickhouse-connect/clickhouse_connect/driver/exceptions.py b/contrib/python/clickhouse-connect/clickhouse_connect/driver/exceptions.py
index cddcac55f59..f00ff9aa712 100644
--- a/contrib/python/clickhouse-connect/clickhouse_connect/driver/exceptions.py
+++ b/contrib/python/clickhouse-connect/clickhouse_connect/driver/exceptions.py
@@ -29,7 +29,7 @@ class ClickHouseError(Exception):
"""Exception related to operation with ClickHouse."""
-class Warning(Warning, ClickHouseError): # noqa: N818
+class Warning(Warning, ClickHouseError): # type: ignore[misc] # noqa: N818
"""Exception raised for important warnings like data truncations
while inserting, etc."""
diff --git a/contrib/python/clickhouse-connect/clickhouse_connect/driver/httpclient.py b/contrib/python/clickhouse-connect/clickhouse_connect/driver/httpclient.py
index 170c2ab6ab8..9d1aa98b0da 100644
--- a/contrib/python/clickhouse-connect/clickhouse_connect/driver/httpclient.py
+++ b/contrib/python/clickhouse-connect/clickhouse_connect/driver/httpclient.py
@@ -8,7 +8,7 @@ from base64 import b64encode
from collections.abc import Callable, Generator, Sequence
from importlib import import_module
from importlib.metadata import version as dist_version
-from typing import Any, BinaryIO
+from typing import Any, BinaryIO, cast
from urllib.parse import urlencode
from urllib3 import Timeout
@@ -43,7 +43,7 @@ from clickhouse_connect.driver.httputil import (
get_response_data,
)
from clickhouse_connect.driver.insert import InsertContext
-from clickhouse_connect.driver.query import QueryContext, QueryResult, TzSource, returns_empty_string_on_empty_body
+from clickhouse_connect.driver.query import QueryContext, QueryResult, TzMode, TzSource, returns_empty_string_on_empty_body
from clickhouse_connect.driver.summary import QuerySummary
from clickhouse_connect.driver.transform import NativeTransform
@@ -57,7 +57,7 @@ _REMOTE_CLOSE_ERRORS = (ConnectionResetError, BrokenPipeError)
class HttpClient(Client):
- params = {}
+ params: dict[str, str] = {}
valid_transport_settings = {
"database",
"buffer_size",
@@ -84,7 +84,7 @@ class HttpClient(Client):
port: int,
username: str,
password: str,
- database: str,
+ database: str | None,
access_token: str | None = None,
token_provider: Callable[[], str] | None = None,
compress: bool | str = True,
@@ -141,7 +141,7 @@ class HttpClient(Client):
self.headers["X-ClickHouse-SSL-Certificate-Auth"] = "on"
if not self.http and (server_host_name or ca_cert or client_cert or not verify or https_proxy):
- options = {"verify": verify}
+ options: dict[str, Any] = {"verify": verify}
dict_add(options, "ca_cert", ca_cert)
dict_add(options, "client_cert", client_cert)
dict_add(options, "client_cert_key", client_cert_key)
@@ -167,7 +167,7 @@ class HttpClient(Client):
elif (not client_cert or tls_mode in ("strict", "proxy")) and username:
self.headers["Authorization"] = "Basic " + b64encode(f"{username}:{password}".encode()).decode()
- self._reported_libs = set()
+ self._reported_libs: set[str] = set()
self.headers["User-Agent"] = common.build_client_name(client_name)
if headers:
self.headers.update(headers)
@@ -184,7 +184,7 @@ class HttpClient(Client):
self._send_progress = None
self._send_comp_setting = False
self._progress_interval = None
- self._active_session = None
+ self._active_session: str | None = None
self._rename_response_column = rename_response_column
# allow to override the global autogenerate_session_id setting via the constructor params
@@ -220,7 +220,7 @@ class HttpClient(Client):
query_retries=query_retries,
server_host_name=server_host_name,
tz_source=tz_source,
- tz_mode=tz_mode,
+ tz_mode=cast(TzMode | None, tz_mode),
show_clickhouse_errors=show_clickhouse_errors,
autoconnect=True,
)
@@ -265,16 +265,17 @@ class HttpClient(Client):
return final_query + fmt
def _query_with_context(self, context: QueryContext) -> QueryResult:
- headers = {}
- params = {}
+ headers: dict[str, Any] = {}
+ params: dict[str, str] = {}
if self.database:
params["database"] = self.database
if self.protocol_version:
- params["client_protocol_version"] = self.protocol_version
+ params["client_protocol_version"] = str(self.protocol_version)
context.block_info = True
params.update(self._validate_settings(context.settings))
context.rename_response_column = self._rename_response_column
use_form = use_form_encoding(context.final_query, context.bind_params, self.form_encode_query_params)
+ fields: dict[str, Any] | None = None
if not context.is_insert and columns_only_re.search(context.uncommented_query):
# Mirror normal query behavior for form encoding and external data
fmt_json_query = f"{context.final_query}\n FORMAT JSON"
@@ -309,7 +310,7 @@ class HttpClient(Client):
logger.debug("Failed to rename col '%s'. Skipping rename. Error: %s", name, e)
names.append(name)
types.append(registry.get_from_name(col["type"]))
- return QueryResult([], None, tuple(names), tuple(types))
+ return QueryResult([], None, tuple(names), tuple(types)) # type: ignore[arg-type]
if self.compression:
headers["Accept-Encoding"] = self.compression
@@ -347,10 +348,12 @@ class HttpClient(Client):
)
exception_tag = response.headers.get(ex_tag_header)
byte_source = RespBuffCls(ResponseSource(response, exception_tag=exception_tag))
- context.set_response_tz(self._check_tz_change(response.headers.get("X-ClickHouse-Timezone")))
+ response_tz = self._check_tz_change(response.headers.get("X-ClickHouse-Timezone"))
+ if response_tz is not None:
+ context.set_response_tz(response_tz)
query_result = self._transform.parse_response(byte_source, context)
query_result.summary = self._summary(response)
- return query_result
+ return cast(QueryResult, query_result)
def data_insert(self, context: InsertContext) -> QuerySummary:
"""
@@ -371,7 +374,7 @@ class HttpClient(Client):
headers = {"Content-Type": "application/octet-stream"}
if context.compression is None:
context.compression = self.write_compression
- if context.compression:
+ if isinstance(context.compression, str):
headers["Content-Encoding"] = context.compression
block_gen = self._transform.build_insert(context)
@@ -401,9 +404,9 @@ class HttpClient(Client):
def raw_insert(
self,
- table: str = None,
+ table: str | None = None,
column_names: Sequence[str] | None = None,
- insert_block: str | bytes | Generator[bytes, None, None] | BinaryIO = None,
+ insert_block: str | bytes | Generator[bytes, None, None] | BinaryIO | None = None,
settings: dict | None = None,
fmt: str | None = None,
compression: str | None = None,
@@ -421,9 +424,11 @@ class HttpClient(Client):
query = f"INSERT INTO {table}{cols} FORMAT {fmt if fmt else self._write_format}"
if not compression and isinstance(insert_block, str):
insert_block = query + "\n" + insert_block
- elif not compression and isinstance(insert_block, (bytes, bytearray, BinaryIO)):
+ elif not compression and isinstance(insert_block, (bytes, bytearray)):
insert_block = (query + "\n").encode() + insert_block
else:
+ # Generators, file-like objects, and compressed data: send the
+ # INSERT query as a URL param and stream the body as-is.
params["query"] = query
if self.database:
params["database"] = self.database
@@ -448,7 +453,7 @@ class HttpClient(Client):
self,
cmd: str,
parameters: Sequence | dict[str, Any] | None = None,
- data: str | bytes = None,
+ data: str | bytes | None = None,
settings: dict | None = None,
use_database: bool = True,
external_data: ExternalData | None = None,
@@ -457,9 +462,9 @@ class HttpClient(Client):
"""
See BaseClient doc_string for this method
"""
- cmd, params = bind_query(cmd, parameters, self.server_tz)
- headers = {}
- payload = None
+ bound_cmd, params = bind_query(cmd, parameters, self.server_tz)
+ headers: dict[str, Any] = {}
+ payload: str | bytes | None = None
fields = None
if external_data:
if data:
@@ -472,12 +477,14 @@ class HttpClient(Client):
elif isinstance(data, bytes):
headers["Content-Type"] = "application/octet-stream"
payload = data
- if payload is None and not cmd:
+ if payload is None and not bound_cmd:
raise ProgrammingError("Command sent without query or recognized data") from None
if payload or fields:
- params["query"] = cmd
+ if isinstance(bound_cmd, bytes):
+ raise ProgrammingError("Binary parameter bind cannot be combined with command data or external data") from None
+ params["query"] = bound_cmd
else:
- payload = cmd
+ payload = bound_cmd
if use_database and self.database:
params["database"] = self.database
params.update(self._validate_settings(settings or {}))
@@ -495,7 +502,7 @@ class HttpClient(Client):
return result
except UnicodeDecodeError:
return str(response.data)
- if returns_empty_string_on_empty_body(cmd):
+ if returns_empty_string_on_empty_body(bound_cmd):
return ""
return QuerySummary(self._summary(response))
@@ -546,7 +553,7 @@ class HttpClient(Client):
stream: bool = False,
server_wait: bool = True,
fields: dict[str, tuple] | None = None,
- error_handler: Callable = None,
+ error_handler: Callable | None = None,
retry_body: Callable[[], Any] | None = None,
) -> HTTPResponse:
if isinstance(data, str):
@@ -571,7 +578,7 @@ class HttpClient(Client):
final_params["query_id"] = str(uuid.uuid4())
url = f"{self.url}?{urlencode(final_params)}"
- kwargs = {"headers": headers, "timeout": self.timeout, "retries": self.http_retries, "preload_content": not stream}
+ kwargs: dict[str, Any] = {"headers": headers, "timeout": self.timeout, "retries": self.http_retries, "preload_content": not stream}
if self.server_host_name:
kwargs["assert_same_host"] = False
kwargs["headers"].update({"Host": self.server_host_name})
@@ -579,7 +586,7 @@ class HttpClient(Client):
kwargs["fields"] = fields
else:
kwargs["body"] = data
- check_conn_expiration(self.http)
+ check_conn_expiration(cast(PoolManager, self.http))
query_session = final_params.get("session_id")
while True:
attempts += 1
@@ -593,7 +600,7 @@ class HttpClient(Client):
# throw an error instead, but in most cases this more helpful error will be thrown first
self._active_session = query_session
try:
- response = self.http.request(method, url, **kwargs)
+ response: HTTPResponse = cast(HTTPResponse, cast(PoolManager, self.http).request(method, url, **kwargs))
except HTTPError as ex:
# Always allow at least one retry on a clean connection error so a single stale
# keep-alive socket doesn't surface to the caller, and additionally honor the
@@ -639,7 +646,7 @@ class HttpClient(Client):
kwargs["body"] = retry_body()
response.close()
logger.debug("Refreshing access token after authentication failure")
- elif error_handler:
+ elif error_handler is not None:
error_handler(response)
else:
self._error_handler(response)
@@ -649,7 +656,7 @@ class HttpClient(Client):
query: str,
parameters: Sequence | dict[str, Any] | None = None,
settings: dict[str, Any] | None = None,
- fmt: str = None,
+ fmt: str | None = None,
use_database: bool = True,
external_data: ExternalData | None = None,
transport_settings: dict[str, str] | None = None,
@@ -665,7 +672,7 @@ class HttpClient(Client):
query: str,
parameters: Sequence | dict[str, Any] | None = None,
settings: dict[str, Any] | None = None,
- fmt: str = None,
+ fmt: str | None = None,
use_database: bool = True,
external_data: ExternalData | None = None,
transport_settings: dict[str, str] | None = None,
@@ -689,7 +696,7 @@ class HttpClient(Client):
query: str,
parameters: Sequence | dict[str, Any] | None,
settings: dict[str, Any] | None,
- fmt: str,
+ fmt: str | None,
use_database: bool,
external_data: ExternalData | None,
):
@@ -699,22 +706,24 @@ class HttpClient(Client):
params = self._validate_settings(settings or {})
if use_database and self.database:
params["database"] = self.database
- fields = {}
+ form_fields: dict[str, Any] = {}
+ fields: dict[str, Any] | None = form_fields
use_form = use_form_encoding(final_query, bind_params, self.form_encode_query_params)
# Setup query body
if external_data and not use_form and isinstance(final_query, bytes):
raise ProgrammingError("Binary query cannot be placed in URL when using External Data; enable form encoding.")
# Setup additional query parameters and body
+ body: str | bytes = b""
if use_form:
- body = b""
- fields["query"] = final_query
- fields.update(bind_params)
+ form_fields["query"] = final_query
+ form_fields.update(bind_params)
if external_data:
params.update(external_data.query_params)
- fields.update(external_data.form_data)
+ form_fields.update(external_data.form_data)
elif external_data:
params.update(bind_params)
- body = b""
+ # Guaranteed str: the check above raises if external_data and not use_form and bytes
+ assert isinstance(final_query, str)
params["query"] = final_query
params.update(external_data.query_params)
fields = external_data.form_data
@@ -774,20 +783,20 @@ class HttpClient(Client):
"""
try:
headers = dict_copy(self.headers)
- kwargs = {"headers": headers, "timeout": 3, "preload_content": True}
+ kwargs: dict[str, Any] = {"headers": headers, "timeout": 3, "preload_content": True}
if self.server_host_name:
kwargs["assert_same_host"] = False
headers["Host"] = self.server_host_name
- response = self.http.request("GET", f"{self.url}/ping", **kwargs)
+ response = cast(PoolManager, self.http).request("GET", f"{self.url}/ping", **kwargs)
return 200 <= response.status < 300
except HTTPError:
logger.debug("ping failed", exc_info=True)
return False
def close_connections(self) -> None:
- self.http.clear()
+ cast(PoolManager, self.http).clear()
def close(self) -> None:
if self._owns_pool_manager:
- self.http.clear()
- all_managers.pop(self.http, None)
+ cast(PoolManager, self.http).clear()
+ all_managers.pop(cast(PoolManager, self.http), None)
diff --git a/contrib/python/clickhouse-connect/clickhouse_connect/driver/httputil.py b/contrib/python/clickhouse-connect/clickhouse_connect/driver/httputil.py
index d135e94194d..0f7a6cfa091 100644
--- a/contrib/python/clickhouse-connect/clickhouse_connect/driver/httputil.py
+++ b/contrib/python/clickhouse-connect/clickhouse_connect/driver/httputil.py
@@ -26,7 +26,7 @@ logger = logging.getLogger(__name__)
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
# Increase this number just to be safe when ClickHouse is returning progress headers
-http.client._MAXHEADERS = 10000
+http.client._MAXHEADERS = 10000 # type: ignore[attr-defined]
DEFAULT_KEEP_INTERVAL = 30
DEFAULT_KEEP_COUNT = 3
@@ -42,8 +42,8 @@ core_socket_options = [
]
logging.getLogger("urllib3").setLevel(logging.WARNING)
-_proxy_managers = {}
-all_managers = {}
+_proxy_managers: dict[str, PoolManager] = {}
+all_managers: dict[PoolManager, int] = {}
@atexit.register
@@ -74,7 +74,7 @@ def get_pool_manager_options(
if getattr(socket, "TCP_KEEPCNT", None) is not None:
socket_options.append((SOCKET_TCP, socket.TCP_KEEPCNT, keep_count))
if getattr(socket, "TCP_KEEPIDLE", None) is not None:
- socket_options.append((SOCKET_TCP, socket.TCP_KEEPIDLE, keep_idle))
+ socket_options.append((SOCKET_TCP, socket.TCP_KEEPIDLE, keep_idle)) # type: ignore[attr-defined]
if sys.platform == "darwin":
socket_options.append((SOCKET_TCP, getattr(socket, "TCP_KEEPALIVE", 0x10), keep_interval))
options["maxsize"] = options.get("maxsize", 8)
@@ -119,7 +119,7 @@ def get_pool_manager(
raise ProgrammingError("Only one of http_proxy or https_proxy should be specified")
if not http_proxy.startswith("http"):
http_proxy = f"http://{http_proxy}"
- manager = ProxyManager(http_proxy, **options)
+ manager: PoolManager = ProxyManager(http_proxy, **options)
elif https_proxy:
if not https_proxy.startswith("http"):
https_proxy = f"https://{https_proxy}"
@@ -238,7 +238,6 @@ class ResponseSource:
done = False
current_size = 0
read_gen = response.stream(chunk_size, decompress is None)
- data_received = False
read_error = None
while True:
while not done:
@@ -246,7 +245,7 @@ class ResponseSource:
try:
chunk = next(read_gen, None) # Always try to read at least one chunk if there are any left
except Exception as ex:
- # Store the exception for potential re-raising if no data was received
+ # Store the exception for re-raising later
read_error = ex
logger.warning("unexpected failure to read next chunk", exc_info=True)
if not chunk:
@@ -257,7 +256,7 @@ class ResponseSource:
if current_size > buffer_size:
break
if len(chunks) == 0:
- if read_error and not data_received:
+ if read_error:
raise OperationalError("Failed to read response data from server") from read_error
return
if decompress:
@@ -267,7 +266,6 @@ class ResponseSource:
chunk = chunks.popleft()
current_size -= len(chunk)
if chunk:
- data_received = True
yield chunk
self.gen = buffered()
diff --git a/contrib/python/clickhouse-connect/clickhouse_connect/driver/insert.py b/contrib/python/clickhouse-connect/clickhouse_connect/driver/insert.py
index bcb361afaac..200063fc940 100644
--- a/contrib/python/clickhouse-connect/clickhouse_connect/driver/insert.py
+++ b/contrib/python/clickhouse-connect/clickhouse_connect/driver/insert.py
@@ -97,6 +97,7 @@ class InsertContext(BaseQueryContext):
self.block_row_count = self._calc_block_size()
def _calc_block_size(self) -> int:
+ assert self._data is not None
if self.req_block_size:
return self.req_block_size
row_size = 0
diff --git a/contrib/python/clickhouse-connect/clickhouse_connect/driver/npquery.py b/contrib/python/clickhouse-connect/clickhouse_connect/driver/npquery.py
index 1c68ed71566..01388f1fb8a 100644
--- a/contrib/python/clickhouse-connect/clickhouse_connect/driver/npquery.py
+++ b/contrib/python/clickhouse-connect/clickhouse_connect/driver/npquery.py
@@ -1,6 +1,7 @@
import itertools
import logging
from collections.abc import Generator, Sequence
+from typing import Any
from clickhouse_connect.driver import options
from clickhouse_connect.driver.common import StreamContext, empty_gen
@@ -13,19 +14,19 @@ logger = logging.getLogger(__name__)
class NumpyResult(Closable):
def __init__(
self,
- block_gen: Generator[Sequence, None, None] = None,
+ block_gen: Generator[Sequence, None, None] | None = None,
column_names: tuple = (),
column_types: tuple = (),
d_types: Sequence = (),
- source: Closable = None,
+ source: Closable | None = None,
):
self.column_names = column_names
self.column_types = column_types
self.np_types = d_types
self.source = source
self.query_id = ""
- self.summary = {}
- self._block_gen = block_gen or empty_gen()
+ self.summary: dict[str, Any] = {}
+ self._block_gen: Generator[Sequence, None, None] | None = block_gen or empty_gen()
self._numpy_result = None
self._df_result = None
diff --git a/contrib/python/clickhouse-connect/clickhouse_connect/driver/parser.py b/contrib/python/clickhouse-connect/clickhouse_connect/driver/parser.py
index 99de3f56cc6..3afe4904fcd 100644
--- a/contrib/python/clickhouse-connect/clickhouse_connect/driver/parser.py
+++ b/contrib/python/clickhouse-connect/clickhouse_connect/driver/parser.py
@@ -74,7 +74,7 @@ def parse_callable(expr) -> tuple[str, tuple[str | int, ...], str]:
return name, tuple(values), expr[pos:].strip()
-def parse_enum(expr) -> tuple[tuple[str], tuple[int]]:
+def parse_enum(expr) -> tuple[tuple[str, ...], tuple[int, ...]]:
"""
Parse a ClickHouse enum definition expression of the form ('key1' = 1, 'key2' = 2)
:param expr: ClickHouse enum expression/arguments
@@ -84,8 +84,8 @@ def parse_enum(expr) -> tuple[tuple[str], tuple[int]]:
values = []
pos = expr.find("(") + 1
in_key = False
- key = []
- value = []
+ key: list[str] = []
+ value: list[str] = []
while True:
char = expr[pos]
pos += 1
@@ -110,8 +110,8 @@ def parse_enum(expr) -> tuple[tuple[str], tuple[int]]:
in_key = True
else:
value.append(char)
- values, keys = zip(*sorted(zip(values, keys)))
- return tuple(keys), tuple(values)
+ sorted_values, sorted_keys = zip(*sorted(zip(values, keys)))
+ return tuple(sorted_keys), tuple(sorted_values)
def parse_columns(expr: str):
diff --git a/contrib/python/clickhouse-connect/clickhouse_connect/driver/query.py b/contrib/python/clickhouse-connect/clickhouse_connect/driver/query.py
index 9338e6ff572..f4a94be4cba 100644
--- a/contrib/python/clickhouse-connect/clickhouse_connect/driver/query.py
+++ b/contrib/python/clickhouse-connect/clickhouse_connect/driver/query.py
@@ -52,7 +52,7 @@ class QueryContext(BaseQueryContext):
def __init__(
self,
query: str | bytes = "",
- parameters: dict[str, Any] | None = None,
+ parameters: Sequence | dict[str, Any] | None = None,
settings: dict[str, Any] | None = None,
query_formats: dict[str, str] | None = None,
column_formats: dict[str, str | dict[str, str]] | None = None,
@@ -117,7 +117,7 @@ class QueryContext(BaseQueryContext):
self.parameters = parameters or {}
self.use_none = True if use_none is None else use_none
self.column_oriented = False if column_oriented is None else column_oriented
- self.use_numpy = use_numpy
+ self.use_numpy = use_numpy if use_numpy is not None else False
self.max_str_len = 0 if max_str_len is None else max_str_len
self.server_tz = server_tz
self.apply_server_tz = apply_server_tz
@@ -132,7 +132,7 @@ class QueryContext(BaseQueryContext):
raise ProgrammingError(f"query_tz {query_tz} is not recognized; {tzutil.TZDATA_HINT}") from ex
self.query_tz = query_tz
if column_tzs is not None:
- resolved_column_tzs = {}
+ resolved_column_tzs: dict[str, str | tzinfo] = {}
for col_name, col_tz in column_tzs.items():
if isinstance(col_tz, str):
try:
@@ -143,8 +143,8 @@ class QueryContext(BaseQueryContext):
resolved_column_tzs[col_name] = col_tz
column_tzs = resolved_column_tzs
self.column_tzs = column_tzs
- self.column_tz = None
- self.response_tz = None
+ self.column_tz: str | tzinfo | None = None
+ self.response_tz: tzinfo | None = None
self.block_info = False
self.as_pandas = as_pandas
self.streaming = streaming
@@ -177,12 +177,12 @@ class QueryContext(BaseQueryContext):
def is_command(self) -> bool:
return command_re.search(self.uncommented_query) is not None or bare_row_policy_show_re.search(self.uncommented_query) is not None
- def set_parameters(self, parameters: dict[str, Any]):
+ def set_parameters(self, parameters: Sequence | dict[str, Any]):
self.parameters = parameters
self._update_query()
def set_parameter(self, key: str, value: Any):
- if not self.parameters:
+ if not isinstance(self.parameters, dict):
self.parameters = {}
self.parameters[key] = value
self._update_query()
@@ -219,7 +219,7 @@ class QueryContext(BaseQueryContext):
def updated_copy(
self,
query: str | bytes | None = None,
- parameters: dict[str, Any] | None = None,
+ parameters: Sequence | dict[str, Any] | None = None,
settings: dict[str, Any] | None = None,
query_formats: dict[str, str] | None = None,
column_formats: dict[str, str | dict[str, str]] | None = None,
@@ -245,7 +245,11 @@ class QueryContext(BaseQueryContext):
resolved_tz_mode = tz_mode if tz_mode is not None else self.tz_mode
return QueryContext(
query=query or self.query,
- parameters=dict_copy(self.parameters, parameters),
+ parameters=(
+ dict_copy(self.parameters, parameters if isinstance(parameters, dict) else None)
+ if isinstance(self.parameters, dict)
+ else (parameters if parameters is not None else self.parameters)
+ ),
settings=dict_copy(self.settings, settings),
query_formats=dict_copy(self.query_formats, query_formats),
column_formats=dict_copy(self.column_formats, column_formats),
@@ -283,18 +287,18 @@ class QueryResult(Closable):
def __init__(
self,
- result_set: Matrix = None,
- block_gen: Generator[Matrix, None, None] = None,
+ result_set: Matrix | None = None,
+ block_gen: Generator[Matrix, None, None] | None = None,
column_names: tuple[str, ...] = (),
column_types: tuple["ClickHouseType", ...] = (),
column_oriented: bool = False,
- source: Closable = None,
- query_id: str = None,
- summary: dict[str, Any] = None,
+ source: Closable | None = None,
+ query_id: str | None = None,
+ summary: dict[str, Any] | None = None,
):
- self._result_rows = result_set
- self._result_columns = None
- self._block_gen = block_gen or empty_gen()
+ self._result_rows: Matrix | None = result_set
+ self._result_columns: Matrix | None = None
+ self._block_gen: Generator[Matrix, None, None] | None = block_gen or empty_gen()
self._in_context = False
self._query_id = query_id
self.column_names = column_names
@@ -320,7 +324,7 @@ class QueryResult(Closable):
else:
self._result_columns = [[] for _ in range(len(self.column_names))]
else:
- result = [[] for _ in range(len(self.column_names))]
+ result: list[list[Any]] = [[] for _ in range(len(self.column_names))]
with self.column_block_stream as stream:
for block in stream:
for base, added in zip(result, block):
@@ -343,7 +347,7 @@ class QueryResult(Closable):
query_id = self.summary.get("query_id")
if query_id:
return query_id
- return self._query_id
+ return self._query_id or ""
def _column_block_stream(self):
if self._block_gen is None:
@@ -383,13 +387,17 @@ class QueryResult(Closable):
return len(self.result_set)
@property
- def first_item(self) -> dict[str, Any]:
+ def first_item(self) -> dict[str, Any] | None:
+ if self.row_count == 0:
+ return None
if self.column_oriented:
return {name: col[0] for name, col in zip(self.column_names, self.result_set)}
return dict(zip(self.column_names, self.result_set[0]))
@property
- def first_row(self) -> Sequence[Any]:
+ def first_row(self) -> Sequence[Any] | None:
+ if self.row_count == 0:
+ return None
if self.column_oriented:
return [col[0] for col in self.result_set]
return self.result_set[0]
diff --git a/contrib/python/clickhouse-connect/clickhouse_connect/driver/streaming.py b/contrib/python/clickhouse-connect/clickhouse_connect/driver/streaming.py
index 0a20bfb7697..ddd50f21b3e 100644
--- a/contrib/python/clickhouse-connect/clickhouse_connect/driver/streaming.py
+++ b/contrib/python/clickhouse-connect/clickhouse_connect/driver/streaming.py
@@ -33,15 +33,15 @@ class StreamingResponseSource(Closable):
self.exception_tag = exception_tag
# maxsize=10 means max ~10 socket reads buffered
- self.queue = AsyncSyncQueue(maxsize=10)
+ self.queue: AsyncSyncQueue[bytes | Exception] = AsyncSyncQueue(maxsize=10)
self._decompressor = None
self._decompressor_initialized = False
# Multiple accesses to .gen must return the same generator, not create new ones
- self._gen_cache = None
+ self._gen_cache: Iterator[bytes] | None = None
- self._producer_task = None
+ self._producer_task: asyncio.Task | None = None
self._producer_started = threading.Event()
self._producer_error: Exception | None = None
self._producer_completed = False
@@ -257,7 +257,7 @@ class StreamingInsertSource:
self.transform = transform
self.context = context
self.loop = loop
- self.queue = AsyncSyncQueue(maxsize=maxsize)
+ self.queue: AsyncSyncQueue[bytes | bytearray | Exception] = AsyncSyncQueue(maxsize=maxsize)
self._producer_future = None
self._started = False
diff --git a/contrib/python/clickhouse-connect/clickhouse_connect/driver/summary.py b/contrib/python/clickhouse-connect/clickhouse_connect/driver/summary.py
index 8938bb82ed1..6627bc65ad0 100644
--- a/contrib/python/clickhouse-connect/clickhouse_connect/driver/summary.py
+++ b/contrib/python/clickhouse-connect/clickhouse_connect/driver/summary.py
@@ -3,9 +3,9 @@ from clickhouse_connect.driver.query import QueryResult
class QuerySummary:
- summary = {}
+ summary: dict[str, str] = {}
- def __init__(self, summary: dict | None = None):
+ def __init__(self, summary: dict[str, str] | None = None):
if summary is not None:
self.summary = summary
@@ -20,7 +20,7 @@ class QuerySummary:
return self.summary.get("query_id", "")
def as_query_result(self) -> QueryResult:
- data = []
+ data: list[int | str] = []
column_names = []
column_types = []
str_type = get_from_name("String")
diff --git a/contrib/python/clickhouse-connect/clickhouse_connect/driver/transform.py b/contrib/python/clickhouse-connect/clickhouse_connect/driver/transform.py
index 5970277b5ee..4a4378d239e 100644
--- a/contrib/python/clickhouse-connect/clickhouse_connect/driver/transform.py
+++ b/contrib/python/clickhouse-connect/clickhouse_connect/driver/transform.py
@@ -3,7 +3,7 @@ import logging
from clickhouse_connect.datatypes import registry
from clickhouse_connect.driver.common import write_leb128
from clickhouse_connect.driver.compression import get_compressor
-from clickhouse_connect.driver.exceptions import StreamCompleteException, StreamFailureError
+from clickhouse_connect.driver.exceptions import OperationalError, StreamCompleteException, StreamFailureError
from clickhouse_connect.driver.insert import InsertContext
from clickhouse_connect.driver.npquery import NumpyResult
from clickhouse_connect.driver.query import QueryContext, QueryResult
@@ -71,8 +71,10 @@ class NativeTransform:
raise StreamFailureError(error_msg) from None
raise StreamFailureError("Stream ended unexpectedly (connection closed by server)") from ex
- # Handle async streaming errors (ClientPayloadError from aiohttp)
- if ex.__class__.__name__ == "ClientPayloadError":
+ # A read failure partway through the stream: OperationalError from the sync reader,
+ # ClientPayloadError from aiohttp. ClickHouse may have written the real error into the
+ # response body before the connection dropped, so prefer that over the transport error.
+ if isinstance(ex, OperationalError) or ex.__class__.__name__ == "ClientPayloadError":
if source.last_message:
error_msg = None
exception_tag = getattr(source, "exception_tag", None)
@@ -106,7 +108,8 @@ class NativeTransform:
@staticmethod
def build_insert(context: InsertContext):
- compressor = get_compressor(context.compression)
+ compression = context.compression if isinstance(context.compression, str) else None
+ compressor = get_compressor(compression)
def chunk_gen():
for block in context.next_block():
diff --git a/contrib/python/clickhouse-connect/clickhouse_connect/driver/types.py b/contrib/python/clickhouse-connect/clickhouse_connect/driver/types.py
index f54b5e86e60..7dadbac32bf 100644
--- a/contrib/python/clickhouse-connect/clickhouse_connect/driver/types.py
+++ b/contrib/python/clickhouse-connect/clickhouse_connect/driver/types.py
@@ -12,7 +12,7 @@ class Closable(ABC):
class ByteSource(Closable):
- last_message: bytes = None
+ last_message: bytes | None = None
@abstractmethod
def read_leb128(self) -> int:
@@ -31,7 +31,7 @@ class ByteSource(Closable):
pass
@abstractmethod
- def read_str_col(self, num_rows: int, encoding: str, nullable: bool = False, null_obj: Any = None):
+ def read_str_col(self, num_rows: int, encoding: str | None, nullable: bool = False, null_obj: Any = None):
pass
@abstractmethod
diff --git a/contrib/python/clickhouse-connect/clickhouse_connect/driver/tzutil.py b/contrib/python/clickhouse-connect/clickhouse_connect/driver/tzutil.py
index 7eb330da8bb..f00065df60d 100644
--- a/contrib/python/clickhouse-connect/clickhouse_connect/driver/tzutil.py
+++ b/contrib/python/clickhouse-connect/clickhouse_connect/driver/tzutil.py
@@ -5,7 +5,7 @@ from datetime import datetime, timedelta, timezone, tzinfo
tzlocal = None
try:
- import tzlocal # Maybe we can use the tzlocal module to get a safe timezone
+ import tzlocal # type: ignore[no-redef] # Maybe we can use the tzlocal module to get a safe timezone
except ImportError:
pass
@@ -270,7 +270,9 @@ def _detect_local_tz() -> tzinfo:
return resolve_zone(env_tz)
except zoneinfo.ZoneInfoNotFoundError:
pass
- return datetime.now().astimezone().tzinfo
+ local = datetime.now().astimezone().tzinfo
+ assert local is not None
+ return local
local_tz, local_tz_dst_safe = normalize_timezone(_detect_local_tz())
diff --git a/contrib/python/clickhouse-connect/clickhouse_connect/driverc/buffer.pyx b/contrib/python/clickhouse-connect/clickhouse_connect/driverc/buffer.pyx
index 1e8bc8e0ca2..9eac5a94d70 100644
--- a/contrib/python/clickhouse-connect/clickhouse_connect/driverc/buffer.pyx
+++ b/contrib/python/clickhouse-connect/clickhouse_connect/driverc/buffer.pyx
@@ -1,3 +1,4 @@
+# cython: freethreading_compatible = True
import sys
from typing import Iterable, Any, Optional
@@ -22,12 +23,19 @@ cdef char * errors = 'strict'
cdef char * utf8 = 'utf8'
cdef dict array_templates = {}
cdef bint must_swap = sys.byteorder == 'big'
-cdef array.array swapper = array.array('Q', [0])
-for c in 'bBuhHiIlLqQfd':
+for c in 'bBhHiIlLqQfd':
array_templates[c] = array.array(c, [])
+cdef inline unsigned long long _bswap_uint64(unsigned long long v):
+ """Byte-swap a 64-bit unsigned integer for big-endian systems."""
+ return (((v & 0xFF) << 56) | (((v >> 8) & 0xFF) << 48) |
+ (((v >> 16) & 0xFF) << 40) | (((v >> 24) & 0xFF) << 32) |
+ (((v >> 32) & 0xFF) << 24) | (((v >> 40) & 0xFF) << 16) |
+ (((v >> 48) & 0xFF) << 8) | ((v >> 56) & 0xFF))
+
+
cdef class ResponseBuffer:
def __init__(self, source):
self.slice_sz = 4096
@@ -279,11 +287,11 @@ cdef class ResponseBuffer:
@cython.wraparound(False)
def read_uint64(self) -> int:
cdef ull_wrapper* x
+ cdef unsigned long long tmp
cdef char* b = self.read_bytes_c(8)
if must_swap:
- memcpy(swapper.data.as_voidptr, b, 8)
- swapper.byteswap()
- return swapper[0]
+ memcpy(&tmp, b, 8)
+ return _bswap_uint64(tmp)
x = <ull_wrapper *> b
return x.int_value
diff --git a/contrib/python/clickhouse-connect/clickhouse_connect/driverc/dataconv.pyx b/contrib/python/clickhouse-connect/clickhouse_connect/driverc/dataconv.pyx
index 2718ae46454..c82fe85b5e3 100644
--- a/contrib/python/clickhouse-connect/clickhouse_connect/driverc/dataconv.pyx
+++ b/contrib/python/clickhouse-connect/clickhouse_connect/driverc/dataconv.pyx
@@ -1,3 +1,4 @@
+# cython: freethreading_compatible = True
import struct
from typing import Sequence, Optional
diff --git a/contrib/python/clickhouse-connect/clickhouse_connect/driverc/npconv.pyx b/contrib/python/clickhouse-connect/clickhouse_connect/driverc/npconv.pyx
index 21568f4f5de..120db6638f5 100644
--- a/contrib/python/clickhouse-connect/clickhouse_connect/driverc/npconv.pyx
+++ b/contrib/python/clickhouse-connect/clickhouse_connect/driverc/npconv.pyx
@@ -1,3 +1,4 @@
+# cython: freethreading_compatible = True
import cython
import numpy as np
diff --git a/contrib/python/clickhouse-connect/clickhouse_connect/py.typed b/contrib/python/clickhouse-connect/clickhouse_connect/py.typed
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/contrib/python/clickhouse-connect/clickhouse_connect/py.typed
diff --git a/contrib/python/clickhouse-connect/ya.make b/contrib/python/clickhouse-connect/ya.make
index 34bf63b5b78..800f19f6818 100644
--- a/contrib/python/clickhouse-connect/ya.make
+++ b/contrib/python/clickhouse-connect/ya.make
@@ -2,7 +2,7 @@
PY3_LIBRARY()
-VERSION(1.3.0)
+VERSION(1.4.1)
LICENSE(Apache-2.0)
@@ -119,6 +119,7 @@ RESOURCE_FILES(
.dist-info/METADATA
.dist-info/entry_points.txt
.dist-info/top_level.txt
+ clickhouse_connect/py.typed
)
END()
diff --git a/contrib/python/textual/.dist-info/METADATA b/contrib/python/textual/.dist-info/METADATA
index f3bb97ae79a..bdcd6666e66 100644
--- a/contrib/python/textual/.dist-info/METADATA
+++ b/contrib/python/textual/.dist-info/METADATA
@@ -1,6 +1,6 @@
Metadata-Version: 2.4
Name: textual
-Version: 8.2.7
+Version: 8.2.8
Summary: Modern Text User Interface framework
License: MIT
License-File: LICENSE
diff --git a/contrib/python/textual/textual/_compositor.py b/contrib/python/textual/textual/_compositor.py
index ee4c231f240..9af38a23c7e 100644
--- a/contrib/python/textual/textual/_compositor.py
+++ b/contrib/python/textual/textual/_compositor.py
@@ -923,11 +923,8 @@ class Compositor:
x -= region.x + gutter_left
y -= region.y + gutter_right
- if y < 0:
- return None, None
-
- if x < 0:
- return widget, Offset(0, y)
+ if x < 0 or y < 0:
+ return widget, Offset(max(0, x), max(0, y))
visible_screen_stack.set(widget.app._background_screens)
line = widget.render_line(y)
diff --git a/contrib/python/textual/textual/_xterm_parser.py b/contrib/python/textual/textual/_xterm_parser.py
index eafa7414ecb..df9112370cd 100644
--- a/contrib/python/textual/textual/_xterm_parser.py
+++ b/contrib/python/textual/textual/_xterm_parser.py
@@ -39,7 +39,7 @@ SPECIAL_SEQUENCES = {BRACKETED_PASTE_START, BRACKETED_PASTE_END, FOCUSIN, FOCUSO
"""Set of special sequences."""
_re_extended_key: Final[re.Pattern[str]] = re.compile(
- r"\x1b\[((?:\d*;?){2,3})([u~ABCDEFHPQRS])"
+ r"\x1b\[((?:[\d:]*;?){2,3})([u~ABCDEFHPQRS])"
)
_re_in_band_window_resize: Final[re.Pattern[str]] = re.compile(
r"\x1b\[48;(\d+(?:\:.*?)?);(\d+(?:\:.*?)?);(\d+(?:\:.*?)?);(\d+(?:\:.*?)?)t"
@@ -334,8 +334,26 @@ class XTermParser(Parser[Message]):
self._debug_log_file.close()
self._debug_log_file = None
+ @classmethod
+ def _parse_colon_codepoints(cls, text_str: str) -> list[str | None]:
+ """Convert codepoints split on colons in to a list of characters.
+
+ Args:
+ text_str: String with groups of digits, separated by one or more colons.
+
+ Returns:
+ A list of characters.
+ """
+ if not text_str:
+ return [None]
+ characters: list[str | None] = [
+ chr(int(part)) if part.isdecimal() else chr(1)
+ for part in text_str.split(":")
+ ]
+ return characters
+
@lru_cache(maxsize=1024)
- def _parse_extended_key(self, sequence: str) -> events.Key | None:
+ def _parse_extended_key(self, sequence: str) -> list[events.Key] | None:
"""Parse a Kitty sequence.
Args:
@@ -348,38 +366,47 @@ class XTermParser(Parser[Message]):
if (match := _re_extended_key.fullmatch(sequence)) is None:
return None
+ key_events: list[events.Key] = []
+
codes, end = match.groups(default="")
codepoint_str, modifiers_str, text_str, *_ = codes.split(";") + ["", "", ""]
-
codepoint = int(codepoint_str or "1")
modifiers = int(modifiers_str or "0")
- text = chr(int(text_str)) if text_str else None
- if not (key := FUNCTIONAL_KEYS.get(f"{codepoint}{end}", "")):
- key = _character_to_key(text if text else chr(codepoint))
+ for text in self._parse_colon_codepoints(text_str):
+ if not (key := FUNCTIONAL_KEYS.get(f"{codepoint}{end}", "")):
+ key = _character_to_key(text if text else chr(codepoint))
- key_tokens: list[str] = []
- # The modifier is redundant on a modifier key
- if modifiers and key not in MODIFIER_FUNCTIONAL_KEYS and text_str is not None:
- modifier_bits = int(modifiers) - 1
- # Not convinced of the utility in reporting caps_lock and num_lock
- MODIFIERS = ("alt", "ctrl", "super", "hyper", "meta")
- # Ignore caps_lock and num_lock modifiers
- if modifier_bits & 1 and (text is None or text.isspace()):
- key_tokens.append("shift")
- for bit, modifier in enumerate(MODIFIERS, 1):
- if modifier == "alt" and text is not None:
- continue
- if modifier_bits & (1 << bit):
- key_tokens.append(modifier)
+ key_tokens: list[str] = []
+ # The modifier is redundant on a modifier key
+ if (
+ modifiers
+ and key not in MODIFIER_FUNCTIONAL_KEYS
+ and text_str is not None
+ ):
+ modifier_bits = int(modifiers) - 1
+ # Not convinced of the utility in reporting caps_lock and num_lock
+ MODIFIERS = ("alt", "ctrl", "super", "hyper", "meta")
+ # Ignore caps_lock and num_lock modifiers
+ if modifier_bits & 1 and (text is None or text.isspace()):
+ key_tokens.append("shift")
+ for bit, modifier in enumerate(MODIFIERS, 1):
+ if modifier == "alt" and text is not None:
+ continue
+ if modifier_bits & (1 << bit):
+ key_tokens.append(modifier)
- key_tokens.sort()
- if key is not None:
- key_tokens.append(key)
- return events.Key(
- "+".join(key_tokens),
- text or (None if modifiers else SPECIAL_KEY_TO_CHARACTER.get(key, None)),
- )
+ key_tokens.sort()
+ if key is not None:
+ key_tokens.append(key)
+ key_events.append(
+ events.Key(
+ "+".join(key_tokens),
+ text
+ or (None if modifiers else SPECIAL_KEY_TO_CHARACTER.get(key, None)),
+ )
+ )
+ return key_events
def _sequence_to_key_events(
self, sequence: str, alt: bool = False
@@ -395,9 +422,10 @@ class XTermParser(Parser[Message]):
if (
not constants.DISABLE_KITTY_KEY
- and (key := self._parse_extended_key(sequence)) is not None
+ and (keys := self._parse_extended_key(sequence)) is not None
):
- yield key.copy()
+ for key in keys:
+ yield key.copy()
return
keys = ANSI_SEQUENCES_KEYS.get(sequence)
diff --git a/contrib/python/textual/textual/pilot.py b/contrib/python/textual/textual/pilot.py
index c387b258276..76adae9d594 100644
--- a/contrib/python/textual/textual/pilot.py
+++ b/contrib/python/textual/textual/pilot.py
@@ -438,7 +438,7 @@ class Pilot(Generic[ReturnType]):
)
offset = Offset(message_arguments["x"], message_arguments["y"])
- if offset not in screen.region:
+ if offset not in screen.size.region:
raise OutOfBounds(
"Target offset is outside of currently-visible screen region."
)
diff --git a/contrib/python/textual/textual/screen.py b/contrib/python/textual/textual/screen.py
index af9070538e7..c8b1d1462d4 100644
--- a/contrib/python/textual/textual/screen.py
+++ b/contrib/python/textual/textual/screen.py
@@ -1838,8 +1838,11 @@ class Screen(Generic[ScreenResultType], Widget):
if select_offset is not None:
content_widget = select_widget
content_offset = select_offset
- assert isinstance(content_widget.parent, Widget)
- container = content_widget.parent
+ container = (
+ content_widget
+ if isinstance(content_widget, Screen)
+ else content_widget.parent
+ )
else:
content_widget = None
container = select_widget
@@ -1884,6 +1887,7 @@ class Screen(Generic[ScreenResultType], Widget):
select_widget, select_offset = self.get_widget_and_offset_at(
event.x, event.y
)
+
if (
select_widget is not None
and select_widget.allow_select
@@ -1893,8 +1897,11 @@ class Screen(Generic[ScreenResultType], Widget):
if select_offset is not None:
content_widget = select_widget
content_offset = select_offset
- assert isinstance(content_widget.parent, Widget)
- container = content_widget.parent
+ container = (
+ content_widget
+ if isinstance(content_widget, Screen)
+ else content_widget.parent
+ )
else:
content_widget = None
container = select_widget
diff --git a/contrib/python/textual/textual/widgets/_input.py b/contrib/python/textual/textual/widgets/_input.py
index 3aa2a945bdb..bcf2c2994de 100644
--- a/contrib/python/textual/textual/widgets/_input.py
+++ b/contrib/python/textual/textual/widgets/_input.py
@@ -122,9 +122,14 @@ class Input(ScrollView):
Binding(
"ctrl+w", "delete_left_word", "Delete left to start of word", show=False
),
- Binding("ctrl+u", "delete_left_all", "Delete all to the left", show=False),
Binding(
- "ctrl+backspace",
+ "ctrl+u,super+backspace",
+ "delete_left_all",
+ "Delete all to the left",
+ show=False,
+ ),
+ Binding(
+ "ctrl+backspace,alt+backspace",
"delete_right_word",
"Delete right to start of word",
show=False,
@@ -145,6 +150,7 @@ class Input(ScrollView):
| shift+right | Move cursor right and select. |
| ctrl+right | Move the cursor one word to the right. |
| backspace | Delete the character to the left of the cursor. |
+ | ctrl+backspace,alt+backspace | Delete right to start of word. |
| ctrl+shift+right | Move cursor right a word and select. |
| ctrl+shift+a | Select all text in the input. |
| home,ctrl+a | Go to the beginning of the input. |
@@ -154,7 +160,7 @@ class Input(ScrollView):
| delete,ctrl+d | Delete the character to the right of the cursor. |
| enter | Submit the current value of the input. |
| ctrl+w | Delete the word to the left of the cursor. |
- | ctrl+u | Delete everything to the left of the cursor. |
+ | ctrl+u,super+backspace | Delete everything to the left of the cursor. |
| ctrl+f | Delete the word to the right of the cursor. |
| ctrl+k | Delete everything to the right of the cursor. |
| ctrl+x | Cut selected text. |
diff --git a/contrib/python/textual/textual/widgets/_text_area.py b/contrib/python/textual/textual/widgets/_text_area.py
index d970a31c5a7..1ca74675e54 100644
--- a/contrib/python/textual/textual/widgets/_text_area.py
+++ b/contrib/python/textual/textual/widgets/_text_area.py
@@ -354,7 +354,7 @@ TextArea {
show=False,
),
Binding(
- "ctrl+w,ctrl+backspace",
+ "ctrl+w,ctrl+backspace,alt+backspace",
"delete_word_left",
"Delete left to start of word",
show=False,
@@ -390,7 +390,7 @@ TextArea {
show=False,
),
Binding(
- "ctrl+u",
+ "ctrl+u,super+backspace",
"delete_to_start_of_line",
"Delete to line start",
show=False,
@@ -446,7 +446,7 @@ TextArea {
| delete,ctrl+d | Delete character to the right of cursor. |
| alt+delete | Delete from cursor to end of the word. |
| ctrl+shift+k | Delete the current line. |
- | ctrl+u | Delete from cursor to the start of the line. |
+ | ctrl+u,super+backspace | Delete from cursor to the start of the line. |
| ctrl+k | Delete from cursor to the end of the line. |
| f6 | Select the current line. |
| f7 | Select all text in the document. |
diff --git a/contrib/python/textual/ya.make b/contrib/python/textual/ya.make
index 84a9205d17e..fd182d0171f 100644
--- a/contrib/python/textual/ya.make
+++ b/contrib/python/textual/ya.make
@@ -2,7 +2,7 @@
PY3_LIBRARY()
-VERSION(8.2.7)
+VERSION(8.2.8)
LICENSE(MIT)
diff --git a/library/cpp/yt/error/error-inl.h b/library/cpp/yt/error/error-inl.h
index 1542d008f58..85751e8a3cf 100644
--- a/library/cpp/yt/error/error-inl.h
+++ b/library/cpp/yt/error/error-inl.h
@@ -604,4 +604,24 @@ void FormatValue(TStringBuilderBase* builder, const TErrorOr<T>& error, TStringB
////////////////////////////////////////////////////////////////////////////////
+template <class F, class... As>
+auto RunNoExcept(F&& functor, As&&... args) noexcept -> decltype(functor(std::forward<As>(args)...))
+{
+ return functor(std::forward<As>(args)...);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline TStringBuf GetWellKnownLoggingTag(const std::exception&)
+{
+ return "Error"_sb;
+}
+
+inline TStringBuf GetWellKnownLoggingTag(const TError&)
+{
+ return "Error"_sb;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
} // namespace NYT
diff --git a/library/cpp/yt/error/error.h b/library/cpp/yt/error/error.h
index 13223af8d6c..1e5c2147d82 100644
--- a/library/cpp/yt/error/error.h
+++ b/library/cpp/yt/error/error.h
@@ -475,10 +475,15 @@ void FormatValue(TStringBuilderBase* builder, const TErrorOr<T>& error, TStringB
////////////////////////////////////////////////////////////////////////////////
template <class F, class... As>
-auto RunNoExcept(F&& functor, As&&... args) noexcept -> decltype(functor(std::forward<As>(args)...))
-{
- return functor(std::forward<As>(args)...);
-}
+auto RunNoExcept(F&& functor, As&&... args) noexcept -> decltype(functor(std::forward<As>(args)...));
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Registers errors as a well-known logging tag (ADL customization point for
+//! library/cpp/yt/logging), so that |YT_TLOG_*(...).With(error)| attaches them under
+//! the "Error" key, rendered after the message in plain text.
+TStringBuf GetWellKnownLoggingTag(const std::exception&);
+TStringBuf GetWellKnownLoggingTag(const TError&);
////////////////////////////////////////////////////////////////////////////////
diff --git a/library/cpp/yt/logging/backends/arcadia/backend.cpp b/library/cpp/yt/logging/backends/arcadia/backend.cpp
index 3c6ff9f5f58..5878ba52be3 100644
--- a/library/cpp/yt/logging/backends/arcadia/backend.cpp
+++ b/library/cpp/yt/logging/backends/arcadia/backend.cpp
@@ -57,7 +57,7 @@ public:
// Use low-level api, because it is more convinient here.
auto loggingContext = GetLoggingContext();
auto event = NDetail::CreateLogEvent(loggingContext, Logger_, logLevel);
- event.MessageRef = NDetail::BuildLogMessage(loggingContext, Logger_, message).MessageRef;
+ event.Payload = NDetail::BuildLogMessage(loggingContext, Logger_, message).Payload;
event.Family = ELogFamily::PlainText;
Logger_.Write(std::move(event));
}
diff --git a/library/cpp/yt/logging/benchmark/logging.cpp b/library/cpp/yt/logging/benchmark/logging.cpp
new file mode 100644
index 00000000000..8745511f2bd
--- /dev/null
+++ b/library/cpp/yt/logging/benchmark/logging.cpp
@@ -0,0 +1,167 @@
+#include <benchmark/benchmark.h>
+
+#include <library/cpp/yt/logging/logger.h>
+
+#include <atomic>
+
+namespace NYT::NLogging {
+namespace {
+
+////////////////////////////////////////////////////////////////////////////////
+
+// A log manager whose Enqueue is essentially free, so that the benchmark measures
+// only the producer-side cost (building the event payload), not the logging thread.
+class TBenchmarkLogManager
+ : public ILogManager
+{
+public:
+ void RegisterStaticAnchor(TLoggingAnchor* anchor, ::TSourceLocation /*sourceLocation*/, TStringBuf /*message*/) override
+ {
+ anchor->Registered.store(true);
+ }
+
+ void UpdateAnchor(TLoggingAnchor* anchor) override
+ {
+ anchor->CurrentVersion.store(ActualVersion_.load());
+ }
+
+ void Enqueue(TLogEvent&& event) override
+ {
+ // Keep the produced payload observable so the build is not optimized away;
+ // the event is then destroyed (freeing its chunk slice) -- same for both APIs.
+ benchmark::DoNotOptimize(std::get<TTaggedLogEventPayload>(event.Payload).Underlying().Begin());
+ }
+
+ const TLoggingCategory* GetCategory(TStringBuf /*categoryName*/) override
+ {
+ return &Category_;
+ }
+
+ void UpdateCategory(TLoggingCategory* category) override
+ {
+ category->CurrentVersion.store(ActualVersion_.load());
+ }
+
+ bool GetAbortOnAlert() const override
+ {
+ return false;
+ }
+
+private:
+ std::atomic<int> ActualVersion_ = 1;
+ TLoggingCategory Category_ = {
+ .Name = "Bench",
+ .MinPlainTextLevel = ELogLevel::Minimum,
+ .CurrentVersion = 0,
+ .ActualVersion = &ActualVersion_,
+ };
+};
+
+////////////////////////////////////////////////////////////////////////////////
+// No tags.
+
+void BM_Log_NoTags(benchmark::State& state)
+{
+ TBenchmarkLogManager manager;
+ TLogger Logger(&manager, "Bench");
+ for (auto _ : state) {
+ YT_LOG_INFO("This is a log message of a typical length without tags");
+ }
+}
+BENCHMARK(BM_Log_NoTags);
+
+void BM_TLog_NoTags(benchmark::State& state)
+{
+ TBenchmarkLogManager manager;
+ TLogger Logger(&manager, "Bench");
+ for (auto _ : state) {
+ YT_TLOG_INFO("This is a log message of a typical length without tags");
+ }
+}
+BENCHMARK(BM_TLog_NoTags);
+
+////////////////////////////////////////////////////////////////////////////////
+// One int tag.
+
+void BM_Log_OneTag(benchmark::State& state)
+{
+ TBenchmarkLogManager manager;
+ TLogger Logger(&manager, "Bench");
+ for (auto _ : state) {
+ YT_LOG_INFO("This is a log message of a typical length (TagName: %v)",
+ 123);
+ }
+}
+BENCHMARK(BM_Log_OneTag);
+
+void BM_TLog_OneTag(benchmark::State& state)
+{
+ TBenchmarkLogManager manager;
+ TLogger Logger(&manager, "Bench");
+ for (auto _ : state) {
+ YT_TLOG_INFO("This is a log message of a typical length")
+ .With("TagName", 123);
+ }
+}
+BENCHMARK(BM_TLog_OneTag);
+
+////////////////////////////////////////////////////////////////////////////////
+// Two tags (int + string).
+
+void BM_Log_TwoTags(benchmark::State& state)
+{
+ TBenchmarkLogManager manager;
+ TLogger Logger(&manager, "Bench");
+ for (auto _ : state) {
+ YT_LOG_INFO("This is a log message of a typical length (TagName1: %v, TagName2: %v)",
+ 123,
+ "this-is-a-string");
+ }
+}
+BENCHMARK(BM_Log_TwoTags);
+
+void BM_TLog_TwoTags(benchmark::State& state)
+{
+ TBenchmarkLogManager manager;
+ TLogger Logger(&manager, "Bench");
+ for (auto _ : state) {
+ YT_TLOG_INFO("This is a log message of a typical length")
+ .With("TagName1", 123)
+ .With("TagName2", "this-is-a-string");
+ }
+}
+BENCHMARK(BM_TLog_TwoTags);
+
+////////////////////////////////////////////////////////////////////////////////
+// Three tags (int + string + double).
+
+void BM_Log_ThreeTags(benchmark::State& state)
+{
+ TBenchmarkLogManager manager;
+ TLogger Logger(&manager, "Bench");
+ for (auto _ : state) {
+ YT_LOG_INFO("This is a log message of a typical length (TagName1: %v, TagName2: %v, TagName3: %v)",
+ 123,
+ "this-is-a-string",
+ 3.14);
+ }
+}
+BENCHMARK(BM_Log_ThreeTags);
+
+void BM_TLog_ThreeTags(benchmark::State& state)
+{
+ TBenchmarkLogManager manager;
+ TLogger Logger(&manager, "Bench");
+ for (auto _ : state) {
+ YT_TLOG_INFO("This is a log message of a typical length")
+ .With("TagName1", 123)
+ .With("TagName2", "this-is-a-string")
+ .With("TagName3", 3.14);
+ }
+}
+BENCHMARK(BM_TLog_ThreeTags);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NLogging
diff --git a/library/cpp/yt/logging/benchmark/ya.make b/library/cpp/yt/logging/benchmark/ya.make
index 61fd0b49276..a7a23128cdf 100644
--- a/library/cpp/yt/logging/benchmark/ya.make
+++ b/library/cpp/yt/logging/benchmark/ya.make
@@ -4,6 +4,7 @@ INCLUDE(${ARCADIA_ROOT}/library/cpp/yt/ya_cpp.make.inc)
SRCS(
logger_tag.cpp
+ logging.cpp
)
PEERDIR(
diff --git a/library/cpp/yt/logging/logger-inl.h b/library/cpp/yt/logging/logger-inl.h
index fa20fb06489..ff4d6d81bbe 100644
--- a/library/cpp/yt/logging/logger-inl.h
+++ b/library/cpp/yt/logging/logger-inl.h
@@ -4,6 +4,8 @@
#include "logger.h"
#endif
+#include "tagged_payload.h"
+
#include <library/cpp/yt/yson_string/convert.h>
#include <library/cpp/yt/yson_string/string.h>
@@ -107,33 +109,6 @@ Y_FORCE_INLINE const TLogger& TLogger::operator()() const
namespace NDetail {
-struct TMessageStringBuilderContext
-{
- TSharedMutableRef Chunk;
-};
-
-struct TMessageBufferTag
-{ };
-
-class TMessageStringBuilder
- : public TStringBuilderBase
-{
-public:
- TSharedRef Flush();
-
- // For testing only.
- static void DisablePerThreadCache();
-
-protected:
- void DoReset() override;
- void DoReserve(size_t newLength) override;
-
-private:
- TSharedMutableRef Buffer_;
-
- static constexpr size_t ChunkSize = 128_KB - 64;
-};
-
inline bool HasMessageTags(
const TLoggingContext& loggingContext,
const TLogger& logger)
@@ -162,14 +137,14 @@ inline void AppendMessageTags(
}
if (const auto& traceLoggingTag = loggingContext.TraceLoggingTag; !traceLoggingTag.empty()) {
if (printComma) {
- builder->AppendString(TStringBuf(", "));
+ builder->AppendString(", "_sb);
}
builder->AppendString(traceLoggingTag);
printComma = true;
}
if (const auto& threadMessageTag = GetThreadMessageTag(); !threadMessageTag.empty()) {
if (printComma) {
- builder->AppendString(TStringBuf(", "));
+ builder->AppendString(", "_sb);
}
builder->AppendString(threadMessageTag);
printComma = true;
@@ -185,10 +160,10 @@ inline void AppendLogMessage(
if (HasMessageTags(loggingContext, logger)) {
if (message.Size() >= 1 && message[message.Size() - 1] == ')') {
builder->AppendString(TStringBuf(message.Begin(), message.Size() - 1));
- builder->AppendString(TStringBuf(", "));
+ builder->AppendString(", "_sb);
} else {
builder->AppendString(TStringBuf(message.Begin(), message.Size()));
- builder->AppendString(TStringBuf(" ("));
+ builder->AppendString(" ("_sb);
}
AppendMessageTags(builder, loggingContext, logger);
builder->AppendChar(')');
@@ -208,10 +183,10 @@ void AppendLogMessageWithFormat(
if (HasMessageTags(loggingContext, logger)) {
if (format.size() >= 2 && format[format.size() - 1] == ')') {
builder->AppendFormat(TRuntimeFormat{format.substr(0, format.size() - 1)}, std::forward<TArgs>(args)...);
- builder->AppendString(TStringBuf(", "));
+ builder->AppendString(", "_sb);
} else {
builder->AppendFormat(TRuntimeFormat{format}, std::forward<TArgs>(args)...);
- builder->AppendString(TStringBuf(" ("));
+ builder->AppendString(" ("_sb);
}
AppendMessageTags(builder, loggingContext, logger);
builder->AppendChar(')');
@@ -222,7 +197,7 @@ void AppendLogMessageWithFormat(
struct TLogMessage
{
- TSharedRef MessageRef;
+ TTaggedLogEventPayload Payload;
TStringBuf Anchor;
};
@@ -233,9 +208,10 @@ TLogMessage BuildLogMessage(
TFormatString<TArgs...> format,
TArgs&&... args)
{
- TMessageStringBuilder builder;
- AppendLogMessageWithFormat(&builder, loggingContext, logger, format.Get(), std::forward<TArgs>(args)...);
- return {builder.Flush(), format.Get()};
+ TTaggedPayloadWriter writer;
+ AppendLogMessageWithFormat(writer.BeginMessage(), loggingContext, logger, format.Get(), std::forward<TArgs>(args)...);
+ writer.EndMessage();
+ return {writer.Finish(), format.Get()};
}
template <CFormattable T>
@@ -245,13 +221,15 @@ TLogMessage BuildLogMessage(
const TLogger& logger,
const T& obj)
{
- TMessageStringBuilder builder;
- FormatValue(&builder, obj, TStringBuf("v"));
+ TTaggedPayloadWriter writer;
+ auto* builder = writer.BeginMessage();
+ FormatValue(builder, obj, "v"_sb);
if (HasMessageTags(loggingContext, logger)) {
- builder.AppendString(TStringBuf(" ("));
- AppendMessageTags(&builder, loggingContext, logger);
- builder.AppendChar(')');
+ builder->AppendString(" ("_sb);
+ AppendMessageTags(builder, loggingContext, logger);
+ builder->AppendChar(')');
}
+ writer.EndMessage();
if constexpr (std::same_as<TStringBuf, std::remove_cvref_t<T>>) {
// NB(arkady-e1ppa): This is the overload where TStringBuf
@@ -262,9 +240,9 @@ TLogMessage BuildLogMessage(
// us having overload for TStringBuf (both have implicit ctors from
// string literals) thus we have to accommodate TStringBuf specifics
// in this if constexpr part.
- return {builder.Flush(), obj};
+ return {writer.Finish(), obj};
} else {
- return {builder.Flush(), TStringBuf()};
+ return {writer.Finish(), TStringBuf()};
}
}
@@ -295,13 +273,10 @@ inline TLogMessage BuildLogMessage(
const TLogger& logger,
TSharedRef&& message)
{
- if (HasMessageTags(loggingContext, logger)) {
- TMessageStringBuilder builder;
- AppendLogMessage(&builder, loggingContext, logger, message);
- return {builder.Flush(), TStringBuf()};
- } else {
- return {std::move(message), TStringBuf()};
- }
+ TTaggedPayloadWriter writer;
+ AppendLogMessage(writer.BeginMessage(), loggingContext, logger, message);
+ writer.EndMessage();
+ return {writer.Finish(), TStringBuf()};
}
inline TLogEvent CreateLogEvent(
@@ -310,10 +285,10 @@ inline TLogEvent CreateLogEvent(
ELogLevel level)
{
TLogEvent event;
- event.Instant = loggingContext.Instant;
event.Category = logger.GetCategory();
- event.Essential = logger.IsEssential();
event.Level = level;
+ event.Essential = logger.IsEssential();
+ event.Instant = loggingContext.Instant;
event.ThreadId = loggingContext.ThreadId;
event.ThreadName = loggingContext.ThreadName;
event.FiberId = loggingContext.FiberId;
@@ -332,15 +307,24 @@ inline void LogEventImpl(
ELogLevel level,
::TSourceLocation sourceLocation,
TLoggingAnchor* anchor,
- TSharedRef message)
+ TTaggedLogEventPayload payload)
{
- auto event = CreateLogEvent(loggingContext, logger, level);
- event.MessageKind = ELogMessageKind::Unstructured;
- event.MessageRef = std::move(message);
- event.Family = ELogFamily::PlainText;
- event.SourceFile = sourceLocation.File;
- event.SourceLine = sourceLocation.Line;
- event.Anchor = anchor;
+ auto event = TLogEvent{
+ .Category = logger.GetCategory(),
+ .Level = level,
+ .Family = ELogFamily::PlainText,
+ .Essential = logger.IsEssential(),
+ .Payload = std::move(payload),
+ .Instant = loggingContext.Instant,
+ .ThreadId = loggingContext.ThreadId,
+ .ThreadName = loggingContext.ThreadName,
+ .FiberId = loggingContext.FiberId,
+ .TraceId = loggingContext.TraceId,
+ .RequestId = loggingContext.RequestId,
+ .SourceFile = sourceLocation.File,
+ .SourceLine = sourceLocation.Line,
+ .Anchor = anchor,
+ };
if (Y_UNLIKELY(event.Level >= ELogLevel::Alert)) {
logger.Write(TLogEvent(event));
OnCriticalLogEvent(logger, event);
@@ -349,6 +333,299 @@ inline void LogEventImpl(
}
}
+////////////////////////////////////////////////////////////////////////////////
+
+//! References the per-call-site static anchor and its one-shot registration flag.
+//! Produced by the lambda embedded in the fluent |YT_TLOG_*| macros.
+struct TStaticAnchorRef
+{
+ TLoggingAnchor* Anchor;
+ std::atomic<bool>* Registered;
+};
+
+//! Wraps the format spec passed to |TTaggedLoggingGuard::With| and validates at compile
+//! time that it is a |%|-prefixed string literal (e.g. |"%v"|, |"%08x"|). The stored
+//! spec has the leading |%| stripped, as expected by |FormatValue|.
+class TLoggingTagSpec
+{
+public:
+ template <size_t N>
+ consteval TLoggingTagSpec(const char (&spec)[N])
+ : Spec_(spec + 1, N - 2)
+ {
+ static_assert(N >= 2, "Logging tag format spec must be a non-empty string literal");
+ if (spec[0] != '%') {
+ TheLoggingTagFormatSpecMustStartWithPercentSign();
+ }
+ }
+
+ TStringBuf Get() const
+ {
+ return Spec_;
+ }
+
+private:
+ const TStringBuf Spec_;
+
+ // Undefined on purpose: calling it from the |consteval| ctor turns a missing
+ // leading |%| into a compile error that names the violated rule.
+ static void TheLoggingTagFormatSpecMustStartWithPercentSign();
+};
+
+class TWellKnownTaggedLoggingGuard;
+
+//! Accumulates a tagged log message via a fluent |.With| chain and emits the event in
+//! its destructor. Instantiated by the fluent |YT_TLOG_*| macros, which guarantee that
+//! the chain is reached only when the level is enabled (so tag value expressions are
+//! not evaluated otherwise).
+//!
+//! The user message -- with the logger's contextual (logger/trace/thread) tags folded
+//! in -- goes to the payload message field; each |.With(key, value)| becomes a
+//! structured payload tag (see #TTaggedPayloadWriter).
+class TTaggedLoggingGuard
+{
+public:
+ TTaggedLoggingGuard(
+ const TLogger& logger,
+ ELogLevel level,
+ ::TSourceLocation sourceLocation,
+ TStaticAnchorRef anchorRef,
+ TStringBuf message)
+ : TTaggedLoggingGuard(
+ logger,
+ level,
+ sourceLocation,
+ anchorRef,
+ message,
+ /*alwaysBuildMessage*/ false)
+ { }
+
+ TTaggedLoggingGuard(const TTaggedLoggingGuard&) = delete;
+ TTaggedLoggingGuard& operator=(const TTaggedLoggingGuard&) = delete;
+
+ bool IsEnabled() const
+ {
+ return Enabled_;
+ }
+
+ template <class TValue>
+ TTaggedLoggingGuard& With(TStringBuf tag, const TValue& value) &
+ {
+ return DoWith(tag, value, "v"_sb);
+ }
+
+ template <class TValue>
+ TTaggedLoggingGuard& With(TStringBuf tag, const TValue& value, TLoggingTagSpec spec) &
+ {
+ return DoWith(tag, value, spec.Get());
+ }
+
+ //! Attaches a well-known tag whose key is resolved from #value's type via the
+ //! |GetWellKnownLoggingTag| ADL point (the type must opt in, e.g. errors).
+ //!
+ //! Returns a #TWellKnownTaggedLoggingGuard, which exposes only further well-known
+ //! tags: the payload contract requires well-known tags to come last (so
+ //! #FormatTaggedPayload can stay single-pass), so a keyed |.With(key, value)| after a
+ //! well-known tag must not compile.
+ template <class TValue>
+ TWellKnownTaggedLoggingGuard With(const TValue& value) &;
+
+ ~TTaggedLoggingGuard()
+ {
+ if (!Enabled_) {
+ return;
+ }
+ LogEventImpl(
+ LoggingContext_,
+ Logger_,
+ EffectiveLevel_,
+ SourceLocation_,
+ Anchor_,
+ Writer_.Finish());
+ }
+
+protected:
+ const TLogger& Logger_;
+ const ::TSourceLocation SourceLocation_;
+ TLoggingAnchor* const Anchor_;
+
+ bool Enabled_ = false;
+ ELogLevel EffectiveLevel_ = ELogLevel::Minimum;
+ TLoggingContext LoggingContext_;
+ TTaggedPayloadWriter Writer_;
+
+ //! Shared constructor. When #alwaysBuildMessage is set the payload message is built
+ //! even if the level is disabled (so a terminal guard can still recover it); #Enabled_
+ //! continues to gate whether the destructor emits the event.
+ TTaggedLoggingGuard(
+ const TLogger& logger,
+ ELogLevel level,
+ ::TSourceLocation sourceLocation,
+ TStaticAnchorRef anchorRef,
+ TStringBuf message,
+ bool alwaysBuildMessage)
+ : Logger_(logger)
+ , SourceLocation_(sourceLocation)
+ , Anchor_(anchorRef.Anchor)
+ {
+ if (!Logger_.IsAnchorUpToDate(*Anchor_)) [[unlikely]] {
+ Logger_.UpdateStaticAnchor(Anchor_, anchorRef.Registered, sourceLocation, message);
+ }
+
+ EffectiveLevel_ = TLogger::GetEffectiveLoggingLevel(level, *Anchor_);
+ Enabled_ = Logger_.IsLevelEnabled(EffectiveLevel_);
+ if (!Enabled_ && !alwaysBuildMessage) {
+ return;
+ }
+
+ LoggingContext_ = GetLoggingContext();
+
+ auto* builder = Writer_.BeginMessage();
+ builder->AppendString(message);
+ if (HasMessageTags(LoggingContext_, Logger_)) {
+ builder->AppendString(" ("_sb);
+ AppendMessageTags(builder, LoggingContext_, Logger_);
+ builder->AppendChar(')');
+ }
+ Writer_.EndMessage();
+ }
+
+private:
+ template <class TValue>
+ TTaggedLoggingGuard& DoWith(TStringBuf tag, const TValue& value, TStringBuf spec) &
+ {
+ // Format the value straight into the payload buffer; no temporary.
+ FormatValue(Writer_.BeginTag(tag), value, spec);
+ Writer_.EndTag();
+ return *this;
+ }
+};
+
+//! Restricts the fluent |.With| chain once a well-known tag has been attached. The
+//! payload contract requires well-known tags to come last (#FormatTaggedPayload is
+//! single-pass), so only further well-known |.With(value)| calls are exposed -- a keyed
+//! |.With(key, value)| after a well-known tag fails to compile.
+class TWellKnownTaggedLoggingGuard
+{
+public:
+ explicit TWellKnownTaggedLoggingGuard(TTaggedLoggingGuard& guard)
+ : Guard_(guard)
+ { }
+
+ template <class TValue>
+ TWellKnownTaggedLoggingGuard With(const TValue& value) &&
+ {
+ return Guard_.With(value);
+ }
+
+private:
+ TTaggedLoggingGuard& Guard_;
+};
+
+template <class TValue>
+TWellKnownTaggedLoggingGuard TTaggedLoggingGuard::With(const TValue& value) &
+{
+ FormatValue(Writer_.BeginWellKnownTag(GetWellKnownLoggingTag(value)), value, "v"_sb);
+ Writer_.EndTag();
+ return TWellKnownTaggedLoggingGuard(*this);
+}
+
+//! Terminal guard for the fluent |YT_TLOG_FATAL| macros. Builds the message
+//! unconditionally and, once the |.With| chain completes, emits the event at |Fatal|
+//! level -- which aborts the process. The enclosing |for| invokes #Commit in its step.
+class TTaggedFatalLoggingGuard
+ : public TTaggedLoggingGuard
+{
+public:
+ TTaggedFatalLoggingGuard(
+ const TLogger& logger,
+ ::TSourceLocation sourceLocation,
+ TStaticAnchorRef anchorRef,
+ TStringBuf message)
+ : TTaggedLoggingGuard(logger, ELogLevel::Fatal, sourceLocation, anchorRef, message, /*alwaysBuildMessage*/ true)
+ { }
+
+ //! Returns true exactly once, so the enclosing |for| runs the |.With| chain a single
+ //! time before its step expression commits the event.
+ bool TryEnter()
+ {
+ bool pending = Pending_;
+ Pending_ = false;
+ return pending;
+ }
+
+ //! Emits the event at |Fatal| level; the log manager aborts the process.
+ [[noreturn]] void Commit() &
+ {
+ Enabled_ = false; // The event is emitted here, not from the base destructor.
+ LogEventImpl(LoggingContext_, Logger_, ELogLevel::Fatal, SourceLocation_, Anchor_, Writer_.Finish());
+ Y_UNREACHABLE();
+ }
+
+private:
+ bool Pending_ = true;
+};
+
+//! Terminal guard for the fluent |YT_TLOG_ALERT_AND_THROW| macros. Builds the message
+//! unconditionally; once the |.With| chain completes, #Commit emits the event at |Alert|
+//! level (when enabled) and returns the message so the macro can attach it to the thrown
+//! error. The throw lives in the macro -- the logging library must not depend on the
+//! error library, and a destructor must not throw.
+class TTaggedThrowingLoggingGuard
+ : public TTaggedLoggingGuard
+{
+public:
+ TTaggedThrowingLoggingGuard(
+ const TLogger& logger,
+ ::TSourceLocation sourceLocation,
+ TStaticAnchorRef anchorRef,
+ TStringBuf message)
+ : TTaggedLoggingGuard(logger, ELogLevel::Alert, sourceLocation, anchorRef, message, /*alwaysBuildMessage*/ true)
+ { }
+
+ //! Returns true exactly once, so the enclosing |for| runs the |.With| chain a single
+ //! time before its step expression commits the event and throws.
+ bool TryEnter()
+ {
+ bool pending = Pending_;
+ Pending_ = false;
+ return pending;
+ }
+
+ //! Emits the alert event (when enabled) and returns its message for the error payload.
+ std::string Commit() &
+ {
+ auto payload = Writer_.Finish();
+ std::string message(GetMessageFromTaggedPayload(payload));
+ if (Enabled_) {
+ Enabled_ = false; // The event is emitted here, not from the base destructor.
+ LogEventImpl(LoggingContext_, Logger_, EffectiveLevel_, SourceLocation_, Anchor_, std::move(payload));
+ }
+ return message;
+ }
+
+private:
+ bool Pending_ = true;
+};
+
+//! A no-op stand-in for #TTaggedLoggingGuard used by compile-time-disabled trace logging:
+//! it swallows the |.With| chain without evaluating it.
+class TNullTaggedLoggingGuard
+{
+public:
+ template <class... TArgs>
+ TNullTaggedLoggingGuard& With(TArgs&&...)
+ {
+ return *this;
+ }
+};
+
+template <class TMessage>
+TNullTaggedLoggingGuard MakeNullTaggedLoggingGuard(const TMessage&)
+{
+ return {};
+}
+
} // namespace NDetail
////////////////////////////////////////////////////////////////////////////////
diff --git a/library/cpp/yt/logging/logger.cpp b/library/cpp/yt/logging/logger.cpp
index acb823a614e..5359c1cd141 100644
--- a/library/cpp/yt/logging/logger.cpp
+++ b/library/cpp/yt/logging/logger.cpp
@@ -1,5 +1,7 @@
#include "logger.h"
+#include "structured_payload.h"
+
#include <library/cpp/yt/assert/assert.h>
#include <library/cpp/yt/cpu_clock/clock.h>
@@ -25,98 +27,13 @@ void OnCriticalLogEvent(
event.Level == ELogLevel::Alert && logger.GetAbortOnAlert())
{
fprintf(stderr, "*** Aborting on critical log event\n");
- fwrite(event.MessageRef.begin(), 1, event.MessageRef.size(), stderr);
+ auto message = FormatTaggedPayload(std::get<TTaggedLogEventPayload>(event.Payload));
+ fwrite(message.data(), 1, message.size(), stderr);
fprintf(stderr, "\n");
YT_ABORT();
}
}
-TSharedRef TMessageStringBuilder::Flush()
-{
- return Buffer_.Slice(0, GetLength());
-}
-
-void TMessageStringBuilder::DoReset()
-{
- Buffer_.Reset();
-}
-
-struct TPerThreadCache;
-
-YT_DEFINE_THREAD_LOCAL(TPerThreadCache*, Cache);
-YT_DEFINE_THREAD_LOCAL(bool, CacheDestroyed);
-
-struct TPerThreadCache
-{
- TSharedMutableRef Chunk;
- size_t ChunkOffset = 0;
-
- ~TPerThreadCache()
- {
- TMessageStringBuilder::DisablePerThreadCache();
- }
-
- static YT_PREVENT_TLS_CACHING TPerThreadCache* GetCache()
- {
- auto& cache = Cache();
- if (Y_LIKELY(cache)) {
- return cache;
- }
- if (CacheDestroyed()) {
- return nullptr;
- }
- static thread_local TPerThreadCache CacheData;
- cache = &CacheData;
- return cache;
- }
-};
-
-void TMessageStringBuilder::DisablePerThreadCache()
-{
- Cache() = nullptr;
- CacheDestroyed() = true;
-}
-
-void TMessageStringBuilder::DoReserve(size_t newCapacity)
-{
- auto oldLength = GetLength();
- newCapacity = FastClp2(newCapacity);
-
- auto newChunkSize = std::max(ChunkSize, newCapacity);
- // Hold the old buffer until the data is copied.
- auto oldBuffer = std::move(Buffer_);
- auto* cache = TPerThreadCache::GetCache();
- if (Y_LIKELY(cache)) {
- auto oldCapacity = End_ - Begin_;
- auto deltaCapacity = newCapacity - oldCapacity;
- if (End_ == cache->Chunk.Begin() + cache->ChunkOffset &&
- cache->ChunkOffset + deltaCapacity <= cache->Chunk.Size())
- {
- // Resize inplace.
- Buffer_ = cache->Chunk.Slice(cache->ChunkOffset - oldCapacity, cache->ChunkOffset + deltaCapacity);
- cache->ChunkOffset += deltaCapacity;
- End_ = Begin_ + newCapacity;
- return;
- }
-
- if (Y_UNLIKELY(cache->ChunkOffset + newCapacity > cache->Chunk.Size())) {
- cache->Chunk = TSharedMutableRef::Allocate<TMessageBufferTag>(newChunkSize, {.InitializeStorage = false});
- cache->ChunkOffset = 0;
- }
-
- Buffer_ = cache->Chunk.Slice(cache->ChunkOffset, cache->ChunkOffset + newCapacity);
- cache->ChunkOffset += newCapacity;
- } else {
- Buffer_ = TSharedMutableRef::Allocate<TMessageBufferTag>(newChunkSize, {.InitializeStorage = false});
- newCapacity = newChunkSize;
- }
- if (oldLength > 0) {
- ::memcpy(Buffer_.Begin(), Begin_, oldLength);
- }
- Begin_ = Buffer_.Begin();
- End_ = Begin_ + newCapacity;
-}
-
} // namespace NDetail
////////////////////////////////////////////////////////////////////////////////
@@ -370,8 +287,7 @@ void LogStructuredEvent(
loggingContext,
logger,
level);
- event.MessageKind = ELogMessageKind::Structured;
- event.MessageRef = message.ToSharedRef();
+ event.Payload = MakeStructuredPayloadFromYson(message);
event.Family = ELogFamily::Structured;
logger.Write(std::move(event));
}
diff --git a/library/cpp/yt/logging/logger.h b/library/cpp/yt/logging/logger.h
index d781326e88d..0f29b6f7376 100644
--- a/library/cpp/yt/logging/logger.h
+++ b/library/cpp/yt/logging/logger.h
@@ -80,11 +80,6 @@ using TRequestId = TGuid;
////////////////////////////////////////////////////////////////////////////////
-DEFINE_ENUM(ELogMessageKind,
- (Unstructured)
- (Structured)
-);
-
struct TLogEvent
{
const TLoggingCategory* Category = nullptr;
@@ -92,8 +87,9 @@ struct TLogEvent
ELogFamily Family = ELogFamily::PlainText;
bool Essential = false;
- ELogMessageKind MessageKind = ELogMessageKind::Unstructured;
- TSharedRef MessageRef;
+ //! The active alternative identifies the event kind (tagged plain-text vs
+ //! structured) and determines how the bytes are decoded.
+ TLogEventPayload Payload;
TCpuInstant Instant = 0;
@@ -327,10 +323,10 @@ void LogStructuredEvent(
#define YT_LOG_ALERT_IF(condition, ...) if (condition) YT_LOG_ALERT(__VA_ARGS__)
#define YT_LOG_ALERT_UNLESS(condition, ...) if (!(condition)) YT_LOG_ALERT(__VA_ARGS__)
-#define YT_LOG_FATAL(...) \
- do { \
+#define YT_LOG_FATAL(...) \
+ do { \
YT_LOG_EVENT(Logger, ::NYT::NLogging::ELogLevel::Fatal, __VA_ARGS__); \
- Y_UNREACHABLE(); \
+ Y_UNREACHABLE(); \
} while(false)
#define YT_LOG_FATAL_IF(condition, ...) if (Y_UNLIKELY(condition)) YT_LOG_FATAL(__VA_ARGS__)
#define YT_LOG_FATAL_UNLESS(condition, ...) if (!Y_LIKELY(condition)) YT_LOG_FATAL(__VA_ARGS__)
@@ -346,126 +342,233 @@ void LogStructuredEvent(
* 4. The exception is raised regardless of the logging level configured for the |Logger| and
* and the currently active message suppressions.
*/
-#define YT_LOG_ALERT_AND_THROW(...) \
- do { \
- /* NOLINTBEGIN(bugprone-reserved-identifier, readability-identifier-naming) */ \
- const auto& logger__ = Logger(); \
- auto level__ = ::NYT::NLogging::ELogLevel::Alert; \
- auto location__ = __LOCATION__; \
- \
- auto loggingContext__ = ::NYT::NLogging::GetLoggingContext(); \
- auto message__ = ::NYT::NLogging::NDetail::BuildLogMessage(loggingContext__, logger__, __VA_ARGS__); \
- auto messageStr__ = ::std::string(message__.MessageRef.ToStringBuf()); \
- \
- static ::NYT::TLeakyStorage<::NYT::NLogging::TLoggingAnchor> anchorStorage__; \
- auto* anchor__ = anchorStorage__.Get(); \
- \
- bool anchorUpToDate__ = logger__.IsAnchorUpToDate(*anchor__); \
- if (!anchorUpToDate__) [[unlikely]] { \
- static std::atomic<bool> anchorRegistered__; \
- logger__.UpdateStaticAnchor(anchor__, &anchorRegistered__, location__, message__.Anchor); \
- } \
- \
- auto effectiveLevel__ = ::NYT::NLogging::TLogger::GetEffectiveLoggingLevel(level__, *anchor__); \
- if (logger__.IsLevelEnabled(effectiveLevel__)) { \
- ::NYT::NLogging::NDetail::LogEventImpl( \
- loggingContext__, \
- logger__, \
- effectiveLevel__, \
- location__, \
- anchor__, \
- std::move(message__.MessageRef)); \
- } \
- \
- THROW_ERROR_EXCEPTION( \
- ::NYT::EErrorCode::Fatal, \
- "Malformed request or incorrect state detected") \
- << ::NYT::TErrorAttribute("message", std::move(messageStr__)); \
- /* NOLINTEND(bugprone-reserved-identifier, readability-identifier-naming) */ \
+#define YT_LOG_ALERT_AND_THROW(...) \
+ do { \
+ /* NOLINTBEGIN(bugprone-reserved-identifier, readability-identifier-naming) */ \
+ const auto& logger__ = Logger(); \
+ auto level__ = ::NYT::NLogging::ELogLevel::Alert; \
+ auto location__ = __LOCATION__; \
+ \
+ auto loggingContext__ = ::NYT::NLogging::GetLoggingContext(); \
+ auto message__ = ::NYT::NLogging::NDetail::BuildLogMessage(loggingContext__, logger__, __VA_ARGS__); \
+ /* Copy the message out before the payload is moved into the log event below. */ \
+ auto messageStr__ = ::std::string(::NYT::NLogging::GetMessageFromTaggedPayload(message__.Payload)); \
+ \
+ static ::NYT::TLeakyStorage<::NYT::NLogging::TLoggingAnchor> anchorStorage__; \
+ auto* anchor__ = anchorStorage__.Get(); \
+ \
+ bool anchorUpToDate__ = logger__.IsAnchorUpToDate(*anchor__); \
+ if (!anchorUpToDate__) [[unlikely]] { \
+ static std::atomic<bool> anchorRegistered__; \
+ logger__.UpdateStaticAnchor(anchor__, &anchorRegistered__, location__, message__.Anchor); \
+ } \
+ \
+ auto effectiveLevel__ = ::NYT::NLogging::TLogger::GetEffectiveLoggingLevel(level__, *anchor__); \
+ if (logger__.IsLevelEnabled(effectiveLevel__)) { \
+ ::NYT::NLogging::NDetail::LogEventImpl( \
+ loggingContext__, \
+ logger__, \
+ effectiveLevel__, \
+ location__, \
+ anchor__, \
+ std::move(message__.Payload)); \
+ } \
+ \
+ THROW_ERROR_EXCEPTION( \
+ ::NYT::EErrorCode::Fatal, \
+ "Malformed request or incorrect state detected") \
+ << ::NYT::TErrorAttribute("message", std::move(messageStr__)); \
+ /* NOLINTEND(bugprone-reserved-identifier, readability-identifier-naming) */ \
} while (false)
#define YT_LOG_ALERT_AND_THROW_IF(condition, ...) \
- if (Y_UNLIKELY(condition)) { \
- YT_LOG_ALERT_AND_THROW(__VA_ARGS__); \
- } \
+ if (Y_UNLIKELY(condition)) { \
+ YT_LOG_ALERT_AND_THROW(__VA_ARGS__); \
+ } \
static_assert(true)
#define YT_LOG_ALERT_AND_THROW_UNLESS(condition, ...) \
- if (!Y_UNLIKELY(condition)) { \
- YT_LOG_ALERT_AND_THROW(__VA_ARGS__); \
- } \
+ if (!Y_UNLIKELY(condition)) { \
+ YT_LOG_ALERT_AND_THROW(__VA_ARGS__); \
+ } \
static_assert(true)
-#define YT_LOG_EVENT(logger, level, ...) \
- do { \
- /* NOLINTBEGIN(bugprone-reserved-identifier, readability-identifier-naming) */ \
- const auto& logger__ = (logger)(); \
- auto level__ = (level); \
- auto location__ = __LOCATION__; \
- static ::NYT::TLeakyStorage<::NYT::NLogging::TLoggingAnchor> anchorStorage__; \
- auto* anchor__ = anchorStorage__.Get(); \
- \
- bool anchorUpToDate__ = logger__.IsAnchorUpToDate(*anchor__); \
- if (anchorUpToDate__) [[likely]] { \
- auto effectiveLevel__ = ::NYT::NLogging::TLogger::GetEffectiveLoggingLevel(level__, *anchor__); \
- if (!logger__.IsLevelEnabled(effectiveLevel__)) { \
- break; \
- } \
- } \
- \
- auto loggingContext__ = ::NYT::NLogging::GetLoggingContext(); \
+#define YT_LOG_EVENT(logger, level, ...) \
+ do { \
+ /* NOLINTBEGIN(bugprone-reserved-identifier, readability-identifier-naming) */ \
+ const auto& logger__ = (logger)(); \
+ auto level__ = (level); \
+ auto location__ = __LOCATION__; \
+ static ::NYT::TLeakyStorage<::NYT::NLogging::TLoggingAnchor> anchorStorage__; \
+ auto* anchor__ = anchorStorage__.Get(); \
+ \
+ bool anchorUpToDate__ = logger__.IsAnchorUpToDate(*anchor__); \
+ if (anchorUpToDate__) [[likely]] { \
+ auto effectiveLevel__ = ::NYT::NLogging::TLogger::GetEffectiveLoggingLevel(level__, *anchor__); \
+ if (!logger__.IsLevelEnabled(effectiveLevel__)) { \
+ break; \
+ } \
+ } \
+ \
+ auto loggingContext__ = ::NYT::NLogging::GetLoggingContext(); \
auto message__ = ::NYT::NLogging::NDetail::BuildLogMessage(loggingContext__, logger__, __VA_ARGS__); \
- \
- if (!anchorUpToDate__) [[unlikely]] { \
- static std::atomic<bool> anchorRegistered__; \
- logger__.UpdateStaticAnchor(anchor__, &anchorRegistered__, location__, message__.Anchor); \
- } \
- \
- auto effectiveLevel__ = ::NYT::NLogging::TLogger::GetEffectiveLoggingLevel(level__, *anchor__); \
- if (!logger__.IsLevelEnabled(effectiveLevel__)) { \
- break; \
- } \
- \
- ::NYT::NLogging::NDetail::LogEventImpl( \
- loggingContext__, \
- logger__, \
- effectiveLevel__, \
- location__, \
- anchor__, \
- std::move(message__.MessageRef)); \
- /* NOLINTEND(bugprone-reserved-identifier, readability-identifier-naming) */ \
+ \
+ if (!anchorUpToDate__) [[unlikely]] { \
+ static std::atomic<bool> anchorRegistered__; \
+ logger__.UpdateStaticAnchor(anchor__, &anchorRegistered__, location__, message__.Anchor); \
+ } \
+ \
+ auto effectiveLevel__ = ::NYT::NLogging::TLogger::GetEffectiveLoggingLevel(level__, *anchor__); \
+ if (!logger__.IsLevelEnabled(effectiveLevel__)) { \
+ break; \
+ } \
+ \
+ ::NYT::NLogging::NDetail::LogEventImpl( \
+ loggingContext__, \
+ logger__, \
+ effectiveLevel__, \
+ location__, \
+ anchor__, \
+ std::move(message__.Payload)); \
+ /* NOLINTEND(bugprone-reserved-identifier, readability-identifier-naming) */ \
} while (false)
-#define YT_LOG_EVENT_WITH_DYNAMIC_ANCHOR(logger, level, anchor, ...) \
- do { \
- const auto& logger__ = (logger)(); \
- auto level__ = (level); \
- auto location__ = __LOCATION__; \
- auto* anchor__ = (anchor); \
- \
- bool anchorUpToDate__ = logger__.IsAnchorUpToDate(*anchor__); \
- if (!anchorUpToDate__) [[unlikely]] { \
- logger__.UpdateDynamicAnchor(anchor__); \
- } \
- \
- auto effectiveLevel__ = ::NYT::NLogging::TLogger::GetEffectiveLoggingLevel(level__, *anchor__); \
- if (!logger__.IsLevelEnabled(effectiveLevel__)) { \
- break; \
- } \
- \
- auto loggingContext__ = ::NYT::NLogging::GetLoggingContext(); \
+#define YT_LOG_EVENT_WITH_DYNAMIC_ANCHOR(logger, level, anchor, ...) \
+ do { \
+ const auto& logger__ = (logger)(); \
+ auto level__ = (level); \
+ auto location__ = __LOCATION__; \
+ auto* anchor__ = (anchor); \
+ \
+ bool anchorUpToDate__ = logger__.IsAnchorUpToDate(*anchor__); \
+ if (!anchorUpToDate__) [[unlikely]] { \
+ logger__.UpdateDynamicAnchor(anchor__); \
+ } \
+ \
+ auto effectiveLevel__ = ::NYT::NLogging::TLogger::GetEffectiveLoggingLevel(level__, *anchor__); \
+ if (!logger__.IsLevelEnabled(effectiveLevel__)) { \
+ break; \
+ } \
+ \
+ auto loggingContext__ = ::NYT::NLogging::GetLoggingContext(); \
auto message__ = ::NYT::NLogging::NDetail::BuildLogMessage(loggingContext__, logger__, __VA_ARGS__); \
- \
- ::NYT::NLogging::NDetail::LogEventImpl( \
- loggingContext__, \
- logger__, \
- effectiveLevel__, \
- location__, \
- anchor__, \
- std::move(message__.MessageRef)); \
+ \
+ ::NYT::NLogging::NDetail::LogEventImpl( \
+ loggingContext__, \
+ logger__, \
+ effectiveLevel__, \
+ location__, \
+ anchor__, \
+ std::move(message__.Payload)); \
} while (false)
////////////////////////////////////////////////////////////////////////////////
+// Tagged logging
+//
+// Tags are supplied via a fluent |.With(name, value)| (or |.With(name, value, "%spec")|)
+// chain; they are carried as structured key/value pairs in the event payload. A single-
+// argument |.With(value)| attaches the value under a statically known key resolved by ADL
+// (e.g. |.With(error)| under the "Error" key):
+//
+// YT_TLOG_INFO("Message")
+// .With("Key", value)
+// .With("Count", count, "%08x")
+// .With(error);
+//
+// If the message is not logged then the |.With| chain is not evaluated, so tag value
+// expressions cost nothing.
+
+//! Yields a #TStaticAnchorRef for the expansion site: a per-call-site leaky anchor and
+//! its one-shot registration flag, produced via an immediately-invoked lambda.
+#define YT_TLOG_STATIC_ANCHOR_REF() \
+ [] { \
+ /* NOLINTBEGIN(bugprone-reserved-identifier, readability-identifier-naming) */ \
+ static ::NYT::TLeakyStorage<::NYT::NLogging::TLoggingAnchor> anchorStorage__; \
+ static std::atomic<bool> anchorRegistered__; \
+ return ::NYT::NLogging::NDetail::TStaticAnchorRef{anchorStorage__.Get(), &anchorRegistered__}; \
+ /* NOLINTEND(bugprone-reserved-identifier, readability-identifier-naming) */ \
+ }()
+
+#define YT_TLOG_EVENT_FLUENT(logger, level, message) \
+ if (::NYT::NLogging::NDetail::TTaggedLoggingGuard loggingGuard__( \
+ (logger)(), \
+ (level), \
+ __LOCATION__, \
+ YT_TLOG_STATIC_ANCHOR_REF(), \
+ (message)); \
+ !loggingGuard__.IsEnabled()) \
+ { } else \
+ loggingGuard__
+
+#ifdef YT_ENABLE_TRACE_LOGGING
+#define YT_TLOG_TRACE(message) YT_TLOG_EVENT_FLUENT(Logger, ::NYT::NLogging::ELogLevel::Trace, message)
+#define YT_TLOG_TRACE_IF(condition, message) if (condition) YT_TLOG_TRACE(message)
+#define YT_TLOG_TRACE_UNLESS(condition, message) if (!(condition)) YT_TLOG_TRACE(message)
+#else
+#define YT_TLOG_UNUSED(message) if (true) { } else ::NYT::NLogging::NDetail::MakeNullTaggedLoggingGuard(message)
+#define YT_TLOG_TRACE(message) YT_TLOG_UNUSED(message)
+#define YT_TLOG_TRACE_IF(condition, message) YT_TLOG_UNUSED(message)
+#define YT_TLOG_TRACE_UNLESS(condition, message) YT_TLOG_UNUSED(message)
+#endif
+
+#define YT_TLOG_DEBUG(message) YT_TLOG_EVENT_FLUENT(Logger, ::NYT::NLogging::ELogLevel::Debug, message)
+#define YT_TLOG_DEBUG_IF(condition, message) if (condition) YT_TLOG_DEBUG(message)
+#define YT_TLOG_DEBUG_UNLESS(condition, message) if (!(condition)) YT_TLOG_DEBUG(message)
+
+#define YT_TLOG_INFO(message) YT_TLOG_EVENT_FLUENT(Logger, ::NYT::NLogging::ELogLevel::Info, message)
+#define YT_TLOG_INFO_IF(condition, message) if (condition) YT_TLOG_INFO(message)
+#define YT_TLOG_INFO_UNLESS(condition, message) if (!(condition)) YT_TLOG_INFO(message)
+
+#define YT_TLOG_WARNING(message) YT_TLOG_EVENT_FLUENT(Logger, ::NYT::NLogging::ELogLevel::Warning, message)
+#define YT_TLOG_WARNING_IF(condition, message) if (condition) YT_TLOG_WARNING(message)
+#define YT_TLOG_WARNING_UNLESS(condition, message) if (!(condition)) YT_TLOG_WARNING(message)
+
+#define YT_TLOG_ERROR(message) YT_TLOG_EVENT_FLUENT(Logger, ::NYT::NLogging::ELogLevel::Error, message)
+#define YT_TLOG_ERROR_IF(condition, message) if (condition) YT_TLOG_ERROR(message)
+#define YT_TLOG_ERROR_UNLESS(condition, message) if (!(condition)) YT_TLOG_ERROR(message)
+
+#define YT_TLOG_ALERT(message) YT_TLOG_EVENT_FLUENT(Logger, ::NYT::NLogging::ELogLevel::Alert, message)
+#define YT_TLOG_ALERT_IF(condition, message) if (condition) YT_TLOG_ALERT(message)
+#define YT_TLOG_ALERT_UNLESS(condition, message) if (!(condition)) YT_TLOG_ALERT(message)
+
+// The terminal action of |YT_TLOG_FATAL|/|YT_TLOG_ALERT_AND_THROW| (aborting or throwing)
+// must run *after* the |.With| chain, which the user appends to the macro. A guard cannot
+// do it in its destructor -- that would run mid-chain on a temporary, and a throwing
+// destructor terminates. So both expand to a single-iteration |for| whose step expression
+// fires once the chain (the loop body) has completed.
+
+#define YT_TLOG_FATAL(message) \
+ for (::NYT::NLogging::NDetail::TTaggedFatalLoggingGuard loggingGuard__( \
+ Logger(), \
+ __LOCATION__, \
+ YT_TLOG_STATIC_ANCHOR_REF(), \
+ (message)); \
+ loggingGuard__.TryEnter(); \
+ loggingGuard__.Commit()) \
+ loggingGuard__
+#define YT_TLOG_FATAL_IF(condition, message) if (condition) [[unlikely]] YT_TLOG_FATAL(message)
+#define YT_TLOG_FATAL_UNLESS(condition, message) if (!(condition)) [[unlikely]] YT_TLOG_FATAL(message)
+
+// See #YT_LOG_ALERT_AND_THROW for the rationale. The throw lives here -- not in the guard
+// -- because the logging library must not depend on the error library. The guard's
+// |Commit| logs the alert (when enabled) and returns the message for the |"message"|
+// attribute.
+#define YT_TLOG_ALERT_AND_THROW(message) \
+ for (::NYT::NLogging::NDetail::TTaggedThrowingLoggingGuard loggingGuard__( \
+ Logger(), \
+ __LOCATION__, \
+ YT_TLOG_STATIC_ANCHOR_REF(), \
+ (message)); \
+ loggingGuard__.TryEnter(); \
+ THROW_ERROR_EXCEPTION( \
+ ::NYT::EErrorCode::Fatal, \
+ "Malformed request or incorrect state detected") \
+ << ::NYT::TErrorAttribute("message", loggingGuard__.Commit())) \
+ loggingGuard__
+#define YT_TLOG_ALERT_AND_THROW_IF(condition, message) if (condition) [[unlikely]] YT_TLOG_ALERT_AND_THROW(message)
+#define YT_TLOG_ALERT_AND_THROW_UNLESS(condition, message) if (!(condition)) [[unlikely]] YT_TLOG_ALERT_AND_THROW(message)
+
+////////////////////////////////////////////////////////////////////////////////
} // namespace NYT::NLogging
diff --git a/library/cpp/yt/logging/plain_text_formatter/formatter.cpp b/library/cpp/yt/logging/plain_text_formatter/formatter.cpp
index ab2181113dd..85f0889fa9b 100644
--- a/library/cpp/yt/logging/plain_text_formatter/formatter.cpp
+++ b/library/cpp/yt/logging/plain_text_formatter/formatter.cpp
@@ -1,9 +1,13 @@
#include "formatter.h"
+#include <library/cpp/yt/logging/structured_payload.h>
+
#include <library/cpp/yt/cpu_clock/clock.h>
#include <library/cpp/yt/misc/port.h>
+#include <variant>
+
#ifdef YT_USE_SSE42
#include <emmintrin.h>
#include <pmmintrin.h>
@@ -146,6 +150,37 @@ void FormatMessage(TBaseFormatter* out, TStringBuf message)
}
}
+// Formats |Message (Key: Value, ...)|, with well-known tags (e.g. an error) appended
+// after the |(...)| group. Well-known tags are always written last, so a single pass
+// suffices. Every piece -- message, tag keys/values, and the newline separating a
+// well-known tag -- goes through FormatMessage and is escaped, so the rendered payload
+// stays on a single physical line (a newline is emitted as the literal "\n").
+void FormatPayload(TBaseFormatter* out, const TTaggedLogEventPayload& payload)
+{
+ TTaggedPayloadReader reader(payload);
+ FormatMessage(out, reader.ReadMessage());
+ bool parenOpen = false;
+ while (auto tag = reader.TryReadTag()) {
+ if (tag->IsWellKnown) {
+ if (parenOpen) {
+ out->AppendChar(')');
+ parenOpen = false;
+ }
+ FormatMessage(out, "\n"_sb);
+ FormatMessage(out, tag->Value);
+ } else {
+ out->AppendString(parenOpen ? ", "_sb : " ("_sb);
+ parenOpen = true;
+ FormatMessage(out, tag->Key);
+ out->AppendString(": "_sb);
+ FormatMessage(out, tag->Value);
+ }
+ }
+ if (parenOpen) {
+ out->AppendChar(')');
+ }
+}
+
////////////////////////////////////////////////////////////////////////////////
void TCachingDateFormatter::Format(TBaseFormatter* buffer, TInstant dateTime, bool printMicroseconds)
@@ -186,7 +221,13 @@ void TPlainTextEventFormatter::Format(TBaseFormatter* buffer, const TLogEvent& e
buffer->AppendChar('\t');
- FormatMessage(buffer, event.MessageRef.ToStringBuf());
+ if (const auto* tagged = std::get_if<TTaggedLogEventPayload>(&event.Payload)) {
+ FormatPayload(buffer, *tagged);
+ } else {
+ // A structured event routed to a plain-text writer: emit its raw YSON fragment
+ // (escaped, so the record stays a single physical line).
+ FormatMessage(buffer, GetYsonFromStructuredPayload(std::get<TStructuredLogEventPayload>(event.Payload)).AsStringBuf());
+ }
buffer->AppendChar('\t');
diff --git a/library/cpp/yt/logging/private.h b/library/cpp/yt/logging/private.h
new file mode 100644
index 00000000000..b80f0488048
--- /dev/null
+++ b/library/cpp/yt/logging/private.h
@@ -0,0 +1,15 @@
+#pragma once
+
+#include "public.h"
+
+namespace NYT::NLogging::NDetail {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Ref-counted memory tag for payload buffers; used for memory accounting.
+struct TMessageBufferTag
+{ };
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NLogging::NDetail
diff --git a/library/cpp/yt/logging/public.h b/library/cpp/yt/logging/public.h
index 1e2b59ca0d3..17f8998f1af 100644
--- a/library/cpp/yt/logging/public.h
+++ b/library/cpp/yt/logging/public.h
@@ -1,6 +1,11 @@
#pragma once
#include <library/cpp/yt/misc/enum.h>
+#include <library/cpp/yt/misc/strong_typedef.h>
+
+#include <library/cpp/yt/memory/ref.h>
+
+#include <variant>
namespace NYT::NLogging {
@@ -36,4 +41,18 @@ struct ILogManager;
////////////////////////////////////////////////////////////////////////////////
+//! Opaque payload of a plain-text log event: a message plus optional key/value tags,
+//! framed by #TTaggedPayloadWriter. Produced by the YT_LOG_*/YT_TLOG_* macros.
+YT_DEFINE_STRONG_TYPEDEF(TTaggedLogEventPayload, TSharedRef);
+
+//! Opaque payload of a structured log event: a raw YSON map fragment.
+//! Produced by #LogStructuredEvent.
+YT_DEFINE_STRONG_TYPEDEF(TStructuredLogEventPayload, TSharedRef);
+
+//! The payload carried by a #TLogEvent: exactly one of the encodings above. The active
+//! alternative both identifies the event kind and determines how the payload is decoded.
+using TLogEventPayload = std::variant<TTaggedLogEventPayload, TStructuredLogEventPayload>;
+
+////////////////////////////////////////////////////////////////////////////////
+
} // namespace NYT::NLogging
diff --git a/library/cpp/yt/logging/structured_payload.cpp b/library/cpp/yt/logging/structured_payload.cpp
new file mode 100644
index 00000000000..1a6ce5ea370
--- /dev/null
+++ b/library/cpp/yt/logging/structured_payload.cpp
@@ -0,0 +1,19 @@
+#include "structured_payload.h"
+
+namespace NYT::NLogging {
+
+////////////////////////////////////////////////////////////////////////////////
+
+TStructuredLogEventPayload MakeStructuredPayloadFromYson(const NYson::TYsonString& message)
+{
+ return TStructuredLogEventPayload(message.ToSharedRef());
+}
+
+NYson::TYsonStringBuf GetYsonFromStructuredPayload(const TStructuredLogEventPayload& payload)
+{
+ return NYson::TYsonStringBuf(payload.Underlying().ToStringBuf(), NYson::EYsonType::MapFragment);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NLogging
diff --git a/library/cpp/yt/logging/structured_payload.h b/library/cpp/yt/logging/structured_payload.h
new file mode 100644
index 00000000000..81464f4aa92
--- /dev/null
+++ b/library/cpp/yt/logging/structured_payload.h
@@ -0,0 +1,25 @@
+#pragma once
+
+#include "public.h"
+
+#include <library/cpp/yt/memory/ref.h>
+
+#include <library/cpp/yt/yson_string/string.h>
+
+namespace NYT::NLogging {
+
+////////////////////////////////////////////////////////////////////////////////
+
+// Structured events carry the raw YSON map fragment as their opaque payload, with no
+// framing. These helpers isolate that representation from callers.
+
+//! Producer: wraps #message into a payload (zero-copy).
+TStructuredLogEventPayload MakeStructuredPayloadFromYson(const NYson::TYsonString& message);
+
+//! Consumer: views the payload as the YSON map fragment it carries. The result views
+//! into #payload, which must outlive it.
+NYson::TYsonStringBuf GetYsonFromStructuredPayload(const TStructuredLogEventPayload& payload);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NLogging
diff --git a/library/cpp/yt/logging/tagged_payload-inl.h b/library/cpp/yt/logging/tagged_payload-inl.h
new file mode 100644
index 00000000000..4b71a5bebef
--- /dev/null
+++ b/library/cpp/yt/logging/tagged_payload-inl.h
@@ -0,0 +1,108 @@
+#ifndef TAGGED_PAYLOAD_INL_H_
+#error "Direct inclusion of this file is not allowed, include tagged_payload.h"
+// For the sake of sane code completion.
+#include "tagged_payload.h"
+#endif
+
+#include <library/cpp/yt/assert/assert.h>
+
+#include <cstring>
+
+namespace NYT::NLogging {
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline TSharedRef TTaggedPayloadBuilder::Flush()
+{
+ return Buffer_.Slice(0, GetLength());
+}
+
+template <class T>
+void TTaggedPayloadBuilder::AppendPod(const T& value)
+{
+ ::memcpy(Preallocate(sizeof(value)), &value, sizeof(value));
+ Advance(sizeof(value));
+}
+
+template <class T>
+void TTaggedPayloadBuilder::WritePodAt(size_t offset, const T& value)
+{
+ YT_ASSERT(offset + sizeof(value) <= GetLength());
+ ::memcpy(Begin_ + offset, &value, sizeof(value));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+inline TStringBuilderBase* TTaggedPayloadWriter::BeginMessage() &
+{
+ YT_ASSERT(!MessageStarted_);
+ MessageStarted_ = true;
+ ReserveLengthPrefix();
+ return &Builder_;
+}
+
+inline TTaggedPayloadWriter& TTaggedPayloadWriter::EndMessage() &
+{
+ YT_ASSERT(MessageStarted_ && !MessageEnded_);
+ MessageEnded_ = true;
+ BackpatchLengthPrefix();
+ return *this;
+}
+
+inline TStringBuilderBase* TTaggedPayloadWriter::BeginTag(TStringBuf key) &
+{
+ return DoBeginTag(key, /*wellKnown*/ false);
+}
+
+inline TStringBuilderBase* TTaggedPayloadWriter::BeginWellKnownTag(TStringBuf key) &
+{
+ return DoBeginTag(key, /*wellKnown*/ true);
+}
+
+inline TStringBuilderBase* TTaggedPayloadWriter::DoBeginTag(TStringBuf key, bool wellKnown)
+{
+ YT_ASSERT(MessageEnded_ && !InTag_);
+ // High bit reserved for the well-known flag.
+ YT_ASSERT(key.size() < WellKnownTagFlag);
+ InTag_ = true;
+ // Reserve the key-size prefix, the key bytes, and the value-size prefix in a single
+ // capacity check; the value is then formatted in place and EndTag backpatches its size.
+ auto keySize = static_cast<ui32>(key.size()) | (wellKnown ? WellKnownTagFlag : 0);
+ char* ptr = Builder_.Preallocate(2 * sizeof(ui32) + key.size());
+ PrefixOffset_ = Builder_.GetLength() + sizeof(ui32) + key.size();
+ ::memcpy(ptr, &keySize, sizeof(keySize));
+ ::memcpy(ptr + sizeof(keySize), key.data(), key.size());
+ // The value-size prefix is left uninitialized; EndTag overwrites it.
+ Builder_.Advance(2 * sizeof(ui32) + key.size());
+ return &Builder_;
+}
+
+inline TTaggedPayloadWriter& TTaggedPayloadWriter::EndTag() &
+{
+ YT_ASSERT(InTag_);
+ InTag_ = false;
+ BackpatchLengthPrefix();
+ return *this;
+}
+
+inline TTaggedLogEventPayload TTaggedPayloadWriter::Finish() &
+{
+ YT_ASSERT(MessageEnded_ && !InTag_);
+ return TTaggedLogEventPayload(Builder_.Flush());
+}
+
+inline void TTaggedPayloadWriter::ReserveLengthPrefix()
+{
+ PrefixOffset_ = Builder_.GetLength();
+ // Reserve a zeroed ui32; backpatched by BackpatchLengthPrefix.
+ Builder_.AppendPod<ui32>(0);
+}
+
+inline void TTaggedPayloadWriter::BackpatchLengthPrefix()
+{
+ Builder_.WritePodAt<ui32>(PrefixOffset_, Builder_.GetLength() - PrefixOffset_ - sizeof(ui32));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NLogging
diff --git a/library/cpp/yt/logging/tagged_payload.cpp b/library/cpp/yt/logging/tagged_payload.cpp
new file mode 100644
index 00000000000..4133cc0bd55
--- /dev/null
+++ b/library/cpp/yt/logging/tagged_payload.cpp
@@ -0,0 +1,214 @@
+#include "tagged_payload.h"
+#include "private.h"
+
+#include <library/cpp/yt/assert/assert.h>
+
+#include <library/cpp/yt/misc/tls.h>
+
+#include <util/system/compiler.h>
+#include <util/system/types.h>
+
+#include <util/generic/bitops.h>
+
+#include <algorithm>
+
+namespace NYT::NLogging {
+
+////////////////////////////////////////////////////////////////////////////////
+
+namespace {
+
+struct TPerThreadCache;
+
+YT_DEFINE_THREAD_LOCAL(TPerThreadCache*, Cache);
+YT_DEFINE_THREAD_LOCAL(bool, CacheDestroyed);
+
+struct TPerThreadCache
+{
+ TSharedMutableRef Chunk;
+ size_t ChunkOffset = 0;
+
+ ~TPerThreadCache()
+ {
+ TTaggedPayloadBuilder::DisablePerThreadCache();
+ }
+
+ static YT_PREVENT_TLS_CACHING TPerThreadCache* GetCache()
+ {
+ auto& cache = Cache();
+ [[likely]] if (cache) {
+ return cache;
+ }
+ if (CacheDestroyed()) {
+ return nullptr;
+ }
+ static thread_local TPerThreadCache CacheData;
+ cache = &CacheData;
+ return cache;
+ }
+};
+
+TSharedMutableRef AllocateChunk(size_t size)
+{
+ return TSharedMutableRef::Allocate<::NYT::NLogging::NDetail::TMessageBufferTag>(size, {.InitializeStorage = false});
+}
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TTaggedPayloadBuilder::DisablePerThreadCache()
+{
+ Cache() = nullptr;
+ CacheDestroyed() = true;
+}
+
+void TTaggedPayloadBuilder::DoReset()
+{
+ Buffer_.Reset();
+}
+
+void TTaggedPayloadBuilder::DoReserve(size_t newCapacity)
+{
+ auto oldLength = GetLength();
+ newCapacity = FastClp2(newCapacity);
+
+ auto newChunkSize = std::max(ChunkSize, newCapacity);
+ // Hold the old buffer until the data is copied.
+ auto oldBuffer = std::move(Buffer_);
+ auto* cache = TPerThreadCache::GetCache();
+ [[likely]] if (cache) {
+ auto oldCapacity = End_ - Begin_;
+ auto deltaCapacity = newCapacity - oldCapacity;
+ if (End_ == cache->Chunk.Begin() + cache->ChunkOffset &&
+ cache->ChunkOffset + deltaCapacity <= cache->Chunk.Size())
+ {
+ // Resize inplace.
+ Buffer_ = cache->Chunk.Slice(cache->ChunkOffset - oldCapacity, cache->ChunkOffset + deltaCapacity);
+ cache->ChunkOffset += deltaCapacity;
+ End_ = Begin_ + newCapacity;
+ return;
+ }
+
+ [[unlikely]] if (cache->ChunkOffset + newCapacity > cache->Chunk.Size()) {
+ cache->Chunk = AllocateChunk(newChunkSize);
+ cache->ChunkOffset = 0;
+ }
+
+ Buffer_ = cache->Chunk.Slice(cache->ChunkOffset, cache->ChunkOffset + newCapacity);
+ cache->ChunkOffset += newCapacity;
+ } else {
+ Buffer_ = AllocateChunk(newChunkSize);
+ newCapacity = newChunkSize;
+ }
+ if (oldLength > 0) {
+ ::memcpy(Buffer_.Begin(), Begin_, oldLength);
+ }
+ Begin_ = Buffer_.Begin();
+ End_ = Begin_ + newCapacity;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+void TTaggedPayloadWriter::DisablePerThreadCache()
+{
+ TTaggedPayloadBuilder::DisablePerThreadCache();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TTaggedPayloadReader::TTaggedPayloadReader(const TTaggedLogEventPayload& payload)
+ : Current_(payload.Underlying().Begin())
+ , End_(payload.Underlying().End())
+{ }
+
+TStringBuf TTaggedPayloadReader::ReadMessage()
+{
+ YT_ASSERT(!MessageRead_);
+ MessageRead_ = true;
+ return ReadString();
+}
+
+std::optional<TTaggedPayloadReader::TTag> TTaggedPayloadReader::TryReadTag()
+{
+ YT_ASSERT(MessageRead_);
+ if (Current_ == End_) {
+ return std::nullopt;
+ }
+ auto keySize = ReadLength();
+ bool wellKnown = (keySize & WellKnownTagFlag) != 0;
+ auto key = ReadBytes(keySize & ~WellKnownTagFlag);
+ auto value = ReadString();
+ return TTag{.IsWellKnown = wellKnown, .Key = key, .Value = value};
+}
+
+ui32 TTaggedPayloadReader::ReadLength()
+{
+ YT_ASSERT(Current_ + sizeof(ui32) <= End_);
+ ui32 size;
+ ::memcpy(&size, Current_, sizeof(size));
+ Current_ += sizeof(size);
+ return size;
+}
+
+TStringBuf TTaggedPayloadReader::ReadBytes(ui32 size)
+{
+ YT_ASSERT(Current_ + size <= End_);
+ TStringBuf result(Current_, size);
+ Current_ += size;
+ return result;
+}
+
+TStringBuf TTaggedPayloadReader::ReadString()
+{
+ return ReadBytes(ReadLength());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TTaggedLogEventPayload MakeTaggedPayloadFromMessage(TStringBuf message)
+{
+ TTaggedPayloadWriter writer;
+ writer.BeginMessage()->AppendString(message);
+ writer.EndMessage();
+ return writer.Finish();
+}
+
+TStringBuf GetMessageFromTaggedPayload(const TTaggedLogEventPayload& payload)
+{
+ return TTaggedPayloadReader(payload).ReadMessage();
+}
+
+std::string FormatTaggedPayload(const TTaggedLogEventPayload& payload)
+{
+ TTaggedPayloadReader reader(payload);
+ std::string result(reader.ReadMessage());
+ // Well-known tags (e.g. an error) are always written last -- the fluent |.With| chain
+ // enforces this at the type level (see #TWellKnownTaggedLoggingGuard) -- so they render
+ // after the |(...)| group on trailing lines in a single pass.
+ bool parenOpen = false;
+ while (auto tag = reader.TryReadTag()) {
+ if (tag->IsWellKnown) {
+ if (parenOpen) {
+ result += ')';
+ parenOpen = false;
+ }
+ result += '\n';
+ result += tag->Value;
+ } else {
+ result += parenOpen ? ", " : " (";
+ parenOpen = true;
+ result += tag->Key;
+ result += ": ";
+ result += tag->Value;
+ }
+ }
+ if (parenOpen) {
+ result += ')';
+ }
+ return result;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NLogging
diff --git a/library/cpp/yt/logging/tagged_payload.h b/library/cpp/yt/logging/tagged_payload.h
new file mode 100644
index 00000000000..da46f30f42a
--- /dev/null
+++ b/library/cpp/yt/logging/tagged_payload.h
@@ -0,0 +1,201 @@
+#pragma once
+
+#include "public.h"
+
+#include <library/cpp/yt/string/string_builder.h>
+
+#include <library/cpp/yt/memory/ref.h>
+
+#include <util/generic/strbuf.h>
+#include <util/generic/size_literals.h>
+
+#include <util/system/types.h>
+
+#include <optional>
+#include <string>
+
+namespace NYT::NLogging {
+
+////////////////////////////////////////////////////////////////////////////////
+
+// Plain-text events carry their message together with structured key/value tags in a
+// flat, length-prefixed payload, deferring tag rendering to the consumer (the logging
+// thread). These helpers own the wire format and isolate it from callers.
+
+//! A per-thread chunk-cached string builder backing payload buffers.
+/*!
+ * Generic: it knows nothing about the payload layout (see #TTaggedPayloadWriter).
+ */
+class TTaggedPayloadBuilder
+ : public TStringBuilderBase
+{
+public:
+ TSharedRef Flush();
+
+ //! Appends a trivially-copyable value as raw bytes (the ui32 length prefix).
+ template <class T>
+ void AppendPod(const T& value);
+
+ //! Overwrites a trivially-copyable value at #offset.
+ //! #offset + sizeof(value) must not exceed the current length.
+ template <class T>
+ void WritePodAt(size_t offset, const T& value);
+
+ //! For testing only.
+ static void DisablePerThreadCache();
+
+private:
+ TSharedMutableRef Buffer_;
+
+ static constexpr size_t ChunkSize = 128_KB - 64;
+
+ void DoReset() override;
+ void DoReserve(size_t newCapacity) override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! A log message together with its tag key/value pairs, serialized into a flat,
+//! opaque byte payload that #TLogEvent hands to the logging thread.
+/*!
+ * Layout (all sizes are host-endian |ui32|; the transport is in-process):
+ * \code
+ * [message size][message bytes]
+ * [key size][key bytes][value size][value bytes] x N
+ * \endcode
+ *
+ * A "well-known" tag sets the high bit of its key-size field; the low 31 bits stay the
+ * key length and the key bytes are still written. Consumers may render such tags
+ * specially (see #GetWellKnownLoggingTag).
+ *
+ * Both the producer (#TTaggedPayloadWriter) and the consumer
+ * (#TTaggedPayloadReader) own this single definition of the layout.
+ */
+
+//! The high bit of a tag's key-size field, marking a well-known tag.
+constexpr ui32 WellKnownTagFlag = 1u << 31;
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Producer side: serializes the message and its tags into a payload.
+/*!
+ * Owns a per-thread chunk-cached buffer; both the message text (#BeginMessage) and
+ * tag values (#BeginTag) are appended through the returned #TStringBuilderBase, so a
+ * formatted value is built directly into the payload without an intermediate
+ * allocation/copy. The length-prefix framing lives entirely here -- the underlying
+ * builder is format-agnostic.
+ */
+class TTaggedPayloadWriter
+{
+public:
+ //! Constructs an empty writer; the message is built incrementally through the
+ //! builder returned by #BeginMessage.
+ TTaggedPayloadWriter() = default;
+
+ TTaggedPayloadWriter(const TTaggedPayloadWriter&) = delete;
+ TTaggedPayloadWriter& operator=(const TTaggedPayloadWriter&) = delete;
+
+ //! Begins the message field and returns a builder for appending its text.
+ //! Must be called exactly once, first.
+ TStringBuilderBase* BeginMessage() &;
+
+ //! Ends the message field, filling in its reserved length prefix. Must be
+ //! called exactly once, after the message text and before any tag/#Finish.
+ TTaggedPayloadWriter& EndMessage() &;
+
+ //! Begins a tag: writes #key, then reserves the value's length prefix and returns
+ //! a builder for appending the value. Lets the value be formatted directly into the
+ //! payload buffer. Pair with #EndTag. Must follow #EndMessage.
+ TStringBuilderBase* BeginTag(TStringBuf key) &;
+
+ //! Like #BeginTag, but marks the tag well-known (sets #WellKnownTagFlag).
+ TStringBuilderBase* BeginWellKnownTag(TStringBuf key) &;
+
+ //! Ends the current tag, filling in the value's reserved length prefix.
+ TTaggedPayloadWriter& EndTag() &;
+
+ //! Returns the serialized payload. Must follow #EndMessage.
+ TTaggedLogEventPayload Finish() &;
+
+ //! For testing only.
+ static void DisablePerThreadCache();
+
+private:
+ TTaggedPayloadBuilder Builder_;
+ bool MessageStarted_ = false;
+ bool MessageEnded_ = false;
+ bool InTag_ = false;
+ //! Offset of the length prefix currently being filled (message or tag value).
+ size_t PrefixOffset_ = 0;
+
+ //! Shared implementation of #BeginTag and #BeginWellKnownTag.
+ TStringBuilderBase* DoBeginTag(TStringBuf key, bool wellKnown);
+ //! Reserves a length prefix at the current position, remembering its offset.
+ void ReserveLengthPrefix();
+ //! Backpatches the reserved prefix with the length appended after it.
+ void BackpatchLengthPrefix();
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Consumer side: parses a payload produced by #TTaggedPayloadWriter.
+/*!
+ * A single-pass cursor; the returned views point into #payload, which must outlive
+ * the reader.
+ *
+ * Call order: #ReadMessage must be called exactly once, first; then #TryReadTag
+ * must be called repeatedly until it returns |std::nullopt|, yielding the tags in
+ * write order.
+ */
+class TTaggedPayloadReader
+{
+public:
+ struct TTag
+ {
+ bool IsWellKnown = false;
+ TStringBuf Key;
+ TStringBuf Value;
+ };
+
+ explicit TTaggedPayloadReader(const TTaggedLogEventPayload& payload);
+
+ //! Reads the message field. Must be called exactly once, before any #TryReadTag.
+ TStringBuf ReadMessage();
+
+ //! Reads the next tag; returns |std::nullopt| once the tags are exhausted.
+ //! Must follow #ReadMessage.
+ std::optional<TTag> TryReadTag();
+
+private:
+ const char* Current_;
+ const char* const End_;
+ bool MessageRead_ = false;
+
+ //! Reads a ui32 length word.
+ ui32 ReadLength();
+ //! Reads #size bytes as a view.
+ TStringBuf ReadBytes(ui32 size);
+ //! Reads a length-prefixed string (a length word followed by its bytes).
+ TStringBuf ReadString();
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Producer convenience: builds a payload carrying only #message and no tags.
+TTaggedLogEventPayload MakeTaggedPayloadFromMessage(TStringBuf message);
+
+//! Consumer convenience: returns the message, disregarding any tags. The result
+//! views into #payload, which must outlive it.
+TStringBuf GetMessageFromTaggedPayload(const TTaggedLogEventPayload& payload);
+
+//! Consumer convenience: renders the payload as |Message (Key: Value, ...)|, or just
+//! the message if it carries no tags.
+std::string FormatTaggedPayload(const TTaggedLogEventPayload& payload);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NLogging
+
+#define TAGGED_PAYLOAD_INL_H_
+#include "tagged_payload-inl.h"
+#undef TAGGED_PAYLOAD_INL_H_
diff --git a/library/cpp/yt/logging/unittests/helpers.cpp b/library/cpp/yt/logging/unittests/helpers.cpp
new file mode 100644
index 00000000000..2a2b20120cc
--- /dev/null
+++ b/library/cpp/yt/logging/unittests/helpers.cpp
@@ -0,0 +1,27 @@
+#include "helpers.h"
+
+namespace NYT::NLogging {
+
+////////////////////////////////////////////////////////////////////////////////
+
+void WriteMessage(TTaggedPayloadWriter* writer, TStringBuf message)
+{
+ writer->BeginMessage()->AppendString(message);
+ writer->EndMessage();
+}
+
+void WriteTag(TTaggedPayloadWriter* writer, TStringBuf key, TStringBuf value)
+{
+ writer->BeginTag(key)->AppendString(value);
+ writer->EndTag();
+}
+
+void WriteWellKnownTag(TTaggedPayloadWriter* writer, TStringBuf key, TStringBuf value)
+{
+ writer->BeginWellKnownTag(key)->AppendString(value);
+ writer->EndTag();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NLogging
diff --git a/library/cpp/yt/logging/unittests/helpers.h b/library/cpp/yt/logging/unittests/helpers.h
new file mode 100644
index 00000000000..6785a65f99b
--- /dev/null
+++ b/library/cpp/yt/logging/unittests/helpers.h
@@ -0,0 +1,22 @@
+#pragma once
+
+#include <library/cpp/yt/logging/tagged_payload.h>
+
+#include <util/generic/strbuf.h>
+
+namespace NYT::NLogging {
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! Writes the message (equivalent to BeginMessage, an append, and EndMessage).
+void WriteMessage(TTaggedPayloadWriter* writer, TStringBuf message);
+
+//! Writes a tag with a ready string value (equivalent to BeginTag, an append, and EndTag).
+void WriteTag(TTaggedPayloadWriter* writer, TStringBuf key, TStringBuf value);
+
+//! Writes a well-known tag with a ready string value (see BeginWellKnownTag).
+void WriteWellKnownTag(TTaggedPayloadWriter* writer, TStringBuf key, TStringBuf value);
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace NYT::NLogging
diff --git a/library/cpp/yt/logging/unittests/logger_ut.cpp b/library/cpp/yt/logging/unittests/logger_ut.cpp
index 410a56fc309..db176ca5394 100644
--- a/library/cpp/yt/logging/unittests/logger_ut.cpp
+++ b/library/cpp/yt/logging/unittests/logger_ut.cpp
@@ -1,15 +1,117 @@
#include <library/cpp/testing/gtest/gtest.h>
#include <library/cpp/yt/logging/logger.h>
+#include <library/cpp/yt/logging/tagged_payload.h>
+#include <library/cpp/yt/logging/structured_payload.h>
#include <library/cpp/yt/error/error.h>
+#include <library/cpp/yt/yson_string/string.h>
+
+#include <atomic>
+#include <string>
+#include <utility>
+#include <vector>
+
namespace NYT::NLogging {
namespace {
+using namespace NDetail;
+
+////////////////////////////////////////////////////////////////////////////////
+
+//! A log manager that captures enqueued events for inspection in tests.
+class TMockLogManager
+ : public ILogManager
+{
+public:
+ explicit TMockLogManager(ELogLevel minLevel = ELogLevel::Minimum)
+ {
+ Category_.MinPlainTextLevel.store(minLevel);
+ }
+
+ void RegisterStaticAnchor(TLoggingAnchor* anchor, ::TSourceLocation /*sourceLocation*/, TStringBuf /*message*/) override
+ {
+ anchor->Registered.store(true);
+ }
+
+ void UpdateAnchor(TLoggingAnchor* anchor) override
+ {
+ anchor->CurrentVersion.store(ActualVersion_.load());
+ }
+
+ void Enqueue(TLogEvent&& event) override
+ {
+ Events_.push_back(std::move(event));
+ }
+
+ const TLoggingCategory* GetCategory(TStringBuf /*categoryName*/) override
+ {
+ return &Category_;
+ }
+
+ void UpdateCategory(TLoggingCategory* category) override
+ {
+ category->CurrentVersion.store(ActualVersion_.load());
+ }
+
+ bool GetAbortOnAlert() const override
+ {
+ return false;
+ }
+
+ const std::vector<TLogEvent>& GetEvents() const
+ {
+ return Events_;
+ }
+
+private:
+ std::vector<TLogEvent> Events_;
+ std::atomic<int> ActualVersion_ = 1;
+ TLoggingCategory Category_ = {
+ .Name = "Test",
+ .MinPlainTextLevel = ELogLevel::Minimum,
+ .CurrentVersion = 0,
+ .ActualVersion = &ActualVersion_,
+ };
+};
+
+struct TDecodedEvent
+{
+ std::string Message;
+ std::vector<std::pair<std::string, std::string>> Tags;
+};
+
+TDecodedEvent DecodeEvent(const TLogEvent& event)
+{
+ TTaggedPayloadReader reader(std::get<TTaggedLogEventPayload>(event.Payload));
+ TDecodedEvent result;
+ result.Message = reader.ReadMessage();
+ while (auto tag = reader.TryReadTag()) {
+ result.Tags.emplace_back(tag->Key, tag->Value);
+ }
+ return result;
+}
+
+TDecodedEvent DecodeSingleEvent(const TMockLogManager& manager)
+{
+ EXPECT_EQ(manager.GetEvents().size(), 1u);
+ return DecodeEvent(manager.GetEvents()[0]);
+}
+
+NYson::TYsonString MakeMapFragment(TStringBuf yson)
+{
+ return NYson::TYsonString(yson, NYson::EYsonType::MapFragment);
+}
+
+TStringBuf GetStructuredYson(const TLogEvent& event)
+{
+ return GetYsonFromStructuredPayload(std::get<TStructuredLogEventPayload>(event.Payload)).AsStringBuf();
+}
+
////////////////////////////////////////////////////////////////////////////////
-TEST(TLogger, NullByDefault)
+TEST(TLoggerTest, NullByDefault)
{
{
TLogger logger;
@@ -23,7 +125,7 @@ TEST(TLogger, NullByDefault)
}
}
-TEST(TLogger, CopyOfNullLogger)
+TEST(TLoggerTest, CopyOfNullLogger)
{
TLogger nullLogger{/*logManager*/ nullptr, "Category"};
ASSERT_FALSE(nullLogger);
@@ -34,7 +136,7 @@ TEST(TLogger, CopyOfNullLogger)
EXPECT_FALSE(logger.IsLevelEnabled(ELogLevel::Fatal));
}
-TEST(TLogger, LogAlertAndThrowMessage)
+TEST(TLoggerTest, LogAlertAndThrowMessage)
{
try {
TLogger Logger;
@@ -52,5 +154,228 @@ TEST(TLogger, LogAlertAndThrowMessage)
////////////////////////////////////////////////////////////////////////////////
+TEST(TTaggedApiTest, MessageOnly)
+{
+ TMockLogManager manager;
+ TLogger Logger(&manager, "Test");
+ YT_TLOG_INFO("Message");
+
+ auto decoded = DecodeSingleEvent(manager);
+ EXPECT_EQ(decoded.Message, "Message");
+ EXPECT_TRUE(decoded.Tags.empty());
+}
+
+TEST(TTaggedApiTest, Tags)
+{
+ TMockLogManager manager;
+ TLogger Logger(&manager, "Test");
+ YT_TLOG_INFO("Message")
+ .With("Arg1", 123)
+ .With("Arg2", "test");
+
+ auto decoded = DecodeSingleEvent(manager);
+ EXPECT_EQ(decoded.Message, "Message");
+ ASSERT_EQ(decoded.Tags.size(), 2u);
+ EXPECT_EQ(decoded.Tags[0], std::pair(std::string("Arg1"), std::string("123")));
+ EXPECT_EQ(decoded.Tags[1], std::pair(std::string("Arg2"), std::string("test")));
+}
+
+TEST(TTaggedApiTest, CustomSpec)
+{
+ TMockLogManager manager;
+ TLogger Logger(&manager, "Test");
+ YT_TLOG_INFO("Message")
+ .With("Arg1", 256, "%x");
+
+ auto decoded = DecodeSingleEvent(manager);
+ ASSERT_EQ(decoded.Tags.size(), 1u);
+ EXPECT_EQ(decoded.Tags[0], std::pair(std::string("Arg1"), std::string("100")));
+}
+
+TEST(TTaggedApiTest, LoggerTagFoldedIntoMessage)
+{
+ TMockLogManager manager;
+ auto Logger = TLogger(&manager, "Test")
+ .WithTag("LoggingTag: %v", 555);
+ YT_TLOG_INFO("Message")
+ .With("Arg1", 123);
+
+ auto decoded = DecodeSingleEvent(manager);
+ EXPECT_EQ(decoded.Message, "Message (LoggingTag: 555)");
+ ASSERT_EQ(decoded.Tags.size(), 1u);
+ EXPECT_EQ(decoded.Tags[0], std::pair(std::string("Arg1"), std::string("123")));
+}
+
+TEST(TTaggedApiTest, DisabledDoesNotEvaluateTags)
+{
+ TMockLogManager manager(/*minLevel*/ ELogLevel::Warning);
+ TLogger Logger(&manager, "Test");
+
+ int evaluated = 0;
+ auto evaluate = [&] {
+ ++evaluated;
+ return 123;
+ };
+ YT_TLOG_INFO("Message")
+ .With("Arg1", evaluate());
+
+ EXPECT_EQ(evaluated, 0);
+ EXPECT_TRUE(manager.GetEvents().empty());
+}
+
+TEST(TTaggedApiTest, WellKnownErrorTag)
+{
+ TMockLogManager manager;
+ TLogger Logger(&manager, "Test");
+ auto error = TError("boom");
+ YT_TLOG_INFO("Message")
+ .With("Tag", 1)
+ .With(error);
+
+ ASSERT_EQ(manager.GetEvents().size(), 1u);
+ TTaggedPayloadReader reader(std::get<TTaggedLogEventPayload>(manager.GetEvents()[0].Payload));
+ EXPECT_EQ(reader.ReadMessage(), "Message");
+
+ auto regular = reader.TryReadTag();
+ ASSERT_TRUE(regular);
+ EXPECT_EQ(regular->Key, "Tag");
+ EXPECT_EQ(regular->Value, "1");
+ EXPECT_FALSE(regular->IsWellKnown);
+
+ // |.With(error)| attaches the error under the well-known "Error" key (resolved via
+ // GetWellKnownLoggingTag), with the formatted error as the value.
+ auto errorTag = reader.TryReadTag();
+ ASSERT_TRUE(errorTag);
+ EXPECT_EQ(errorTag->Key, "Error");
+ EXPECT_TRUE(errorTag->IsWellKnown);
+ EXPECT_NE(errorTag->Value.find("boom"), TStringBuf::npos);
+
+ EXPECT_FALSE(reader.TryReadTag());
+}
+
+// The single-pass formatter assumes well-known tags come last, so the fluent API must
+// forbid a keyed tag after a well-known one (e.g. |.With(error).With("Key", 1)|) at
+// compile time.
+template <class TGuard>
+concept CAllowsKeyedTagAfterWellKnown = requires (TGuard guard, TError error) {
+ guard.With(error).With("Key", 1);
+};
+
+// A further well-known tag after a well-known one stays allowed.
+template <class TGuard>
+concept CAllowsWellKnownTagAfterWellKnown = requires (TGuard guard, TError error) {
+ guard.With(error).With(error);
+};
+
+static_assert(!CAllowsKeyedTagAfterWellKnown<TTaggedLoggingGuard>);
+static_assert(CAllowsWellKnownTagAfterWellKnown<TTaggedLoggingGuard>);
+
+TEST(TTaggedApiTest, AlertAndThrow)
+{
+ TMockLogManager manager;
+ TLogger Logger(&manager, "Test");
+
+ try {
+ YT_TLOG_ALERT_AND_THROW("Alert message")
+ .With("Arg1", 1)
+ .With("Arg2", 2);
+ EXPECT_TRUE(false);
+ } catch (const TErrorException& ex) {
+ const auto& error = ex.Error();
+ EXPECT_EQ(error.GetCode(), NYT::EErrorCode::Fatal);
+ EXPECT_EQ(error.GetMessage(), "Malformed request or incorrect state detected");
+ EXPECT_EQ(error.Attributes().Get<std::string>("message"), "Alert message");
+ }
+
+ // The alert is also logged, carrying the structured tags.
+ auto decoded = DecodeSingleEvent(manager);
+ EXPECT_EQ(decoded.Message, "Alert message");
+ ASSERT_EQ(decoded.Tags.size(), 2u);
+ EXPECT_EQ(decoded.Tags[0], std::pair(std::string("Arg1"), std::string("1")));
+ EXPECT_EQ(decoded.Tags[1], std::pair(std::string("Arg2"), std::string("2")));
+}
+
+TEST(TTaggedApiTest, AlertAndThrowDisabledStillThrows)
+{
+ TMockLogManager manager(/*minLevel*/ ELogLevel::Fatal);
+ TLogger Logger(&manager, "Test");
+
+ // The level is disabled, so nothing is logged, but the exception (with its message)
+ // is still raised.
+ EXPECT_THROW(
+ YT_TLOG_ALERT_AND_THROW("Alert message")
+ .With("Arg1", 1),
+ TErrorException);
+ EXPECT_TRUE(manager.GetEvents().empty());
+}
+
+TEST(TTaggedApiDeathTest, Fatal)
+{
+ EXPECT_DEATH({
+ TMockLogManager manager;
+ TLogger Logger(&manager, "Test");
+ YT_TLOG_FATAL("Fatal message")
+ .With("Arg1", 1);
+ }, "Fatal message \\(Arg1: 1\\)");
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(TStructuredApiTest, StructuredEvent)
+{
+ TMockLogManager manager;
+ TLogger Logger(&manager, "Test");
+
+ LogStructuredEvent(Logger, MakeMapFragment("\"key\"=\"value\""), ELogLevel::Info);
+
+ ASSERT_EQ(manager.GetEvents().size(), 1u);
+ const auto& event = manager.GetEvents()[0];
+ EXPECT_TRUE(std::holds_alternative<TStructuredLogEventPayload>(event.Payload));
+ EXPECT_EQ(event.Family, ELogFamily::Structured);
+ EXPECT_EQ(event.Level, ELogLevel::Info);
+ // The payload is the raw YSON map fragment.
+ EXPECT_EQ(GetStructuredYson(event), "\"key\"=\"value\"");
+}
+
+TEST(TStructuredApiTest, PreservesLevel)
+{
+ TMockLogManager manager;
+ TLogger Logger(&manager, "Test");
+
+ LogStructuredEvent(Logger, MakeMapFragment("\"a\"=1;\"b\"=2"), ELogLevel::Warning);
+
+ ASSERT_EQ(manager.GetEvents().size(), 1u);
+ EXPECT_EQ(manager.GetEvents()[0].Level, ELogLevel::Warning);
+ EXPECT_EQ(GetStructuredYson(manager.GetEvents()[0]), "\"a\"=1;\"b\"=2");
+}
+
+TEST(TStructuredApiTest, EmptyFragment)
+{
+ TMockLogManager manager;
+ TLogger Logger(&manager, "Test");
+
+ LogStructuredEvent(Logger, MakeMapFragment(""), ELogLevel::Info);
+
+ ASSERT_EQ(manager.GetEvents().size(), 1u);
+ EXPECT_EQ(GetStructuredYson(manager.GetEvents()[0]), "");
+}
+
+TEST(TStructuredApiTest, MultipleEvents)
+{
+ TMockLogManager manager;
+ TLogger Logger(&manager, "Test");
+
+ LogStructuredEvent(Logger, MakeMapFragment("\"i\"=0"), ELogLevel::Debug);
+ LogStructuredEvent(Logger, MakeMapFragment("\"i\"=1"), ELogLevel::Info);
+
+ ASSERT_EQ(manager.GetEvents().size(), 2u);
+ EXPECT_EQ(manager.GetEvents()[0].Level, ELogLevel::Debug);
+ EXPECT_EQ(GetStructuredYson(manager.GetEvents()[0]), "\"i\"=0");
+ EXPECT_EQ(manager.GetEvents()[1].Level, ELogLevel::Info);
+ EXPECT_EQ(GetStructuredYson(manager.GetEvents()[1]), "\"i\"=1");
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
} // namespace
} // namespace NYT::NLogging
diff --git a/library/cpp/yt/logging/unittests/structured_payload_ut.cpp b/library/cpp/yt/logging/unittests/structured_payload_ut.cpp
new file mode 100644
index 00000000000..f15970dcb23
--- /dev/null
+++ b/library/cpp/yt/logging/unittests/structured_payload_ut.cpp
@@ -0,0 +1,36 @@
+#include <library/cpp/testing/gtest/gtest.h>
+
+#include <library/cpp/yt/logging/structured_payload.h>
+
+#include <library/cpp/yt/yson_string/string.h>
+
+namespace NYT::NLogging {
+namespace {
+
+using namespace NYson;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TYsonString MakeMapFragment(TStringBuf yson)
+{
+ return TYsonString(yson, EYsonType::MapFragment);
+}
+
+TEST(TStructuredPayloadTest, RoundTrip)
+{
+ auto payload = MakeStructuredPayloadFromYson(MakeMapFragment("\"key\"=\"value\""));
+ auto view = GetYsonFromStructuredPayload(payload);
+ EXPECT_EQ(view.GetType(), EYsonType::MapFragment);
+ EXPECT_EQ(view.AsStringBuf(), "\"key\"=\"value\"");
+}
+
+TEST(TStructuredPayloadTest, EmptyFragment)
+{
+ auto payload = MakeStructuredPayloadFromYson(MakeMapFragment(""));
+ EXPECT_EQ(GetYsonFromStructuredPayload(payload).AsStringBuf(), "");
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NLogging
diff --git a/library/cpp/yt/logging/unittests/tagged_payload_ut.cpp b/library/cpp/yt/logging/unittests/tagged_payload_ut.cpp
new file mode 100644
index 00000000000..57d9ac168f4
--- /dev/null
+++ b/library/cpp/yt/logging/unittests/tagged_payload_ut.cpp
@@ -0,0 +1,146 @@
+#include "helpers.h"
+
+#include <library/cpp/testing/gtest/gtest.h>
+
+#include <library/cpp/yt/logging/tagged_payload.h>
+
+#include <string>
+#include <vector>
+
+namespace NYT::NLogging {
+namespace {
+
+using namespace NDetail;
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct TDecodedPayload
+{
+ std::string Message;
+ std::vector<std::pair<std::string, std::string>> Tags;
+};
+
+TTaggedLogEventPayload Encode(TStringBuf message, const std::vector<std::pair<TStringBuf, TStringBuf>>& tags = {})
+{
+ TTaggedPayloadWriter writer;
+ WriteMessage(&writer, message);
+ for (auto [key, value] : tags) {
+ WriteTag(&writer, key, value);
+ }
+ return writer.Finish();
+}
+
+TDecodedPayload Decode(const TTaggedLogEventPayload& payload)
+{
+ TTaggedPayloadReader reader(payload);
+ TDecodedPayload result;
+ result.Message = reader.ReadMessage();
+ while (auto tag = reader.TryReadTag()) {
+ result.Tags.emplace_back(tag->Key, tag->Value);
+ }
+ return result;
+}
+
+TEST(TTaggedPayloadTest, MessageOnly)
+{
+ auto decoded = Decode(Encode("Hello"));
+ EXPECT_EQ(decoded.Message, "Hello");
+ EXPECT_TRUE(decoded.Tags.empty());
+}
+
+TEST(TTaggedPayloadTest, EmptyMessage)
+{
+ auto decoded = Decode(Encode(""));
+ EXPECT_EQ(decoded.Message, "");
+ EXPECT_TRUE(decoded.Tags.empty());
+}
+
+TEST(TTaggedPayloadTest, OneTag)
+{
+ auto decoded = Decode(Encode("Message", {{"Key", "Value"}}));
+ EXPECT_EQ(decoded.Message, "Message");
+ ASSERT_EQ(decoded.Tags.size(), 1u);
+ EXPECT_EQ(decoded.Tags[0].first, "Key");
+ EXPECT_EQ(decoded.Tags[0].second, "Value");
+}
+
+TEST(TTaggedPayloadTest, ManyTags)
+{
+ auto decoded = Decode(Encode("Message", {{"Arg1", "123"}, {"Arg2", "test"}, {"Arg3", ""}}));
+ EXPECT_EQ(decoded.Message, "Message");
+ ASSERT_EQ(decoded.Tags.size(), 3u);
+ EXPECT_EQ(decoded.Tags[0], std::pair(std::string("Arg1"), std::string("123")));
+ EXPECT_EQ(decoded.Tags[1], std::pair(std::string("Arg2"), std::string("test")));
+ EXPECT_EQ(decoded.Tags[2], std::pair(std::string("Arg3"), std::string("")));
+}
+
+TEST(TTaggedPayloadTest, BinarySafeValues)
+{
+ // Keys/values may carry arbitrary bytes, including embedded NULs and delimiters.
+ std::string message("a\0b ()", 6);
+ std::string key("k\0:", 3);
+ std::string value("v\0, ", 4);
+
+ auto decoded = Decode(Encode(message, {{key, value}}));
+ EXPECT_EQ(decoded.Message, message);
+ ASSERT_EQ(decoded.Tags.size(), 1u);
+ EXPECT_EQ(decoded.Tags[0].first, key);
+ EXPECT_EQ(decoded.Tags[0].second, value);
+}
+
+TEST(TTaggedPayloadTest, ReaderViewsPointIntoPayload)
+{
+ auto payload = Encode("Message", {{"Key", "Value"}});
+
+ auto pointsInto = [&] (TStringBuf view) {
+ return view.data() >= payload.Underlying().Begin() && view.data() + view.size() <= payload.Underlying().End();
+ };
+
+ TTaggedPayloadReader reader(payload);
+ EXPECT_TRUE(pointsInto(reader.ReadMessage()));
+ auto tag = reader.TryReadTag();
+ ASSERT_TRUE(tag.has_value());
+ EXPECT_TRUE(pointsInto(tag->Key));
+ EXPECT_TRUE(pointsInto(tag->Value));
+}
+
+TEST(TTaggedPayloadTest, WellKnownTag)
+{
+ TTaggedPayloadWriter writer;
+ WriteMessage(&writer, "Message");
+ WriteTag(&writer, "Key", "Value");
+ WriteWellKnownTag(&writer, "Error", "boom");
+ auto payload = writer.Finish();
+
+ TTaggedPayloadReader reader(payload);
+ EXPECT_EQ(reader.ReadMessage(), "Message");
+
+ auto regular = reader.TryReadTag();
+ ASSERT_TRUE(regular);
+ EXPECT_EQ(regular->Key, "Key");
+ EXPECT_EQ(regular->Value, "Value");
+ EXPECT_FALSE(regular->IsWellKnown);
+
+ auto wellKnown = reader.TryReadTag();
+ ASSERT_TRUE(wellKnown);
+ EXPECT_EQ(wellKnown->Key, "Error");
+ EXPECT_EQ(wellKnown->Value, "boom");
+ EXPECT_TRUE(wellKnown->IsWellKnown);
+
+ EXPECT_FALSE(reader.TryReadTag());
+}
+
+TEST(TTaggedPayloadTest, FormatWellKnownTagTrailing)
+{
+ TTaggedPayloadWriter writer;
+ WriteMessage(&writer, "Message");
+ WriteTag(&writer, "Key", "Value");
+ WriteWellKnownTag(&writer, "Error", "boom");
+ // Regular tags stay inline; the well-known tag is appended after the |(...)| group.
+ EXPECT_EQ(FormatTaggedPayload(writer.Finish()), "Message (Key: Value)\nboom");
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+} // namespace
+} // namespace NYT::NLogging
diff --git a/library/cpp/yt/logging/unittests/ya.make b/library/cpp/yt/logging/unittests/ya.make
index 01946699bb2..cc068fa24c6 100644
--- a/library/cpp/yt/logging/unittests/ya.make
+++ b/library/cpp/yt/logging/unittests/ya.make
@@ -3,7 +3,10 @@ GTEST(unittester-library-logging)
INCLUDE(${ARCADIA_ROOT}/library/cpp/yt/ya_cpp.make.inc)
SRCS(
+ helpers.cpp
logger_ut.cpp
+ tagged_payload_ut.cpp
+ structured_payload_ut.cpp
static_analysis_ut.cpp
)
diff --git a/library/cpp/yt/logging/ya.make b/library/cpp/yt/logging/ya.make
index b9cf5c6e715..d839a6f07b7 100644
--- a/library/cpp/yt/logging/ya.make
+++ b/library/cpp/yt/logging/ya.make
@@ -4,6 +4,8 @@ INCLUDE(${ARCADIA_ROOT}/library/cpp/yt/ya_cpp.make.inc)
SRCS(
logger.cpp
+ tagged_payload.cpp
+ structured_payload.cpp
)
PEERDIR(
diff --git a/ya b/ya
index d9af68c5939..216a17f02e1 100755
--- a/ya
+++ b/ya
@@ -46,33 +46,33 @@ REGISTRY_ENDPOINT = os.environ.get("YA_REGISTRY_ENDPOINT", "https://devtools-reg
PLATFORM_MAP = {
"data": {
"win32": {
- "md5": "0080c0947dc0ddf9b20ca50821b32356",
+ "md5": "9dee86473a0e4b0c0a0d963f419dbf33",
"urls": [
- f"{REGISTRY_ENDPOINT}/12778691402"
+ f"{REGISTRY_ENDPOINT}/12807581301"
]
},
"darwin": {
- "md5": "257864a1bc4ab2f0beef69bb19c5944f",
+ "md5": "e5067a1d83c2dd418be0faf32c2ab354",
"urls": [
- f"{REGISTRY_ENDPOINT}/12778688709"
+ f"{REGISTRY_ENDPOINT}/12807578337"
]
},
"darwin-arm64": {
- "md5": "8166643048c1c44d3b172d57f9f558b4",
+ "md5": "732315791641f6d8dec06aad3ac70427",
"urls": [
- f"{REGISTRY_ENDPOINT}/12778685977"
+ f"{REGISTRY_ENDPOINT}/12807575686"
]
},
"linux-aarch64": {
- "md5": "0cb28f3d593d8e7751db41688da47d48",
+ "md5": "9ac4edc8c328aff5bbb941668634ab70",
"urls": [
- f"{REGISTRY_ENDPOINT}/12778683580"
+ f"{REGISTRY_ENDPOINT}/12807572366"
]
},
"linux": {
- "md5": "eada2433c6c25c0096f9c77433a82fdf",
+ "md5": "01654ceac32bcf11ad928ce31db2cc42",
"urls": [
- f"{REGISTRY_ENDPOINT}/12778694423"
+ f"{REGISTRY_ENDPOINT}/12807584185"
]
}
}
diff --git a/yql/essentials/ast/yql_constraint.cpp b/yql/essentials/ast/yql_constraint.cpp
index 0946468b2a5..7fe88ae991a 100644
--- a/yql/essentials/ast/yql_constraint.cpp
+++ b/yql/essentials/ast/yql_constraint.cpp
@@ -2026,7 +2026,7 @@ NYT::TNode TStreamingConstraintNode::ToYson() const {
}
bool TStreamingConstraintNode::IsApplicableToType(const TTypeAnnotationNode& type) const {
- return IsIn({ETypeAnnotationKind::List, ETypeAnnotationKind::Stream, ETypeAnnotationKind::Flow}, type.GetKind());
+ return IsIn({ETypeAnnotationKind::Tuple, ETypeAnnotationKind::List, ETypeAnnotationKind::Stream, ETypeAnnotationKind::Flow}, type.GetKind());
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
diff --git a/yql/essentials/core/ut/yql_expr_constraint_ut.cpp b/yql/essentials/core/ut/yql_expr_constraint_ut.cpp
index 3b23e8d8385..cbbcf49d481 100644
--- a/yql/essentials/core/ut/yql_expr_constraint_ut.cpp
+++ b/yql/essentials/core/ut/yql_expr_constraint_ut.cpp
@@ -4199,6 +4199,79 @@ Y_UNIT_TEST(StreamingConstraintShuffleByKeys) {
}
}
+Y_UNIT_TEST(DirectStreamingConstraintWithSwitch) {
+ const auto s = R"((
+(let res (DataSink 'result))
+
+(let list1 (AsList
+ (AsStruct '('key (String '4)) '('subkey (String 'c)) '('value (String 'v)))
+ (AsStruct '('key (String '1)) '('subkey (String 'd)) '('value (String 'v)))
+ (AsStruct '('key (String '3)) '('subkey (String 'b)) '('value (String 'v)))
+))
+
+(let list2 (AsList
+ (AsStruct '('key (String '2)) '('subkey (String 'c)) '('value (String 'v)))
+ (AsStruct '('key (String '5)) '('subkey (String 'd)) '('value (String 'v)))
+ (AsStruct '('key (String '4)) '('subkey (String 'b)) '('value (String 'v)))
+))
+
+(let data (Mux '(list1 list2)))
+(let data (AssumeConstraints data '"{\"Streaming\" = #}"))
+
+(let data (Switch (Iterator data) '0 '('0) (lambda '(s) (FlatMap s (lambda '(item) (Just item)))) '('1) (lambda '(s) s)))
+
+(let result (Nth (Demux data) '1))
+
+(let world (Write! world res (Key) (Collect result) '()))
+(let world (Commit! world res))
+(return world)
+))";
+
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TStreamingConstraintNode>(exprRoot, "Switch", "Streaming");
+ CheckConstraint<TStreamingConstraintNode>(exprRoot, "FlatMap", "Streaming");
+ CheckConstraint<TStreamingConstraintNode>(exprRoot, "Demux", "Streaming");
+ CheckConstraint<TStreamingConstraintNode>(exprRoot, "Nth", "Streaming");
+}
+
+Y_UNIT_TEST(MultiItemStreamingConstraintWithSwitch) {
+ const auto s = R"((
+(let res (DataSink 'result))
+
+(let list1 (AsList
+ (AsStruct '('key (String '4)) '('subkey (String 'c)) '('value (String 'v)))
+ (AsStruct '('key (String '1)) '('subkey (String 'd)) '('value (String 'v)))
+ (AsStruct '('key (String '3)) '('subkey (String 'b)) '('value (String 'v)))
+))
+
+(let list2 (AsList
+ (AsStruct '('key (String '2)) '('subkey (String 'c)) '('value (String 'v)))
+ (AsStruct '('key (String '5)) '('subkey (String 'd)) '('value (String 'v)))
+ (AsStruct '('key (String '4)) '('subkey (String 'b)) '('value (String 'v)))
+))
+(let list2 (AssumeConstraints list2 '"{\"Streaming\" = #}"))
+
+(let data (Mux '(list1 list2)))
+
+(let data (Switch (Iterator data) '0 '('0) (lambda '(s) (FlatMap s (lambda '(item) (Just item)))) '('1) (lambda '(s) s)))
+
+(let result (Nth (Demux data) '1))
+
+(let world (Write! world res (Key) (Collect result) '()))
+(let world (Commit! world res))
+(return world)
+))";
+
+ TExprContext exprCtx;
+ const auto exprRoot = ParseAndAnnotate(s, exprCtx);
+ CheckConstraint<TStreamingConstraintNode>(exprRoot, "Mux", "Streaming");
+ CheckConstraint<TStreamingConstraintNode>(exprRoot, "Switch", "Streaming");
+ CheckConstraint<TStreamingConstraintNode>(exprRoot, "FlatMap", "Streaming");
+ CheckConstraint<TStreamingConstraintNode>(exprRoot, "Demux", "Streaming");
+ CheckConstraint<TStreamingConstraintNode>(exprRoot, "Nth", "Streaming");
+}
+
} // Y_UNIT_TEST_SUITE(TYqlExprConstraints)
} // namespace NYql
diff --git a/yql/essentials/core/yql_expr_constraint.cpp b/yql/essentials/core/yql_expr_constraint.cpp
index 864f46e248d..eb8a9694f63 100644
--- a/yql/essentials/core/yql_expr_constraint.cpp
+++ b/yql/essentials/core/yql_expr_constraint.cpp
@@ -207,6 +207,7 @@ public:
Functions_["DynamicVariant"] = &TCallableConstraintTransformer::DynamicVariantWrap;
Functions_["Guess"] = &TCallableConstraintTransformer::GuessWrap;
Functions_["Mux"] = &TCallableConstraintTransformer::MuxWrap;
+ Functions_["Demux"] = &TCallableConstraintTransformer::FromFirst<TStreamingConstraintNode>;
Functions_["Nth"] = &TCallableConstraintTransformer::NthWrap;
Functions_["EquiJoin"] = &TCallableConstraintTransformer::EquiJoinWrap;
Functions_["JoinDict"] = &TCallableConstraintTransformer::JoinDictWrap;
@@ -1844,6 +1845,8 @@ private:
TStatus SwitchWrap(const TExprNode::TPtr& input, TExprNode::TPtr& output, TExprContext& ctx) const {
TStatus status = TStatus::Ok;
TDynBitMap outFromChildren; // children, from which take a multi constraint for output
+ const auto* inputStreaming = input->Head().GetConstraint<TStreamingConstraintNode>();
+
if (const auto multi = input->Head().GetConstraint<TMultiConstraintNode>()) {
for (size_t i = 2; i < input->ChildrenSize(); ++i) {
TMultiConstraintNode::TMapType items;
@@ -1861,18 +1864,33 @@ private:
if (input->Child(i)->ChildrenSize() > 1) {
argConstraints.push_back(ctx.MakeConstraint<TMultiConstraintNode>(std::move(items)));
argConstraints.push_back(ctx.MakeConstraint<TVarIndexConstraintNode>(input->Child(i)->ChildrenSize()));
+
+ if (inputStreaming) {
+ argConstraints.push_back(inputStreaming);
+ }
} else {
argConstraints = items.front().second.GetAllConstraints();
+
+ if (inputStreaming && !items.front().second.GetConstraint<TStreamingConstraintNode>()) {
+ argConstraints.push_back(inputStreaming);
+ }
}
+ } else if (inputStreaming) {
+ argConstraints.push_back(inputStreaming);
}
status = status.Combine(UpdateLambdaConstraints(input->ChildRef(++i), ctx, {argConstraints}));
}
} else {
- const bool inVar = GetSeqItemType(*input->Head().GetTypeAnn()).GetKind() == ETypeAnnotationKind::Variant;
- const TSmallVec<TConstraintNode::TListType> argConstraints(1U, inVar ? TConstraintNode::TListType() : input->Head().GetAllConstraints());
+ TConstraintNode::TListType argConstraintsList;
+ if (GetSeqItemType(*input->Head().GetTypeAnn()).GetKind() != ETypeAnnotationKind::Variant) {
+ argConstraintsList = input->Head().GetAllConstraints();
+ } else if (inputStreaming) {
+ argConstraintsList.push_back(inputStreaming);
+ }
+
for (size_t i = 3; i < input->ChildrenSize(); i += 2) {
- status = status.Combine(UpdateLambdaConstraints(input->ChildRef(i), ctx, argConstraints));
+ status = status.Combine(UpdateLambdaConstraints(input->ChildRef(i), ctx, {argConstraintsList}));
}
outFromChildren.Set(0, input->ChildrenSize());
}
@@ -1883,6 +1901,7 @@ private:
const auto inputVarIndex = input->Head().GetConstraint<TVarIndexConstraintNode>();
const bool emptyInput = input->Head().GetConstraint<TEmptyConstraintNode>();
+ bool hasStreamingOutput = false;
if (GetSeqItemType(*input->GetTypeAnn()).GetKind() == ETypeAnnotationKind::Variant) {
ui32 outIndexOffset = 0;
TMultiConstraintNode::TMapType multiItems;
@@ -1948,6 +1967,9 @@ private:
}
if (!multiItems.empty()) {
+ hasStreamingOutput = std::any_of(multiItems.begin(), multiItems.end(), [](const auto& item) {
+ return !!item.second.template GetConstraint<TStreamingConstraintNode>();
+ });
input->AddConstraint(ctx.MakeConstraint<TMultiConstraintNode>(std::move(multiItems)));
}
if (emptyOut) {
@@ -1957,6 +1979,11 @@ private:
YQL_ENSURE(input->ChildrenSize() == 4);
input->CopyConstraints(*input->Child(3));
}
+
+ if (hasStreamingOutput || inputStreaming) {
+ input->AddConstraint(ctx.MakeConstraint<TStreamingConstraintNode>());
+ }
+
return FromFirst<TEmptyConstraintNode>(input, output, ctx);
}
@@ -2234,19 +2261,26 @@ private:
TMultiConstraintNode::TMapType items;
ui32 index = 0;
ui32 emptyCount = 0;
+ bool hasStreaming = false;
for (auto& child: input->Head().Children()) {
if (!child->GetConstraint<TEmptyConstraintNode>()) {
items.push_back(std::make_pair(index, child->GetConstraintSet()));
+ hasStreaming = hasStreaming || !!child->GetConstraint<TStreamingConstraintNode>();
} else {
++emptyCount;
}
++index;
}
+
if (!items.empty()) {
input->AddConstraint(ctx.MakeConstraint<TMultiConstraintNode>(std::move(items)));
} else if (index == emptyCount) {
input->AddConstraint(ctx.MakeConstraint<TEmptyConstraintNode>());
}
+
+ if (hasStreaming) {
+ input->AddConstraint(ctx.MakeConstraint<TStreamingConstraintNode>());
+ }
}
}
return TStatus::Ok;
@@ -2289,6 +2323,10 @@ private:
input->SetConstraints(*c);
}
}
+
+ if (const auto* c = input->Head().GetConstraint<TStreamingConstraintNode>()) {
+ input->AddConstraint(c);
+ }
}
return TStatus::Ok;
}
diff --git a/yql/essentials/public/purecalc/io_specs/protobuf/no_llvm/ya.make b/yql/essentials/public/purecalc/io_specs/protobuf/no_llvm/ya.make
new file mode 100644
index 00000000000..76ed1dc0196
--- /dev/null
+++ b/yql/essentials/public/purecalc/io_specs/protobuf/no_llvm/ya.make
@@ -0,0 +1,23 @@
+LIBRARY()
+
+ADDINCL(
+ yql/essentials/public/purecalc/io_specs/protobuf
+)
+
+SRCDIR(
+ yql/essentials/public/purecalc/io_specs/protobuf
+)
+
+SRCS(
+ spec.cpp
+ proto_variant.cpp
+)
+
+PEERDIR(
+ yql/essentials/public/purecalc/common/no_llvm
+ yql/essentials/public/purecalc/io_specs/protobuf_raw/no_llvm
+)
+
+YQL_LAST_ABI_VERSION()
+
+END()
diff --git a/yql/essentials/public/purecalc/io_specs/protobuf/ya.make b/yql/essentials/public/purecalc/io_specs/protobuf/ya.make
index b9441ceecf4..a7cca185da2 100644
--- a/yql/essentials/public/purecalc/io_specs/protobuf/ya.make
+++ b/yql/essentials/public/purecalc/io_specs/protobuf/ya.make
@@ -14,6 +14,10 @@ YQL_LAST_ABI_VERSION()
END()
+RECURSE(
+ no_llvm
+)
+
RECURSE_FOR_TESTS(
ut
)
diff --git a/yql/essentials/public/purecalc/io_specs/protobuf_raw/no_llvm/ya.make b/yql/essentials/public/purecalc/io_specs/protobuf_raw/no_llvm/ya.make
new file mode 100644
index 00000000000..f89c7800864
--- /dev/null
+++ b/yql/essentials/public/purecalc/io_specs/protobuf_raw/no_llvm/ya.make
@@ -0,0 +1,23 @@
+LIBRARY()
+
+ADDINCL(
+ yql/essentials/public/purecalc/io_specs/protobuf_raw
+)
+
+SRCDIR(
+ yql/essentials/public/purecalc/io_specs/protobuf_raw
+)
+
+SRCS(
+ proto_holder.cpp
+ spec.cpp
+)
+
+PEERDIR(
+ yql/essentials/public/purecalc/common/no_llvm
+ yql/essentials/public/purecalc/helpers/protobuf
+)
+
+YQL_LAST_ABI_VERSION()
+
+END()
diff --git a/yql/essentials/public/purecalc/io_specs/protobuf_raw/ya.make b/yql/essentials/public/purecalc/io_specs/protobuf_raw/ya.make
index db3fab7e7a5..4faaf4d2de1 100644
--- a/yql/essentials/public/purecalc/io_specs/protobuf_raw/ya.make
+++ b/yql/essentials/public/purecalc/io_specs/protobuf_raw/ya.make
@@ -14,3 +14,7 @@ SRCS(
YQL_LAST_ABI_VERSION()
END()
+
+RECURSE(
+ no_llvm
+)
diff --git a/yql/essentials/sql/v1/SQLv1Antlr4.g.in b/yql/essentials/sql/v1/SQLv1Antlr4.g.in
index d1eba59441a..54ffbdeec54 100644
--- a/yql/essentials/sql/v1/SQLv1Antlr4.g.in
+++ b/yql/essentials/sql/v1/SQLv1Antlr4.g.in
@@ -882,6 +882,7 @@ column_option:
| default_value
| compression
| encoding
+ | generated_always
;
compression: COMPRESSION LPAREN (compression_setting_entry (COMMA compression_setting_entry)*)? COMMA? RPAREN;
@@ -896,6 +897,7 @@ encoding_config: encoding_config_name (LPAREN (encoding_setting_entry (COMMA enc
encoding_config_name: an_id_or_type;
encoding_setting_entry: an_id EQUALS encoding_setting_value;
encoding_setting_value: integer | id;
+generated_always: (GENERATED ALWAYS)? AS LPAREN expr RPAREN (STORED | VIRTUAL)?;
column_order_by_specification: an_id (ASC | DESC)?;
@@ -1476,6 +1478,7 @@ keyword_as_compat:
| ADD
| AFTER
| ALTER
+ | ALWAYS
| ANALYZE
| AND
| ANSI
@@ -1551,6 +1554,7 @@ keyword_as_compat:
| FOR
| FOREIGN
| FUNCTION
+ | GENERATED
| GLOB
| GLOBAL
| GRANT
@@ -1669,6 +1673,7 @@ keyword_as_compat:
| SOURCE
| START
| STATISTICS
+ | STORED
| STREAMING
| SUBQUERY
| SUBSET
@@ -1717,6 +1722,7 @@ keyword_compat: (
| ADD
| AFTER
| ALTER
+ | ALWAYS
| ANALYZE
| AND
| ANSI
@@ -1792,6 +1798,7 @@ keyword_compat: (
| FOR
| FOREIGN
| FUNCTION
+ | GENERATED
| GLOB
| GLOBAL
| GRANT
@@ -1910,6 +1917,7 @@ keyword_compat: (
| SOURCE
| START
| STATISTICS
+ | STORED
| STREAMING
| SUBQUERY
| SUBSET
@@ -2055,6 +2063,7 @@ ADD: A D D;
AFTER: A F T E R;
ALL: A L L;
ALTER: A L T E R;
+ALWAYS: A L W A Y S;
ANALYZE: A N A L Y Z E;
AND: A N D;
ANSI: A N S I;
@@ -2159,6 +2168,7 @@ FOREIGN: F O R E I G N;
FROM: F R O M;
FULL: F U L L;
FUNCTION: F U N C T I O N;
+GENERATED: G E N E R A T E D;
GLOB: G L O B;
GLOBAL: G L O B A L;
GRANT: G R A N T;
@@ -2300,6 +2310,7 @@ SECRET: S E C R E T;
SEQUENCE: S E Q U E N C E;
SOURCE: S O U R C E;
START: S T A R T;
+STORED: S T O R E D;
STATISTICS: S T A T I S T I C S;
STREAM: S T R E A M;
STREAMING: S T R E A M I N G;
diff --git a/yql/essentials/sql/v1/format/sql_format_ut.h b/yql/essentials/sql/v1/format/sql_format_ut.h
index ce5d448c9dd..6406608c26b 100644
--- a/yql/essentials/sql/v1/format/sql_format_ut.h
+++ b/yql/essentials/sql/v1/format/sql_format_ut.h
@@ -432,6 +432,20 @@ Y_UNIT_TEST(CreateTable) {
{"create table\tuser(key int32, val String encoding(off))", "CREATE TABLE user (\n\tkey int32,\n\tval String ENCODING (off)\n);\n"},
{"create table\tuser(key int32, val String encoding())", "CREATE TABLE user (\n\tkey int32,\n\tval String ENCODING ()\n);\n"},
{"create table user(key int32, val String encoding(dict(max_size=100)))", "CREATE TABLE user (\n\tkey int32,\n\tval String ENCODING (dict (max_size = 100))\n);\n"},
+ {"create table user(key int32, val int64 generated always as (key+1) stored)",
+ "CREATE TABLE user (\n\tkey int32,\n\tval int64 GENERATED ALWAYS AS (key + 1) STORED\n);\n"},
+ {"create table user(key int32, val int64 generated always as (key+1) virtual)",
+ "CREATE TABLE user (\n\tkey int32,\n\tval int64 GENERATED ALWAYS AS (key + 1) VIRTUAL\n);\n"},
+ {"create table user(key int32, val int64 generated always as (key+1))",
+ "CREATE TABLE user (\n\tkey int32,\n\tval int64 GENERATED ALWAYS AS (key + 1)\n);\n"},
+ {"create table user(key int32, val int64 as (key+1) stored)",
+ "CREATE TABLE user (\n\tkey int32,\n\tval int64 AS (key + 1) STORED\n);\n"},
+ {"create table user(key int32, val int64 as (key+1))",
+ "CREATE TABLE user (\n\tkey int32,\n\tval int64 AS (key + 1)\n);\n"},
+ {"create table user(key int32, val int64 GeNeRaTeD AlWaYs As (key+1) StOrEd)",
+ "CREATE TABLE user (\n\tkey int32,\n\tval int64 GENERATED ALWAYS AS (key + 1) STORED\n);\n"},
+ {"create table user(key int32, val int64 (not null, generated always as (key+1) stored))",
+ "CREATE TABLE user (\n\tkey int32,\n\tval int64 (NOT NULL, GENERATED ALWAYS AS (key + 1) STORED)\n);\n"},
};
TSetup setup;
@@ -691,6 +705,18 @@ Y_UNIT_TEST(AlterTable) {
"ALTER TABLE t\n\tALTER COLUMN c SET ENCODING ()\n;\n"},
{"alter table t alter column c set encoding(dict(max_size=100))",
"ALTER TABLE t\n\tALTER COLUMN c SET ENCODING (dict (max_size = 100))\n;\n"},
+ {"alter table user add column val int64 generated always as (key+1) stored",
+ "ALTER TABLE user\n\tADD COLUMN val int64 GENERATED ALWAYS AS (key + 1) STORED\n;\n"},
+ {"alter table user add column val int64 generated always as (key+1) virtual",
+ "ALTER TABLE user\n\tADD COLUMN val int64 GENERATED ALWAYS AS (key + 1) VIRTUAL\n;\n"},
+ {"alter table user add column val int64 generated always as (key+1)",
+ "ALTER TABLE user\n\tADD COLUMN val int64 GENERATED ALWAYS AS (key + 1)\n;\n"},
+ {"alter table user add column val int64 as (key+1) stored",
+ "ALTER TABLE user\n\tADD COLUMN val int64 AS (key + 1) STORED\n;\n"},
+ {"alter table user add val int64 GeNeRaTeD AlWaYs As (key+1) StOrEd",
+ "ALTER TABLE user\n\tADD val int64 GENERATED ALWAYS AS (key + 1) STORED\n;\n"},
+ {"alter table user add column val int64 (not null, generated always as (key+1) stored)",
+ "ALTER TABLE user\n\tADD COLUMN val int64 (NOT NULL, GENERATED ALWAYS AS (key + 1) STORED)\n;\n"},
};
TSetup setup;
diff --git a/yql/essentials/sql/v1/node.h b/yql/essentials/sql/v1/node.h
index c9e8b16a799..b79eb7d04bc 100644
--- a/yql/essentials/sql/v1/node.h
+++ b/yql/essentials/sql/v1/node.h
@@ -791,12 +791,19 @@ struct TEncoding {
TMap<TString, TNodePtr> Entries;
};
+struct TGeneratedColumn {
+ TString ContextPrefix;
+ TString ExprBody;
+ bool Stored = false;
+};
+
struct TColumnOptions {
TNodePtr DefaultExpr;
TVector<TIdentifier> Families;
TMaybe<TCompression> Compression;
bool Nullable = true;
TMaybe<TVector<TEncoding>> ColumnEncoding;
+ TMaybe<TGeneratedColumn> Generated;
};
struct TColumnSchema {
@@ -821,6 +828,7 @@ struct TColumnSchema {
bool Nullable = false;
bool Serial = false;
TMaybe<TVector<TEncoding>> ColumnEncoding;
+ TMaybe<TGeneratedColumn> Generated;
};
struct TColumns: public TSimpleRefCount<TColumns> {
diff --git a/yql/essentials/sql/v1/query.cpp b/yql/essentials/sql/v1/query.cpp
index 6ec40c25ce5..0276df12534 100644
--- a/yql/essentials/sql/v1/query.cpp
+++ b/yql/essentials/sql/v1/query.cpp
@@ -490,6 +490,13 @@ INode::TPtr CreateEncodingsListSettings(const TVector<TEncoding>& columnEncoding
return encodingsList;
}
+INode::TPtr CreateGeneratedValue(const TGeneratedColumn& generated, TPosition pos, const INode& node) {
+ const ui32 flags = NYql::TAstNodeFlags::ArbitraryContent | NYql::TAstNodeFlags::UnstableFormat;
+ auto contextText = BuildQuotedAtom(pos, generated.ContextPrefix, flags);
+ auto exprText = BuildQuotedAtom(pos, generated.ExprBody, flags);
+ return node.Q(node.Y(contextText, exprText, node.Q(generated.Stored ? "stored" : "virtual")));
+}
+
} // namespace
class TPrepTableKeys: public ITableKeys {
@@ -1357,6 +1364,11 @@ public:
columnConstraints = L(columnConstraints, Q(Y(Q("default"), col.DefaultExpr)));
}
+ if (col.Generated) {
+ auto generatedValue = CreateGeneratedValue(*col.Generated, Pos_, *this);
+ columnConstraints = L(columnConstraints, Q(Y(Q("generated"), generatedValue)));
+ }
+
columnDesc = L(columnDesc, Q(Y(Q("columnConstrains"), Q(columnConstraints))));
if (col.Compression) {
@@ -1715,6 +1727,11 @@ public:
columnConstraints = L(columnConstraints, Q(Y(Q("default"), col.DefaultExpr)));
}
+ if (col.Generated) {
+ auto generatedValue = CreateGeneratedValue(*col.Generated, Pos_, *this);
+ columnConstraints = L(columnConstraints, Q(Y(Q("generated"), generatedValue)));
+ }
+
columnDesc = L(columnDesc, Q(Y(Q("columnConstrains"), Q(columnConstraints))));
if (col.Compression) {
diff --git a/yql/essentials/sql/v1/sql_query.cpp b/yql/essentials/sql/v1/sql_query.cpp
index c6de747288b..42228fa5586 100644
--- a/yql/essentials/sql/v1/sql_query.cpp
+++ b/yql/essentials/sql/v1/sql_query.cpp
@@ -2778,6 +2778,17 @@ bool TSqlQuery::AlterTableAddColumn(const TRule_alter_table_add_column& node, TA
Ctx_.Error() << "Several column families for a single column are not yet supported";
return false;
}
+ if (columnSchema->Generated) {
+ const TString& service = Ctx_.Scoped->CurrService;
+ if (service != KikimrProviderName && service != YdbProviderName) {
+ Ctx_.Error(columnSchema->Pos) << "GENERATED ALWAYS AS columns are supported only for the ydb provider";
+ return false;
+ }
+ if (params.TableType != ETableType::Table) {
+ Ctx_.Error(columnSchema->Pos) << "GENERATED ALWAYS AS columns are supported only for CREATE TABLE and ALTER TABLE";
+ return false;
+ }
+ }
params.AddColumns.push_back(*columnSchema);
return true;
}
@@ -5020,6 +5031,11 @@ bool BuildColumnFeatures(std::map<TString, TDeferredAtom>& result, const TRule_c
return false;
}
+ if (options->Generated) {
+ translation.Context().Error(pos) << "GENERATED ALWAYS AS columns are supported only for CREATE TABLE and ALTER TABLE";
+ return false;
+ }
+
auto& typeBind = columnSchema.GetRule_type_name_or_bind2();
switch (typeBind.Alt_case()) {
case TRule_type_name_or_bind::kAltTypeNameOrBind1: {
diff --git a/yql/essentials/sql/v1/sql_translation.cpp b/yql/essentials/sql/v1/sql_translation.cpp
index 141d416f6d4..c40afa7705e 100644
--- a/yql/essentials/sql/v1/sql_translation.cpp
+++ b/yql/essentials/sql/v1/sql_translation.cpp
@@ -17,6 +17,7 @@
#include <util/generic/scope.h>
#include <util/string/join.h>
+#include <util/string/strip.h>
#include <library/cpp/protobuf/util/simple_reflection.h>
@@ -1731,6 +1732,7 @@ TMaybe<TColumnOptions> ColumnOptions(const TRule_column_schema& node, TSqlTransl
TMaybe<TCompression> compression;
bool nullable = true;
TMaybe<TVector<TEncoding>> columnEncoding;
+ TMaybe<TGeneratedColumn> generated;
const auto& optionsList = node.GetRule_column_option_list3();
@@ -1740,9 +1742,10 @@ TMaybe<TColumnOptions> ColumnOptions(const TRule_column_schema& node, TSqlTransl
DefaultValue,
Compression,
Encoding,
+ Generated,
};
- TVector<TRule_column_option> columnOptions(Reserve(static_cast<size_t>(EOption::Encoding) + 1));
+ TVector<TRule_column_option> columnOptions(Reserve(static_cast<size_t>(EOption::Generated) + 1));
{
switch (optionsList.Alt_case()) {
@@ -1873,6 +1876,57 @@ TMaybe<TColumnOptions> ColumnOptions(const TRule_column_schema& node, TSqlTransl
break;
}
+ case TRule_column_option::kAltColumnOption6: { // generated_always
+ const auto& opt = rule.GetAlt_column_option6().GetRule_generated_always1();
+ if (std::find(usedOptions.begin(), usedOptions.end(), EOption::Generated) != usedOptions.end()) {
+ TPosition pos = ctx.Context().TokenPosition(opt.GetToken2());
+ ctx.Context().Error(pos) << "'GENERATED ALWAYS AS' option can be specified only once";
+ return {};
+ }
+
+ usedOptions.push_back(EOption::Generated);
+
+ {
+ TColumnRefScope scope(ctx.Context(), EColumnRefState::Allow);
+ TSqlExpression expr(ctx);
+ if (!Unwrap(expr.Build(opt.GetRule_expr4()))) {
+ return {};
+ }
+ }
+
+ TStringBuilder contextPrefix;
+ if (!BuildContextRecreationQuery(ctx.Context(), contextPrefix)) {
+ return {};
+ }
+
+ const auto& lparen = opt.GetToken3();
+ const auto& rparen = opt.GetToken5();
+ auto begin = GetQueryPosition(ctx.Context().Query, lparen);
+ auto end = GetQueryPosition(ctx.Context().Query, rparen);
+
+ if (begin == std::string::npos || end == std::string::npos) {
+ TPosition pos = ctx.Context().TokenPosition(opt.GetToken2());
+ ctx.Context().Error(pos) << "Failed to extract GENERATED ALWAYS AS expression text";
+ return {};
+ }
+
+ begin += lparen.value().size();
+ TString exprBody = StripString(ctx.Context().Query.substr(begin, end - begin));
+
+ bool generatedStored = false;
+ if (opt.HasBlock6()) {
+ const auto tokenId = opt.GetBlock6().GetToken1().GetId();
+ generatedStored = IS_TOKEN(tokenId, STORED);
+ }
+
+ generated = TGeneratedColumn{
+ .ContextPrefix = std::move(contextPrefix),
+ .ExprBody = std::move(exprBody),
+ .Stored = generatedStored,
+ };
+
+ break;
+ }
case TRule_column_option::ALT_NOT_SET:
YQL_ENSURE(false, "Unreachable");
}
@@ -1885,6 +1939,7 @@ TMaybe<TColumnOptions> ColumnOptions(const TRule_column_schema& node, TSqlTransl
.Compression = std::move(compression),
.Nullable = nullable,
.ColumnEncoding = std::move(columnEncoding),
+ .Generated = std::move(generated),
};
}
@@ -1899,7 +1954,7 @@ TMaybe<TColumnSchema> TSqlTranslation::ColumnSchemaImpl(const TRule_column_schem
return {};
}
- auto&& [defaultExpr, families, compression, nullable, columnEncoding] = columnOptions.GetRef();
+ auto&& [defaultExpr, families, compression, nullable, columnEncoding, generated] = columnOptions.GetRef();
if (!type) {
type = TypeNodeOrBind(node.GetRule_type_name_or_bind2());
@@ -1919,6 +1974,7 @@ TMaybe<TColumnSchema> TSqlTranslation::ColumnSchemaImpl(const TRule_column_schem
.Nullable = nullable,
.Serial = serial,
.ColumnEncoding = columnEncoding,
+ .Generated = generated,
};
}
@@ -2056,6 +2112,17 @@ bool TSqlTranslation::CreateTableEntry(const TRule_create_table_entry& node, TCr
Ctx_.Error() << "Several column families for a single column are not yet supported";
return false;
}
+ if (columnSchema->Generated) {
+ const TString& service = Ctx_.Scoped->CurrService;
+ if (service != KikimrProviderName && service != YdbProviderName) {
+ Ctx_.Error(columnSchema->Pos) << "GENERATED ALWAYS AS columns are supported only for the ydb provider";
+ return false;
+ }
+ if (params.TableType != ETableType::Table) {
+ Ctx_.Error(columnSchema->Pos) << "GENERATED ALWAYS AS columns are supported only for CREATE TABLE and ALTER TABLE";
+ return false;
+ }
+ }
params.Columns.push_back(*columnSchema);
break;
}
diff --git a/yql/essentials/sql/v1/sql_ut_common.h b/yql/essentials/sql/v1/sql_ut_common.h
index b53675b12ea..8558598e1da 100644
--- a/yql/essentials/sql/v1/sql_ut_common.h
+++ b/yql/essentials/sql/v1/sql_ut_common.h
@@ -16190,4 +16190,415 @@ Y_UNIT_TEST(OptionalNull) {
} // Y_UNIT_TEST_SUITE(NullAsTypeName)
+Y_UNIT_TEST_SUITE(GeneratedColumns) {
+
+Y_UNIT_TEST(CreateTableGeneratedVirtualByDefault) {
+ auto res = SqlToYql(R"sql(
+ USE ydb;
+ CREATE TABLE tbl (
+ key Uint32,
+ gen Int32 AS (5),
+ PRIMARY KEY (key)
+ );
+ )sql");
+ UNIT_ASSERT_C(res.IsOk(), Err2Str(res));
+
+ const auto program = GetPrettyPrint(res);
+ UNIT_ASSERT_STRING_CONTAINS(program, R"#('('generated '('"USE ydb;\n" '"5" 'virtual)))#");
+ UNIT_ASSERT(!program.Contains("stored"));
+ UNIT_ASSERT(!program.Contains(R"#((Int32 '"5"))#"));
+}
+
+Y_UNIT_TEST(CreateTableGeneratedExplicitVirtual) {
+ auto res = SqlToYql(R"sql(
+ USE ydb;
+ CREATE TABLE tbl (
+ key Uint32,
+ gen Int32 AS (5) VIRTUAL,
+ PRIMARY KEY (key)
+ );
+ )sql");
+ UNIT_ASSERT_C(res.IsOk(), Err2Str(res));
+
+ const auto program = GetPrettyPrint(res);
+ UNIT_ASSERT_STRING_CONTAINS(program, R"#('('generated '('"USE ydb;\n" '"5" 'virtual)))#");
+ UNIT_ASSERT(!program.Contains("stored"));
+}
+
+Y_UNIT_TEST(CreateTableGeneratedStored) {
+ auto res = SqlToYql(R"sql(
+ USE ydb;
+ CREATE TABLE tbl (
+ key Uint32,
+ gen Int32 AS (5) STORED,
+ PRIMARY KEY (key)
+ );
+ )sql");
+ UNIT_ASSERT_C(res.IsOk(), Err2Str(res));
+
+ const auto program = GetPrettyPrint(res);
+ UNIT_ASSERT_STRING_CONTAINS(program, R"#('('generated '('"USE ydb;\n" '"5" 'stored)))#");
+ UNIT_ASSERT(!program.Contains("virtual"));
+}
+
+Y_UNIT_TEST(CreateTableGeneratedAlwaysKeyword) {
+ auto res = SqlToYql(R"sql(
+ USE ydb;
+ CREATE TABLE tbl (
+ key Uint32,
+ gen Int32 GENERATED ALWAYS AS (5) STORED,
+ PRIMARY KEY (key)
+ );
+ )sql");
+ UNIT_ASSERT_C(res.IsOk(), Err2Str(res));
+
+ const auto program = GetPrettyPrint(res);
+ UNIT_ASSERT_STRING_CONTAINS(program, R"#('('generated '('"USE ydb;\n" '"5" 'stored)))#");
+ UNIT_ASSERT(!program.Contains("virtual"));
+}
+
+Y_UNIT_TEST(CreateTableGeneratedWithoutAlways) {
+ auto res = SqlToYql(R"sql(
+ USE ydb;
+ CREATE TABLE tbl (
+ id Uint32,
+ Col2 Uint32,
+ Col3 Uint32,
+ Col1 Uint32 AS (2 + 3),
+ PRIMARY KEY (id)
+ );
+ )sql");
+ UNIT_ASSERT_C(res.IsOk(), Err2Str(res));
+
+ const auto program = GetPrettyPrint(res);
+ UNIT_ASSERT_STRING_CONTAINS(program, R"#('('generated '('"USE ydb;\n" '"2 + 3" 'virtual)))#");
+ UNIT_ASSERT(!program.Contains("stored"));
+}
+
+Y_UNIT_TEST(CreateTableGeneratedDeterministicExpr) {
+ auto res = SqlToYql(R"sql(
+ USE ydb;
+ CREATE TABLE tbl (
+ key Uint32,
+ gen Int32 AS (1 + 2) VIRTUAL,
+ PRIMARY KEY (key)
+ );
+ )sql");
+ UNIT_ASSERT_C(res.IsOk(), Err2Str(res));
+
+ const auto program = GetPrettyPrint(res);
+ UNIT_ASSERT_STRING_CONTAINS(program, R"#('('generated '('"USE ydb;\n" '"1 + 2" 'virtual)))#");
+ UNIT_ASSERT(!program.Contains("stored"));
+}
+
+Y_UNIT_TEST(CreateTableGeneratedNonDeterministicExpr) {
+ auto res = SqlToYql(R"sql(
+ USE ydb;
+ CREATE TABLE tbl (
+ key Uint32,
+ gen Uint64 AS (RandomNumber(1)) STORED,
+ PRIMARY KEY (key)
+ );
+ )sql");
+ UNIT_ASSERT_C(res.IsOk(), Err2Str(res));
+
+ const auto program = GetPrettyPrint(res);
+ UNIT_ASSERT_STRING_CONTAINS(program, R"#('('generated '('"USE ydb;\n" '"RandomNumber(1)" 'stored)))#");
+ UNIT_ASSERT(!program.Contains("virtual"));
+}
+
+Y_UNIT_TEST(CreateTableGeneratedReferencesColumns) {
+ auto res = SqlToYql(R"sql(
+ USE ydb;
+ CREATE TABLE tbl (
+ a Int32,
+ b Int32,
+ gen Int32 GENERATED ALWAYS AS (a + b) STORED,
+ PRIMARY KEY (a)
+ );
+ )sql");
+ UNIT_ASSERT_C(res.IsOk(), Err2Str(res));
+
+ const auto program = GetPrettyPrint(res);
+ UNIT_ASSERT_STRING_CONTAINS(program, R"#('('generated '('"USE ydb;\n" '"a + b" 'stored)))#");
+ UNIT_ASSERT(!program.Contains("virtual"));
+}
+
+Y_UNIT_TEST(CreateTableMultipleGeneratedColumns) {
+ auto res = SqlToYql(R"sql(
+ USE ydb;
+ CREATE TABLE tbl (
+ key Uint32,
+ x Int32 AS (1) VIRTUAL,
+ y Int32 AS (2) STORED,
+ PRIMARY KEY (key)
+ );
+ )sql");
+ UNIT_ASSERT_C(res.IsOk(), Err2Str(res));
+
+ const auto program = GetPrettyPrint(res);
+ UNIT_ASSERT_STRING_CONTAINS(program, R"#('('generated '('"USE ydb;\n" '"1" 'virtual)))#");
+ UNIT_ASSERT_STRING_CONTAINS(program, R"#('('generated '('"USE ydb;\n" '"2" 'stored)))#");
+}
+
+Y_UNIT_TEST(AlterTableAddGeneratedVirtualColumn) {
+ auto res = SqlToYql(R"sql(
+ USE ydb;
+ ALTER TABLE tbl ADD COLUMN gen Int32 AS (5);
+ )sql");
+ UNIT_ASSERT_C(res.IsOk(), Err2Str(res));
+
+ const auto program = GetPrettyPrint(res);
+ UNIT_ASSERT_STRING_CONTAINS(program, "addColumns");
+ UNIT_ASSERT_STRING_CONTAINS(program, R"#('('generated '('"USE ydb;\n" '"5" 'virtual)))#");
+ UNIT_ASSERT(!program.Contains("stored"));
+}
+
+Y_UNIT_TEST(AlterTableAddGeneratedStoredColumn) {
+ auto res = SqlToYql(R"sql(
+ USE ydb;
+ ALTER TABLE tbl ADD COLUMN gen Int32 GENERATED ALWAYS AS (a + b) STORED;
+ )sql");
+ UNIT_ASSERT_C(res.IsOk(), Err2Str(res));
+
+ const auto program = GetPrettyPrint(res);
+ UNIT_ASSERT_STRING_CONTAINS(program, "addColumns");
+ UNIT_ASSERT_STRING_CONTAINS(program, R"#('('generated '('"USE ydb;\n" '"a + b" 'stored)))#");
+ UNIT_ASSERT(!program.Contains("virtual"));
+ UNIT_ASSERT(!program.Contains("(Member row"));
+}
+
+Y_UNIT_TEST(AlterTableAddGeneratedColumnWithNamedLambda) {
+ auto res = SqlToYql(R"sql(
+ USE ydb;
+ $f = ($x) -> ($x * 2);
+ ALTER TABLE tbl ADD COLUMN gen Int32 GENERATED ALWAYS AS ($f(a)) STORED;
+ )sql");
+ UNIT_ASSERT_C(res.IsOk(), Err2Str(res));
+
+ const auto program = GetPrettyPrint(res);
+ UNIT_ASSERT_STRING_CONTAINS(program, "addColumns");
+ UNIT_ASSERT_STRING_CONTAINS(program, R"#('('generated '('"USE ydb;\n$f = ($x) -> ($x * 2);\n" '"$f(a)" 'stored)))#");
+ UNIT_ASSERT(!program.Contains("virtual"));
+}
+
+Y_UNIT_TEST(CreateTableGeneratedColumnWithNamedNodeRef) {
+ auto res = SqlToYql(R"sql(
+ USE ydb;
+ $factor = 10;
+ CREATE TABLE tbl (
+ key Uint32,
+ num Int32,
+ gen Int32 GENERATED ALWAYS AS (num * $factor) STORED,
+ PRIMARY KEY (key)
+ );
+ )sql");
+ UNIT_ASSERT_C(res.IsOk(), Err2Str(res));
+
+ const auto program = GetPrettyPrint(res);
+ UNIT_ASSERT_STRING_CONTAINS(program, R"#('('generated '('"USE ydb;\n$factor = 10;\n" '"num * $factor" 'stored)))#");
+ UNIT_ASSERT(!program.Contains("virtual"));
+ UNIT_ASSERT(!program.Contains("Read"));
+}
+
+Y_UNIT_TEST(CreateTableGeneratedColumnWithSubqueryNamedNodeRef) {
+ auto res = SqlToYql(R"sql(
+ USE ydb;
+ $ids = (SELECT id FROM other);
+ CREATE TABLE tbl (
+ key Uint32,
+ num Int32,
+ gen Bool GENERATED ALWAYS AS (num IN $ids) STORED,
+ PRIMARY KEY (key)
+ );
+ )sql");
+ UNIT_ASSERT_C(res.IsOk(), Err2Str(res));
+
+ const auto program = GetPrettyPrint(res);
+ UNIT_ASSERT_STRING_CONTAINS(program, R"#('('generated '('"USE ydb;\n$ids = (SELECT id FROM other);\n" '"num IN $ids" 'stored)))#");
+ UNIT_ASSERT(!program.Contains("virtual"));
+ UNIT_ASSERT(!program.Contains("Read"));
+}
+
+Y_UNIT_TEST(CreateTableGeneratedColumnWithInlineSubquery) {
+ auto res = SqlToYql(R"sql(
+ USE ydb;
+ CREATE TABLE tbl (
+ key Uint32,
+ num Int32,
+ gen Bool GENERATED ALWAYS AS (num IN (SELECT id FROM other)) STORED,
+ PRIMARY KEY (key)
+ );
+ )sql");
+ UNIT_ASSERT_C(res.IsOk(), Err2Str(res));
+
+ const auto program = GetPrettyPrint(res);
+ UNIT_ASSERT_STRING_CONTAINS(program, R"#('('generated '('"USE ydb;\n" '"num IN (SELECT id FROM other)" 'stored)))#");
+ UNIT_ASSERT(!program.Contains("virtual"));
+ UNIT_ASSERT(!program.Contains("Read"));
+}
+
+Y_UNIT_TEST(CreateTableGeneratedColumnWithScalarSubquery) {
+ auto res = SqlToYql(R"sql(
+ USE ydb;
+ CREATE TABLE tbl (
+ key Uint32,
+ num Int32,
+ gen Int32 GENERATED ALWAYS AS ((SELECT MAX(id) FROM other)) STORED,
+ PRIMARY KEY (key)
+ );
+ )sql");
+ UNIT_ASSERT_C(res.IsOk(), Err2Str(res));
+
+ const auto program = GetPrettyPrint(res);
+ UNIT_ASSERT_STRING_CONTAINS(program, R"#('('generated '('"USE ydb;\n" '"(SELECT MAX(id) FROM other)" 'stored)))#");
+ UNIT_ASSERT(!program.Contains("virtual"));
+ UNIT_ASSERT(!program.Contains("Read"));
+}
+
+Y_UNIT_TEST(CreateTableGeneratedWithPragma) {
+ auto res = SqlToYql(R"sql(
+ USE ydb;
+ PRAGMA FlexibleTypes;
+ CREATE TABLE tbl (
+ key Uint32,
+ num Int32,
+ gen Int32 GENERATED ALWAYS AS (num / 2) STORED,
+ PRIMARY KEY (key)
+ );
+ )sql");
+ UNIT_ASSERT_C(res.IsOk(), Err2Str(res));
+
+ const auto program = GetPrettyPrint(res);
+ UNIT_ASSERT_STRING_CONTAINS(program, R"#('('generated '('"USE ydb;\nPRAGMA FlexibleTypes;\n" '"num / 2" 'stored)))#");
+ UNIT_ASSERT(!program.Contains("virtual"));
+}
+
+Y_UNIT_TEST(CreateTableGeneratedNestedPath) {
+ auto res = SqlToYql(R"sql(
+ USE ydb;
+ CREATE TABLE `a/b/tbl` (
+ key Uint32,
+ num Int32,
+ gen Int32 GENERATED ALWAYS AS (num / 2) STORED,
+ PRIMARY KEY (key)
+ );
+ )sql");
+ UNIT_ASSERT_C(res.IsOk(), Err2Str(res));
+
+ const auto program = GetPrettyPrint(res);
+ UNIT_ASSERT_STRING_CONTAINS(program, R"#('('generated '('"USE ydb;\n" '"num / 2" 'stored)))#");
+}
+
+Y_UNIT_TEST(AlterTableAddGeneratedColumnNestedPath) {
+ auto res = SqlToYql(R"sql(
+ USE ydb;
+ ALTER TABLE `a/b/tbl` ADD COLUMN gen Int32 GENERATED ALWAYS AS (a + b) STORED;
+ )sql");
+ UNIT_ASSERT_C(res.IsOk(), Err2Str(res));
+
+ const auto program = GetPrettyPrint(res);
+ UNIT_ASSERT_STRING_CONTAINS(program, R"#('('generated '('"USE ydb;\n" '"a + b" 'stored)))#");
+}
+
+Y_UNIT_TEST(CreateTableStoreGeneratedColumnFails) {
+ auto res = SqlToYql(R"sql(
+ USE ydb;
+ CREATE TABLESTORE tbl (
+ key Uint32 NOT NULL,
+ gen Int32 AS (5) STORED,
+ PRIMARY KEY (key)
+ );
+ )sql");
+ UNIT_ASSERT(!res.IsOk());
+ UNIT_ASSERT_STRING_CONTAINS(Err2Str(res), "GENERATED ALWAYS AS columns are supported only for CREATE TABLE and ALTER TABLE");
+}
+
+Y_UNIT_TEST(CreateExternalTableGeneratedColumnFails) {
+ auto res = SqlToYql(R"sql(
+ USE ydb;
+ CREATE EXTERNAL TABLE tbl (
+ a Int32,
+ gen Int32 AS (a) STORED
+ ) WITH (
+ DATA_SOURCE="/Root/mydatasource",
+ LOCATION="/folder1/*"
+ );
+ )sql");
+ UNIT_ASSERT(!res.IsOk());
+ UNIT_ASSERT_STRING_CONTAINS(Err2Str(res), "GENERATED ALWAYS AS columns are supported only for CREATE TABLE and ALTER TABLE");
+}
+
+Y_UNIT_TEST(AlterExternalTableAddGeneratedColumnFails) {
+ auto res = SqlToYql(R"sql(
+ USE ydb;
+ ALTER EXTERNAL TABLE tbl ADD COLUMN gen Int32 AS (5) STORED;
+ )sql");
+ UNIT_ASSERT(!res.IsOk());
+ UNIT_ASSERT_STRING_CONTAINS(Err2Str(res), "GENERATED ALWAYS AS columns are supported only for CREATE TABLE and ALTER TABLE");
+}
+
+Y_UNIT_TEST(AlterTableStoreAddGeneratedColumnFails) {
+ auto res = SqlToYql(R"sql(
+ USE ydb;
+ ALTER TABLESTORE tbl ADD COLUMN gen Int32 AS (5) STORED;
+ )sql");
+ UNIT_ASSERT(!res.IsOk());
+ UNIT_ASSERT_STRING_CONTAINS(Err2Str(res), "GENERATED ALWAYS AS columns are supported only for CREATE TABLE and ALTER TABLE");
+}
+
+Y_UNIT_TEST(CreateTableGeneratedColumnNonYdbProviderFails) {
+ auto res = SqlToYql(R"sql(
+ USE plato;
+ CREATE TABLE tbl (
+ a Int32,
+ gen Int32 AS (a) STORED,
+ PRIMARY KEY (a)
+ );
+ )sql");
+ UNIT_ASSERT(!res.IsOk());
+ UNIT_ASSERT_STRING_CONTAINS(Err2Str(res), "GENERATED ALWAYS AS columns are supported only for the ydb provider");
+}
+
+Y_UNIT_TEST(CreateTableGeneratedColumnNonYdbProviderVirtualFails) {
+ auto res = SqlToYql(R"sql(
+ USE plato;
+ CREATE TABLE tbl (
+ key Uint32,
+ gen Int32 GENERATED ALWAYS AS (5),
+ PRIMARY KEY (key)
+ );
+ )sql");
+ UNIT_ASSERT(!res.IsOk());
+ UNIT_ASSERT_STRING_CONTAINS(Err2Str(res), "GENERATED ALWAYS AS columns are supported only for the ydb provider");
+}
+
+Y_UNIT_TEST(AlterTableAddGeneratedColumnNonYdbProviderFails) {
+ auto res = SqlToYql(R"sql(
+ USE plato;
+ ALTER TABLE tbl ADD COLUMN gen Int32 AS (5) STORED;
+ )sql");
+ UNIT_ASSERT(!res.IsOk());
+ UNIT_ASSERT_STRING_CONTAINS(Err2Str(res), "ALTER TABLE is not supported for yt provider");
+}
+
+Y_UNIT_TEST(CreateTableGeneratedStringFunction) {
+ auto res = SqlToYql(R"sql(
+ USE ydb;
+ CREATE TABLE tbl (
+ id Int32,
+ name String,
+ upper_name String GENERATED ALWAYS AS (String::AsciiToUpper(name)) STORED,
+ PRIMARY KEY (id)
+ );
+ )sql");
+ UNIT_ASSERT_C(res.IsOk(), Err2Str(res));
+
+ const auto program = GetPrettyPrint(res);
+ UNIT_ASSERT_STRING_CONTAINS(program, R"#('('generated '('"USE ydb;\n" '"String::AsciiToUpper(name)" 'stored)))#");
+ UNIT_ASSERT(!program.Contains("virtual"));
+}
+
+} // Y_UNIT_TEST_SUITE(GeneratedColumns)
+
// NOLINTEND(misc-definitions-in-headers)
diff --git a/yt/cpp/mapreduce/interface/logging/yt_log.cpp b/yt/cpp/mapreduce/interface/logging/yt_log.cpp
index 1db626adb04..9de369fdcb5 100644
--- a/yt/cpp/mapreduce/interface/logging/yt_log.cpp
+++ b/yt/cpp/mapreduce/interface/logging/yt_log.cpp
@@ -2,10 +2,14 @@
#include "logger.h"
+#include <library/cpp/yt/logging/tagged_payload.h>
+
#include <util/generic/guid.h>
#include <util/system/mutex.h>
+#include <variant>
+
namespace NYT {
using namespace NLogging;
@@ -44,13 +48,14 @@ public:
void Enqueue(TLogEvent&& event) override
{
if (auto logger = GetLogger()) {
+ auto message = GetMessageFromTaggedPayload(std::get<TTaggedLogEventPayload>(event.Payload));
LogMessage(
logger,
ToImplLevel(event.Level),
::TSourceLocation(event.SourceFile, event.SourceLine),
"%.*s",
- event.MessageRef.size(),
- event.MessageRef.begin());
+ message.size(),
+ message.begin());
} else if (auto* defaultLogManager = GetDefaultLogManager()) {
defaultLogManager->Enqueue(std::move(event));
}
diff --git a/yt/yql/providers/yt/codec/yt_codec_tz.h b/yt/yql/providers/yt/codec/yt_codec_tz.h
index deadb9debb4..f6b23af95b2 100644
--- a/yt/yql/providers/yt/codec/yt_codec_tz.h
+++ b/yt/yql/providers/yt/codec/yt_codec_tz.h
@@ -25,7 +25,7 @@ void WriteTzNative(NCommon::TOutputBuf& buf, typename NUdf::TDataType<T>::TLayou
buf.WriteMany((const char*)&tzId, sizeof(tzId));
}
-// yt native table format (string with presorted timestamp concatenated with timezone name)
+// yt native table format (string with presorted timestamp concatenated with presorted timezone id)
// only tzId is validated
template<typename T>
bool ReadTzTableNative(NCommon::TInputBuf& buf, typename NUdf::TDataType<T>::TLayout& value, ui16& tzId) {
@@ -33,7 +33,7 @@ bool ReadTzTableNative(NCommon::TInputBuf& buf, typename NUdf::TDataType<T>::TLa
using TLayout = NUdf::TDataType<T>::TLayout;
TStringBuf str = buf.ReadYtString();
- if (str.size() < sizeof(value)) {
+ if (str.size() != sizeof(value) + sizeof(tzId)) {
return false;
}
@@ -46,11 +46,9 @@ bool ReadTzTableNative(NCommon::TInputBuf& buf, typename NUdf::TDataType<T>::TLa
value = (TLayout)SwapBytes(data);
str.Skip(sizeof(data));
- if (auto maybeTzId = NKikimr::NMiniKQL::FindTimezoneId(str)) {
- tzId = *maybeTzId;
- return true;
- }
- return false;
+ std::memcpy(&tzId, str.data(), sizeof(tzId));
+ tzId = SwapBytes(tzId);
+ return NKikimr::NMiniKQL::IsValidTimezoneId(tzId);
}
template<typename T>
@@ -63,13 +61,12 @@ void WriteTzTableNative(NCommon::TOutputBuf& buf, typename NUdf::TDataType<T>::T
data = 0x80 ^ data;
}
- auto tzName = NKikimr::NMiniKQL::GetTimezoneIANAName(tzId);
-
- ui32 size = sizeof(data) + tzName.size();;
+ ui32 size = sizeof(data) + sizeof(tzId);
buf.Write(NYson::NDetail::StringMarker);
buf.WriteVarI32(size);
buf.WriteMany((const char*)&data, sizeof(data));
- buf.WriteMany(tzName);
+ tzId = SwapBytes(tzId);
+ buf.WriteMany((const char*)&tzId, sizeof(tzId));
}
namespace {
diff --git a/yt/yql/providers/yt/provider/yql_yt_ytflow_integration.cpp b/yt/yql/providers/yt/provider/yql_yt_ytflow_integration.cpp
index 9e4c87017fe..c733740bf06 100644
--- a/yt/yql/providers/yt/provider/yql_yt_ytflow_integration.cpp
+++ b/yt/yql/providers/yt/provider/yql_yt_ytflow_integration.cpp
@@ -289,6 +289,15 @@ public:
sinkSettings.SetDoesExist(tableDesc.Meta->DoesExist);
sinkSettings.SetTruncate(tableDesc.Intents & TYtTableIntent::Override);
+
+ const auto& rowSpec = tableDesc.RowSpec;
+ if (rowSpec) {
+ for (const auto& [column, _] : rowSpec->GetForeignSort()) {
+ if (!rowSpec->ExpressionColumns.contains(column)) {
+ sinkSettings.AddKeyColumns(column);
+ }
+ }
+ }
}
settings.PackFrom(sinkSettings);
diff --git a/yt/yql/providers/ytflow/integration/proto/yt.proto b/yt/yql/providers/ytflow/integration/proto/yt.proto
index 2959ca4b6c8..4ebfad1059b 100644
--- a/yt/yql/providers/ytflow/integration/proto/yt.proto
+++ b/yt/yql/providers/ytflow/integration/proto/yt.proto
@@ -14,4 +14,5 @@ message TQYTSinkMessage {
optional bytes RowType = 3;
optional bool DoesExist = 5;
optional bool Truncate = 6;
+ repeated string KeyColumns = 7;
}
diff --git a/yt/yql/tests/sql/suites/ytflow/multiple_sorted_outputs_with_different_keys.cfg b/yt/yql/tests/sql/suites/ytflow/multiple_sorted_outputs_with_different_keys.cfg
new file mode 100644
index 00000000000..0e3721e4b77
--- /dev/null
+++ b/yt/yql/tests/sql/suites/ytflow/multiple_sorted_outputs_with_different_keys.cfg
@@ -0,0 +1,4 @@
+in Input input.txt
+out OutputKey output_sorted_by_key.txt
+out OutputValue output_sorted_by_value.txt
+providers ytflow
diff --git a/yt/yql/tests/sql/suites/ytflow/multiple_sorted_outputs_with_different_keys.yql b/yt/yql/tests/sql/suites/ytflow/multiple_sorted_outputs_with_different_keys.yql
new file mode 100644
index 00000000000..b938bbf058a
--- /dev/null
+++ b/yt/yql/tests/sql/suites/ytflow/multiple_sorted_outputs_with_different_keys.yql
@@ -0,0 +1,27 @@
+/* sort outputs */
+
+use plato;
+
+pragma Engine = "ytflow";
+
+pragma Ytflow.Cluster = "plato";
+pragma Ytflow.PipelinePath = "pipelines/test";
+
+$lambda = ($row) -> {
+ $output_row_type = Struct<"key":Optional<String>, "value":Optional<Int64>>;
+ $variant_type = Variant<$output_row_type, $output_row_type>;
+
+ return If(
+ $row.int64_field < 10,
+ Variant(<|"key":$row.string_field, "value":$row.int64_field|>, "0", $variant_type),
+ Variant(<|"key":$row.string_field, "value":$row.int64_field|>, "1", $variant_type)
+ );
+};
+
+$sorted_by_key, $sorted_by_value = process Input using $lambda(TableRow());
+
+insert into OutputKey
+select * from $sorted_by_key;
+
+insert into OutputValue
+select * from $sorted_by_value;
diff --git a/yt/yql/tests/sql/suites/ytflow/output_composite_key_value_table.txt b/yt/yql/tests/sql/suites/ytflow/output_composite_key_value_table.txt
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/yt/yql/tests/sql/suites/ytflow/output_composite_key_value_table.txt
diff --git a/yt/yql/tests/sql/suites/ytflow/output_composite_key_value_table.txt.attr b/yt/yql/tests/sql/suites/ytflow/output_composite_key_value_table.txt.attr
new file mode 100644
index 00000000000..8f69a0c4304
--- /dev/null
+++ b/yt/yql/tests/sql/suites/ytflow/output_composite_key_value_table.txt.attr
@@ -0,0 +1,23 @@
+{
+ "schema" = <
+ unique_keys = %true;
+ strict = %true;
+ > [
+ {
+ "name" = "key_b";
+ "type" = "string";
+ "sort_order" = "ascending";
+ };
+ {
+ "name" = "key_a";
+ "type" = "int64";
+ "sort_order" = "ascending";
+ };
+ {
+ "name" = "kv_value";
+ "type" = "int64";
+ };
+ ];
+ "_yql_dynamic" = %true;
+ "_yql_dynamic_native_read" = %true;
+}
diff --git a/yt/yql/tests/sql/suites/ytflow/output_sorted_by_key.txt b/yt/yql/tests/sql/suites/ytflow/output_sorted_by_key.txt
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/yt/yql/tests/sql/suites/ytflow/output_sorted_by_key.txt
diff --git a/yt/yql/tests/sql/suites/ytflow/output_sorted_by_key.txt.attr b/yt/yql/tests/sql/suites/ytflow/output_sorted_by_key.txt.attr
new file mode 100644
index 00000000000..6086a84dbc9
--- /dev/null
+++ b/yt/yql/tests/sql/suites/ytflow/output_sorted_by_key.txt.attr
@@ -0,0 +1,18 @@
+{
+ "schema" = <
+ unique_keys = %true;
+ strict = %true;
+ > [
+ {
+ "name" = "key";
+ "type" = "string";
+ "sort_order" = "ascending";
+ };
+ {
+ "name" = "value";
+ "type" = "int64";
+ };
+ ];
+ "_yql_dynamic" = %true;
+ "_yql_dynamic_native_read" = %true;
+}
diff --git a/yt/yql/tests/sql/suites/ytflow/output_sorted_by_value.txt b/yt/yql/tests/sql/suites/ytflow/output_sorted_by_value.txt
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/yt/yql/tests/sql/suites/ytflow/output_sorted_by_value.txt
diff --git a/yt/yql/tests/sql/suites/ytflow/output_sorted_by_value.txt.attr b/yt/yql/tests/sql/suites/ytflow/output_sorted_by_value.txt.attr
new file mode 100644
index 00000000000..3acb927d6d8
--- /dev/null
+++ b/yt/yql/tests/sql/suites/ytflow/output_sorted_by_value.txt.attr
@@ -0,0 +1,18 @@
+{
+ "schema" = <
+ unique_keys = %true;
+ strict = %true;
+ > [
+ {
+ "name" = "value";
+ "type" = "int64";
+ "sort_order" = "ascending";
+ };
+ {
+ "name" = "key";
+ "type" = "string";
+ };
+ ];
+ "_yql_dynamic" = %true;
+ "_yql_dynamic_native_read" = %true;
+}
diff --git a/yt/yql/tests/sql/suites/ytflow/reuse_output_to_sorted_tables_with_different_keys.cfg b/yt/yql/tests/sql/suites/ytflow/reuse_output_to_sorted_tables_with_different_keys.cfg
new file mode 100644
index 00000000000..0e3721e4b77
--- /dev/null
+++ b/yt/yql/tests/sql/suites/ytflow/reuse_output_to_sorted_tables_with_different_keys.cfg
@@ -0,0 +1,4 @@
+in Input input.txt
+out OutputKey output_sorted_by_key.txt
+out OutputValue output_sorted_by_value.txt
+providers ytflow
diff --git a/yt/yql/tests/sql/suites/ytflow/reuse_output_to_sorted_tables_with_different_keys.yql b/yt/yql/tests/sql/suites/ytflow/reuse_output_to_sorted_tables_with_different_keys.yql
new file mode 100644
index 00000000000..7131563360a
--- /dev/null
+++ b/yt/yql/tests/sql/suites/ytflow/reuse_output_to_sorted_tables_with_different_keys.yql
@@ -0,0 +1,16 @@
+/* sort outputs */
+
+use plato;
+
+pragma Engine = "ytflow";
+
+pragma Ytflow.Cluster = "plato";
+pragma Ytflow.PipelinePath = "pipelines/test";
+
+$result_stream = select string_field as key, int64_field as value from Input;
+
+insert into OutputKey
+select * from $result_stream;
+
+insert into OutputValue
+select * from $result_stream;
diff --git a/yt/yql/tests/sql/suites/ytflow/reuse_output_to_sorted_tables_with_same_key.cfg b/yt/yql/tests/sql/suites/ytflow/reuse_output_to_sorted_tables_with_same_key.cfg
new file mode 100644
index 00000000000..596696392fb
--- /dev/null
+++ b/yt/yql/tests/sql/suites/ytflow/reuse_output_to_sorted_tables_with_same_key.cfg
@@ -0,0 +1,4 @@
+in Input input.txt
+out OutputFirst output_sorted_by_key.txt
+out OutputSecond output_sorted_by_key.txt
+providers ytflow
diff --git a/yt/yql/tests/sql/suites/ytflow/reuse_output_to_sorted_tables_with_same_key.yql b/yt/yql/tests/sql/suites/ytflow/reuse_output_to_sorted_tables_with_same_key.yql
new file mode 100644
index 00000000000..784e88094d3
--- /dev/null
+++ b/yt/yql/tests/sql/suites/ytflow/reuse_output_to_sorted_tables_with_same_key.yql
@@ -0,0 +1,16 @@
+/* sort outputs */
+
+use plato;
+
+pragma Engine = "ytflow";
+
+pragma Ytflow.Cluster = "plato";
+pragma Ytflow.PipelinePath = "pipelines/test";
+
+$result_stream = select string_field as key, int64_field as value from Input;
+
+insert into OutputFirst
+select * from $result_stream;
+
+insert into OutputSecond
+select * from $result_stream;
diff --git a/yt/yql/tests/sql/suites/ytflow/sorted_and_unsorted_outputs.cfg b/yt/yql/tests/sql/suites/ytflow/sorted_and_unsorted_outputs.cfg
new file mode 100644
index 00000000000..d28699e18b4
--- /dev/null
+++ b/yt/yql/tests/sql/suites/ytflow/sorted_and_unsorted_outputs.cfg
@@ -0,0 +1,4 @@
+in Input input.txt
+out OutputSorted output_sorted_by_key.txt
+out OutputUnsorted output_value_multiple_outputs.txt
+providers ytflow
diff --git a/yt/yql/tests/sql/suites/ytflow/sorted_and_unsorted_outputs.yql b/yt/yql/tests/sql/suites/ytflow/sorted_and_unsorted_outputs.yql
new file mode 100644
index 00000000000..b0ab691b823
--- /dev/null
+++ b/yt/yql/tests/sql/suites/ytflow/sorted_and_unsorted_outputs.yql
@@ -0,0 +1,28 @@
+/* sort outputs */
+
+use plato;
+
+pragma Engine = "ytflow";
+
+pragma Ytflow.Cluster = "plato";
+pragma Ytflow.PipelinePath = "pipelines/test";
+
+$lambda = ($row) -> {
+ $sorted_row_type = Struct<"key":Optional<String>, "value":Optional<Int64>>;
+ $unsorted_row_type = Struct<"value":Optional<Int64>>;
+ $variant_type = Variant<$sorted_row_type, $unsorted_row_type>;
+
+ return If(
+ $row.int64_field < 10,
+ Variant(<|"key":$row.string_field, "value":$row.int64_field|>, "0", $variant_type),
+ Variant(<|"value":$row.int64_field|>, "1", $variant_type)
+ );
+};
+
+$sorted, $unsorted = process Input using $lambda(TableRow());
+
+insert into OutputSorted
+select * from $sorted;
+
+insert into OutputUnsorted
+select * from $unsorted;
diff --git a/yt/yql/tests/sql/suites/ytflow/write_to_sorted_table_with_composite_key.cfg b/yt/yql/tests/sql/suites/ytflow/write_to_sorted_table_with_composite_key.cfg
new file mode 100644
index 00000000000..ae0ae973f2e
--- /dev/null
+++ b/yt/yql/tests/sql/suites/ytflow/write_to_sorted_table_with_composite_key.cfg
@@ -0,0 +1,3 @@
+in Input input_stream_composite_key.txt
+out Output output_composite_key_value_table.txt
+providers ytflow
diff --git a/yt/yql/tests/sql/suites/ytflow/write_to_sorted_table_with_composite_key.yql b/yt/yql/tests/sql/suites/ytflow/write_to_sorted_table_with_composite_key.yql
new file mode 100644
index 00000000000..37f5aa55876
--- /dev/null
+++ b/yt/yql/tests/sql/suites/ytflow/write_to_sorted_table_with_composite_key.yql
@@ -0,0 +1,15 @@
+/* sort outputs */
+
+use plato;
+
+pragma Engine = "ytflow";
+
+pragma Ytflow.Cluster = "plato";
+pragma Ytflow.PipelinePath = "pipelines/test";
+
+insert into Output
+select
+ key_a,
+ key_b || "_b" as key_b,
+ value as kv_value
+from Input;
diff --git a/yt/yql/tools/mrjob/mrjob.cpp b/yt/yql/tools/mrjob/mrjob.cpp
index 52611c7abf9..d8a8c058740 100644
--- a/yt/yql/tools/mrjob/mrjob.cpp
+++ b/yt/yql/tools/mrjob/mrjob.cpp
@@ -5,17 +5,11 @@
#include <yt/cpp/mapreduce/client/init.h>
-#include <tcmalloc/malloc_extension.h>
-
#include <util/system/env.h>
#include <util/system/mlock.h>
#include <util/system/yassert.h>
int main(int argc, const char *argv[]) {
- // disable tcmalloc sampling
- tcmalloc::MallocExtension::SetGuardedSamplingInterval(-1);
- tcmalloc::MallocExtension::SetProfileSamplingInterval(0);
-
Y_UNUSED(NKikimr::NUdf::GetStaticSymbols());
try {
LockAllMemory(LockCurrentMemory | LockFutureMemory);
diff --git a/yt/yql/tools/mrjob/ya.make b/yt/yql/tools/mrjob/ya.make
index 84ba0a368b0..4d88e6b5b59 100644
--- a/yt/yql/tools/mrjob/ya.make
+++ b/yt/yql/tools/mrjob/ya.make
@@ -1,10 +1,6 @@
PROGRAM(mrjob)
-IF (OS_LINUX)
- ALLOCATOR(TCMALLOC)
-ELSE()
- ALLOCATOR(J)
-ENDIF()
+ALLOCATOR(J)
SRCS(
mrjob.cpp
@@ -17,7 +13,6 @@ IF (OS_LINUX)
ENDIF()
PEERDIR(
- contrib/libs/tcmalloc/malloc_extension
yt/cpp/mapreduce/client
yql/essentials/public/udf/service/terminate_policy
yql/essentials/providers/common/gateway
diff --git a/yt/yt/RpcProxyProtocolVersion.txt b/yt/yt/RpcProxyProtocolVersion.txt
index 9eea6d6e60a..c1cd2292c32 100644
--- a/yt/yt/RpcProxyProtocolVersion.txt
+++ b/yt/yt/RpcProxyProtocolVersion.txt
@@ -16,7 +16,7 @@ SET(YT_RPC_PROXY_SERVER_PROTOCOL_VERSION_MINOR 2)
SET(YT_RPC_MODIFY_ROWS_STRONG_LOCKS_VERSION 2)
# RPC python client has protocol version plus some patch version
-SET(YT_RPC_PYTHON_BINDINGS_PATCH_VERSION 22)
+SET(YT_RPC_PYTHON_BINDINGS_PATCH_VERSION 23)
# YT proto package has protocol version plus some patch version
SET(YT_PROTO_PACKAGE_PATCH_VERSION 15)
diff --git a/yt/yt/client/api/rpc_proxy/client_impl.h b/yt/yt/client/api/rpc_proxy/client_impl.h
index fa7dfdb1a26..51905bdf769 100644
--- a/yt/yt/client/api/rpc_proxy/client_impl.h
+++ b/yt/yt/client/api/rpc_proxy/client_impl.h
@@ -705,7 +705,6 @@ public:
private:
const TConnectionPtr Connection_;
const TClientOptions ClientOptions_;
- const NRpc::TDynamicChannelPoolPtr ChannelPool_;
const NRpc::IChannelPtr RetryingChannel_;
TLazyIntrusivePtr<NTabletClient::ITableMountCache> TableMountCache_;
diff --git a/yt/yt/client/object_client/helpers.cpp b/yt/yt/client/object_client/helpers.cpp
index facf1ae54d8..45746d9150e 100644
--- a/yt/yt/client/object_client/helpers.cpp
+++ b/yt/yt/client/object_client/helpers.cpp
@@ -140,6 +140,7 @@ bool IsVersionedType(EObjectType type)
case EObjectType::Pipeline:
case EObjectType::QueueConsumer:
case EObjectType::QueueProducer:
+ case EObjectType::QueueMultiConsumer:
case EObjectType::CypressProxyMap:
return true;
diff --git a/yt/yt/client/object_client/public.h b/yt/yt/client/object_client/public.h
index ae8d918bab1..6c6e9a31fba 100644
--- a/yt/yt/client/object_client/public.h
+++ b/yt/yt/client/object_client/public.h
@@ -4,14 +4,14 @@
#include <yt/yt/client/job_tracker_client/public.h>
+#include <library/cpp/yt/compact_containers/compact_flat_set.h>
+#include <library/cpp/yt/compact_containers/compact_vector.h>
+
#include <library/cpp/yt/misc/enum.h>
#include <library/cpp/yt/misc/guid.h>
#include <library/cpp/yt/misc/hash.h>
#include <library/cpp/yt/misc/strong_typedef.h>
-#include <library/cpp/yt/compact_containers/compact_vector.h>
-#include <library/cpp/yt/compact_containers/compact_flat_set.h>
-
#include <library/cpp/yt/string/string_builder.h>
namespace NYT::NObjectClient {
@@ -368,6 +368,7 @@ DEFINE_ENUM(EObjectType,
// Queue stuff
((QueueConsumer) (1700))
((QueueProducer) (1701))
+ ((QueueMultiConsumer) (1702))
);
//! A bit mask marking schema types.
diff --git a/yt/yt/client/sequoia_client/public.h b/yt/yt/client/sequoia_client/public.h
index de32a1a1b9c..b9c79e97750 100644
--- a/yt/yt/client/sequoia_client/public.h
+++ b/yt/yt/client/sequoia_client/public.h
@@ -12,6 +12,7 @@ YT_DEFINE_ERROR_ENUM(
((SequoiaRetriableError) (6002))
((InvalidSequoiaReign) (6003))
((TransactionActionFailedOnMasterCell) (6004))
+ ((SequoiaSubtreeTooLarge) (6005))
);
////////////////////////////////////////////////////////////////////////////////
diff --git a/yt/yt/core/logging/config.cpp b/yt/yt/core/logging/config.cpp
index e5ea991aa72..f21f8aa336f 100644
--- a/yt/yt/core/logging/config.cpp
+++ b/yt/yt/core/logging/config.cpp
@@ -57,6 +57,8 @@ void TLogWriterConfig::Register(TRegistrar registrar)
.Default(true);
registrar.Parameter("enable_host_field", &TThis::EnableHostField)
.Default(false);
+ registrar.Parameter("enable_native_tags", &TThis::EnableNativeTags)
+ .Default(false);
registrar.Parameter("json_format", &TThis::JsonFormat)
.Default();
registrar.Parameter("yson_format", &TThis::YsonFormat)
diff --git a/yt/yt/core/logging/config.h b/yt/yt/core/logging/config.h
index 452f41b4233..d0b1cbf366b 100644
--- a/yt/yt/core/logging/config.h
+++ b/yt/yt/core/logging/config.h
@@ -10,6 +10,8 @@
#include <yt/yt/core/ytree/public.h>
#include <yt/yt/core/ytree/yson_struct.h>
+#include <library/cpp/yt/yson_string/string.h>
+
namespace NYT::NLogging {
////////////////////////////////////////////////////////////////////////////////
@@ -35,9 +37,15 @@ struct TLogWriterConfig
bool EnableSourceLocation;
//! Structured formatter options.
+ //! If set, every event carries the system fields "instant", "level" and "category".
bool EnableSystemFields;
+ //! If set, every event carries the "host" field with the local hostname.
bool EnableHostField;
- THashMap<std::string, NYTree::INodePtr> CommonFields;
+ //! If set, a tagged message's tags go to a nested "tags" map; otherwise they
+ //! are folded into the "message" field as |Message (Key: Value, ...)|.
+ bool EnableNativeTags;
+ //! Fields injected into every event as |key -> raw YSON value|.
+ THashMap<std::string, NYson::TYsonString> CommonFields;
NJson::TJsonFormatConfigPtr JsonFormat;
NYson::EYsonFormat YsonFormat;
diff --git a/yt/yt/core/logging/formatter.cpp b/yt/yt/core/logging/formatter.cpp
index 318b4f72653..1dd37fba492 100644
--- a/yt/yt/core/logging/formatter.cpp
+++ b/yt/yt/core/logging/formatter.cpp
@@ -2,6 +2,9 @@
#include "private.h"
+#include <library/cpp/yt/logging/tagged_payload.h>
+#include <library/cpp/yt/logging/structured_payload.h>
+
#include <yt/yt/core/json/json_writer.h>
#include <yt/yt/core/ytree/fluent.h>
@@ -18,9 +21,9 @@ using namespace NYson;
////////////////////////////////////////////////////////////////////////////////
-TPlainTextLogFormatter::TPlainTextLogFormatter(bool enableSourceLocation)
- : TLogFormatterBase(enableSourceLocation)
- , EventFormatter_(enableSourceLocation)
+TPlainTextLogFormatter::TPlainTextLogFormatter(TPlainTextLogFormatterOptions options)
+ : TLogFormatterBase(TLogFormatterBaseOptions{.EnableSourceLocation = options.EnableSourceLocation})
+ , EventFormatter_(options.EnableSourceLocation)
{ }
i64 TPlainTextLogFormatter::WriteFormatted(IOutputStream* outputStream, const TLogEvent& event)
@@ -45,34 +48,32 @@ void TPlainTextLogFormatter::WriteLogReopenSeparator(IOutputStream* outputStream
////////////////////////////////////////////////////////////////////////////////
-TLogFormatterBase::TLogFormatterBase(bool enableSourceLocation)
- : EnableSourceLocation_(enableSourceLocation)
+TLogFormatterBase::TLogFormatterBase(TLogFormatterBaseOptions options)
+ : Options_(std::move(options))
{ }
bool TLogFormatterBase::IsSourceLocationEnabled() const
{
- return EnableSourceLocation_;
+ return Options_.EnableSourceLocation;
}
////////////////////////////////////////////////////////////////////////////////
-TStructuredLogFormatter::TStructuredLogFormatter(
- ELogFormat format,
- THashMap<std::string, INodePtr> commonFields,
- bool enableSourceLocation,
- bool enableSystemFields,
- bool enableHostField,
- NJson::TJsonFormatConfigPtr jsonFormat,
- EYsonFormat ysonFormat)
- : TLogFormatterBase(enableSourceLocation)
- , Format_(format)
- , CommonFields_(std::move(commonFields))
- , EnableSystemFields_(enableSystemFields)
- , EnableHostField_(enableHostField)
- , JsonFormat_(!jsonFormat && (Format_ == ELogFormat::Json)
- ? New<NJson::TJsonFormatConfig>()
- : std::move(jsonFormat))
- , YsonFormat_(ysonFormat)
+namespace {
+
+TStructuredLogFormatterOptions NormalizeStructuredOptions(TStructuredLogFormatterOptions options)
+{
+ if (!options.JsonFormat && options.Format == ELogFormat::Json) {
+ options.JsonFormat = New<NJson::TJsonFormatConfig>();
+ }
+ return options;
+}
+
+} // namespace
+
+TStructuredLogFormatter::TStructuredLogFormatter(TStructuredLogFormatterOptions options)
+ : TLogFormatterBase(TLogFormatterBaseOptions{.EnableSourceLocation = options.EnableSourceLocation})
+ , Options_(NormalizeStructuredOptions(std::move(options)))
{ }
i64 TStructuredLogFormatter::WriteFormatted(IOutputStream* stream, const TLogEvent& event)
@@ -84,17 +85,17 @@ i64 TStructuredLogFormatter::WriteFormatted(IOutputStream* stream, const TLogEve
auto countingStream = TCountingOutput(stream);
std::unique_ptr<IFlushableYsonConsumer> consumer;
- switch (Format_) {
+ switch (Options_.Format) {
case ELogFormat::Json:
- YT_VERIFY(JsonFormat_);
- consumer = NJson::CreateJsonConsumer(&countingStream, EYsonType::Node, JsonFormat_);
+ YT_VERIFY(Options_.JsonFormat);
+ consumer = NJson::CreateJsonConsumer(&countingStream, EYsonType::Node, Options_.JsonFormat);
break;
case ELogFormat::Yson:
consumer = std::make_unique<TYsonWriter>(
&countingStream,
- YsonFormat_,
+ Options_.YsonFormat,
EYsonType::Node,
- /*enableRaw*/ YsonFormat_ == EYsonFormat::Binary);
+ /*enableRaw*/ Options_.YsonFormat == EYsonFormat::Binary);
break;
default:
YT_ABORT();
@@ -105,23 +106,40 @@ i64 TStructuredLogFormatter::WriteFormatted(IOutputStream* stream, const TLogEve
BuildYsonFluently(consumer.get())
.BeginMap()
- .DoFor(CommonFields_, [] (auto fluent, auto item) {
+ .DoFor(Options_.CommonFields, [] (auto fluent, auto item) {
fluent.Item(item.first).Value(item.second);
})
- .DoIf(event.MessageKind == ELogMessageKind::Structured, [&] (auto fluent) {
- fluent.Items(TYsonString(event.MessageRef, EYsonType::MapFragment));
+ .DoIf(std::holds_alternative<TStructuredLogEventPayload>(event.Payload), [&] (auto fluent) {
+ fluent.Items(TYsonString(GetYsonFromStructuredPayload(std::get<TStructuredLogEventPayload>(event.Payload))));
})
- .DoIf(event.MessageKind == ELogMessageKind::Unstructured, [&] (auto fluent) {
- fluent.Item("message").Value(event.MessageRef.ToStringBuf());
+ .DoIf(std::holds_alternative<TTaggedLogEventPayload>(event.Payload), [&] (auto fluent) {
+ const auto& taggedPayload = std::get<TTaggedLogEventPayload>(event.Payload);
+ if (Options_.EnableNativeTags) {
+ TTaggedPayloadReader reader(taggedPayload);
+ auto message = reader.ReadMessage();
+ auto firstTag = reader.TryReadTag();
+ fluent
+ .Item("message").Value(message)
+ .DoIf(firstTag.has_value(), [&] (auto fluent) {
+ fluent.Item("tags").DoMap([&] (auto fluent) {
+ for (auto tag = firstTag; tag; tag = reader.TryReadTag()) {
+ fluent.Item(tag->Key).Value(tag->Value);
+ }
+ });
+ });
+ } else {
+ // Fold the tags into the message: |Message (Key: Value, ...)|.
+ fluent.Item("message").Value(FormatTaggedPayload(taggedPayload));
+ }
})
- .DoIf(EnableSystemFields_, [&] (auto fluent) {
+ .DoIf(Options_.EnableSystemFields, [&] (auto fluent) {
fluent
.Item("instant").Value(dateTimeBuffer.GetBuffer())
.Item("level").Value(FormatEnum(event.Level))
.Item("category").Value(event.Category->Name);
})
// TODO(achulkov2): The presence of different system fields should be controller by a flag enum instead of multiple boolean options.
- .DoIf(EnableHostField_, [&] (auto fluent) {
+ .DoIf(Options_.EnableHostField, [&] (auto fluent) {
fluent.Item("host").Value(NNet::GetLocalHostName());
})
.DoIf(event.Family == ELogFamily::PlainText, [&] (auto fluent) {
@@ -139,7 +157,7 @@ i64 TStructuredLogFormatter::WriteFormatted(IOutputStream* stream, const TLogEve
.EndMap();
consumer->Flush();
- if (Format_ == ELogFormat::Yson) {
+ if (Options_.Format == ELogFormat::Yson) {
// In order to obtain proper list fragment, we must manually insert trailing semicolon in each line.
countingStream.Write(';');
}
diff --git a/yt/yt/core/logging/formatter.h b/yt/yt/core/logging/formatter.h
index df7c1a46acd..91d0569484b 100644
--- a/yt/yt/core/logging/formatter.h
+++ b/yt/yt/core/logging/formatter.h
@@ -6,6 +6,8 @@
#include <library/cpp/yt/logging/plain_text_formatter/formatter.h>
+#include <library/cpp/yt/yson_string/string.h>
+
namespace NYT::NLogging {
////////////////////////////////////////////////////////////////////////////////
@@ -20,25 +22,35 @@ struct ILogFormatter
////////////////////////////////////////////////////////////////////////////////
+struct TLogFormatterBaseOptions
+{
+ bool EnableSourceLocation = false;
+};
+
class TLogFormatterBase
: public ILogFormatter
{
protected:
- TLogFormatterBase(bool enableSourceLocation);
+ explicit TLogFormatterBase(TLogFormatterBaseOptions options);
bool IsSourceLocationEnabled() const;
private:
- const bool EnableSourceLocation_;
+ const TLogFormatterBaseOptions Options_;
};
////////////////////////////////////////////////////////////////////////////////
+struct TPlainTextLogFormatterOptions
+{
+ bool EnableSourceLocation = false;
+};
+
class TPlainTextLogFormatter
: public TLogFormatterBase
{
public:
- explicit TPlainTextLogFormatter(bool enableSourceLocation = false);
+ explicit TPlainTextLogFormatter(TPlainTextLogFormatterOptions options = {});
i64 WriteFormatted(IOutputStream* outputStream, const TLogEvent& event) override;
void WriteLogReopenSeparator(IOutputStream* outputStream) override;
@@ -50,29 +62,30 @@ private:
////////////////////////////////////////////////////////////////////////////////
+struct TStructuredLogFormatterOptions
+{
+ ELogFormat Format = ELogFormat::Json;
+ //! Fields injected into every event as |key -> raw YSON value|.
+ THashMap<std::string, NYson::TYsonString> CommonFields;
+ bool EnableSourceLocation = false;
+ bool EnableSystemFields = true;
+ bool EnableHostField = false;
+ bool EnableNativeTags = false;
+ NJson::TJsonFormatConfigPtr JsonFormat;
+ NYson::EYsonFormat YsonFormat = NYson::EYsonFormat::Text;
+};
+
class TStructuredLogFormatter
: public TLogFormatterBase
{
public:
- TStructuredLogFormatter(
- ELogFormat format,
- THashMap<std::string, NYTree::INodePtr> commonFields,
- bool enableSourceLocation = false,
- bool enableSystemFields = true,
- bool enableHostField = false,
- NJson::TJsonFormatConfigPtr jsonFormat = nullptr,
- NYson::EYsonFormat ysonFormat = NYson::EYsonFormat::Text);
+ explicit TStructuredLogFormatter(TStructuredLogFormatterOptions options);
i64 WriteFormatted(IOutputStream* outputStream, const TLogEvent& event) override;
void WriteLogReopenSeparator(IOutputStream* outputStream) override;
private:
- const ELogFormat Format_;
- const THashMap<std::string, NYTree::INodePtr> CommonFields_;
- const bool EnableSystemFields_;
- const bool EnableHostField_;
- const NJson::TJsonFormatConfigPtr JsonFormat_;
- const NYson::EYsonFormat YsonFormat_;
+ const TStructuredLogFormatterOptions Options_;
TCachingDateFormatter CachingDateFormatter_;
};
diff --git a/yt/yt/core/logging/log-inl.h b/yt/yt/core/logging/log-inl.h
index 52178a5ab75..076d3c28d32 100644
--- a/yt/yt/core/logging/log-inl.h
+++ b/yt/yt/core/logging/log-inl.h
@@ -20,11 +20,13 @@ TLogMessage BuildLogMessage(
TFormatString<TArgs...> format,
TArgs&&... args)
{
- TMessageStringBuilder builder;
- AppendLogMessageWithFormat(&builder, loggingContext, logger, format.Get(), std::forward<TArgs>(args)...);
- builder.AppendChar('\n');
- FormatValue(&builder, error, TStringBuf("v"));
- return {builder.Flush(), format.Get()};
+ TTaggedPayloadWriter writer;
+ auto* builder = writer.BeginMessage();
+ AppendLogMessageWithFormat(builder, loggingContext, logger, format.Get(), std::forward<TArgs>(args)...);
+ builder->AppendChar('\n');
+ FormatValue(builder, error, TStringBuf("v"));
+ writer.EndMessage();
+ return {writer.Finish(), format.Get()};
}
} // namespace NDetail
diff --git a/yt/yt/core/logging/log_manager.cpp b/yt/yt/core/logging/log_manager.cpp
index bda678f2d63..aa41d1fb4e0 100644
--- a/yt/yt/core/logging/log_manager.cpp
+++ b/yt/yt/core/logging/log_manager.cpp
@@ -10,6 +10,9 @@
#include "stream_log_writer.h"
#include "system_log_event_provider.h"
+#include <library/cpp/yt/logging/private.h>
+#include <library/cpp/yt/logging/tagged_payload.h>
+
#include <yt/yt/core/concurrency/helpers.h>
#include <yt/yt/core/concurrency/periodic_executor.h>
#include <yt/yt/core/concurrency/scheduler_thread.h>
@@ -417,7 +420,7 @@ public:
// Limit message length.
TRawFormatter<1024> truncatedMessage;
- truncatedMessage.AppendString(event.MessageRef.ToStringBuf());
+ truncatedMessage.AppendString(GetMessageFromTaggedPayload(std::get<TTaggedLogEventPayload>(event.Payload)));
// NB(coteeq): It's safe to save event.SourceFile since it was
// generated by __LOCATION__ macro.
@@ -701,19 +704,22 @@ private:
switch (writerConfig->Format) {
case ELogFormat::PlainText:
- return std::make_unique<TPlainTextLogFormatter>(
- writerConfig->EnableSourceLocation);
+ return std::make_unique<TPlainTextLogFormatter>(TPlainTextLogFormatterOptions{
+ .EnableSourceLocation = writerConfig->EnableSourceLocation,
+ });
case ELogFormat::Json: [[fallthrough]];
case ELogFormat::Yson:
- return std::make_unique<TStructuredLogFormatter>(
- writerConfig->Format,
- writerConfig->CommonFields,
- writerConfig->EnableSourceLocation,
- writerConfig->EnableSystemFields,
- writerConfig->EnableHostField,
- writerConfig->JsonFormat,
- writerConfig->YsonFormat);
+ return std::make_unique<TStructuredLogFormatter>(TStructuredLogFormatterOptions{
+ .Format = writerConfig->Format,
+ .CommonFields = writerConfig->CommonFields,
+ .EnableSourceLocation = writerConfig->EnableSourceLocation,
+ .EnableSystemFields = writerConfig->EnableSystemFields,
+ .EnableHostField = writerConfig->EnableHostField,
+ .EnableNativeTags = writerConfig->EnableNativeTags,
+ .JsonFormat = writerConfig->JsonFormat,
+ .YsonFormat = writerConfig->YsonFormat,
+ });
default:
YT_ABORT();
@@ -815,7 +821,9 @@ private:
if (event.Anchor) {
event.Anchor->MessageCounter.Current += 1;
- event.Anchor->ByteCounter.Current += std::ssize(event.MessageRef);
+ event.Anchor->ByteCounter.Current += std::visit(
+ [] (const auto& payload) { return std::ssize(payload.Underlying()); },
+ event.Payload);
}
for (const auto& writer : GetWriters(config, event)) {
diff --git a/yt/yt/core/logging/system_log_event_provider.cpp b/yt/yt/core/logging/system_log_event_provider.cpp
index 9bec1d61515..bef41ce1c76 100644
--- a/yt/yt/core/logging/system_log_event_provider.cpp
+++ b/yt/yt/core/logging/system_log_event_provider.cpp
@@ -6,6 +6,8 @@
#include <yt/yt/core/ytree/fluent.h>
#include <library/cpp/yt/logging/logger.h>
+#include <library/cpp/yt/logging/tagged_payload.h>
+#include <library/cpp/yt/logging/structured_payload.h>
namespace NYT::NLogging {
@@ -47,8 +49,7 @@ public:
return TLogEvent{
.Category = Logger().GetCategory(),
.Level = ELogLevel::Info,
- .MessageKind = ELogMessageKind::Unstructured,
- .MessageRef = TSharedRef::FromString(Format("Logging started (Version: %v, BuildHost: %v, BuildTime: %v)",
+ .Payload = MakeTaggedPayloadFromMessage(Format("Logging started (Version: %v, BuildHost: %v, BuildTime: %v)",
GetVersion(),
GetBuildHost(),
GetBuildTime())),
@@ -65,8 +66,7 @@ public:
return TLogEvent{
.Category = Logger().GetCategory(),
.Level = ELogLevel::Info,
- .MessageKind = ELogMessageKind::Unstructured,
- .MessageRef = TSharedRef::FromString(Format("Skipped log records in last second (Count: %v, SkippedBy: %v)",
+ .Payload = MakeTaggedPayloadFromMessage(Format("Skipped log records in last second (Count: %v, SkippedBy: %v)",
count,
skippedBy)),
.Instant = GetCpuInstant(),
@@ -89,14 +89,12 @@ public:
return TLogEvent{
.Category = Logger().GetCategory(),
.Level = ELogLevel::Info,
- .MessageKind = ELogMessageKind::Structured,
- .MessageRef = BuildYsonStringFluently<NYson::EYsonType::MapFragment>()
+ .Payload = MakeStructuredPayloadFromYson(BuildYsonStringFluently<NYson::EYsonType::MapFragment>()
.Item("message").Value("Logging started")
.Item("version").Value(GetVersion())
.Item("build_host").Value(GetBuildHost())
.Item("build_time").Value(GetBuildTime())
- .Finish()
- .ToSharedRef(),
+ .Finish()),
.Instant = GetCpuInstant(),
};
}
@@ -110,13 +108,11 @@ public:
return TLogEvent{
.Category = Logger().GetCategory(),
.Level = ELogLevel::Info,
- .MessageKind = ELogMessageKind::Structured,
- .MessageRef = BuildYsonStringFluently<NYson::EYsonType::MapFragment>()
+ .Payload = MakeStructuredPayloadFromYson(BuildYsonStringFluently<NYson::EYsonType::MapFragment>()
.Item("message").Value("Events skipped")
.Item("skipped_by").Value(skippedBy)
.Item("events_skipped").Value(count)
- .Finish()
- .ToSharedRef(),
+ .Finish()),
.Instant = GetCpuInstant(),
};
}
diff --git a/yt/yt/core/logging/unittests/logging_ut.cpp b/yt/yt/core/logging/unittests/logging_ut.cpp
index ce9c6c006aa..e4c03879b84 100644
--- a/yt/yt/core/logging/unittests/logging_ut.cpp
+++ b/yt/yt/core/logging/unittests/logging_ut.cpp
@@ -20,6 +20,9 @@
#include <yt/yt/core/logging/system_log_event_provider.h>
#include <yt/yt/core/logging/zstd_log_codec.h>
+#include <library/cpp/yt/logging/tagged_payload.h>
+#include <library/cpp/yt/logging/structured_payload.h>
+
#include <yt/yt/core/misc/fs.h>
#include <yt/yt/core/test_framework/framework.h>
@@ -55,6 +58,8 @@
namespace NYT::NLogging {
namespace {
+using namespace NDetail;
+
using namespace NYTree;
using namespace NConcurrency;
using namespace NYson;
@@ -63,6 +68,26 @@ using namespace NCoreDump;
////////////////////////////////////////////////////////////////////////////////
+void WriteMessage(TTaggedPayloadWriter* writer, TStringBuf message)
+{
+ writer->BeginMessage()->AppendString(message);
+ writer->EndMessage();
+}
+
+void WriteTag(TTaggedPayloadWriter* writer, TStringBuf key, TStringBuf value)
+{
+ writer->BeginTag(key)->AppendString(value);
+ writer->EndTag();
+}
+
+void WriteWellKnownTag(TTaggedPayloadWriter* writer, TStringBuf key, TStringBuf value)
+{
+ writer->BeginWellKnownTag(key)->AppendString(value);
+ writer->EndTag();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
YT_DEFINE_GLOBAL(NLogging::TLogger, Logger, "Test");
std::string GenerateLogFileName()
@@ -98,13 +123,13 @@ protected:
void WritePlainTextEvent(const ILogWriterPtr& writer)
{
- TLogEvent event;
- event.Family = ELogFamily::PlainText;
- event.Category = Logger().GetCategory();
- event.Level = ELogLevel::Debug;
- event.MessageRef = TSharedRef::FromString(std::string("message"));
- event.MessageKind = ELogMessageKind::Unstructured;
- event.ThreadId = 0xba;
+ TLogEvent event{
+ .Category = Logger().GetCategory(),
+ .Level = ELogLevel::Debug,
+ .Family = ELogFamily::PlainText,
+ .Payload = MakeTaggedPayloadFromMessage("message"),
+ .ThreadId = 0xba,
+ };
WriteEvent(writer, event);
}
@@ -530,16 +555,16 @@ TEST_F(TLoggingTest, ThreadMinLogLevel)
TEST_F(TLoggingTest, PlainTextLoggingStructuredFormatter)
{
- TLogEvent event;
- event.Family = ELogFamily::PlainText;
- event.Category = Logger().GetCategory();
- event.Level = ELogLevel::Debug;
- event.MessageRef = TSharedRef::FromString(std::string("test_message"));
- event.MessageKind = ELogMessageKind::Unstructured;
- event.FiberId = 31;
- event.TraceId = TGuid(1, 2, 3, 4);
- event.SourceFile = "a/b.cpp";
- event.SourceLine = 123;
+ TLogEvent event{
+ .Category = Logger().GetCategory(),
+ .Level = ELogLevel::Debug,
+ .Family = ELogFamily::PlainText,
+ .Payload = MakeTaggedPayloadFromMessage("test_message"),
+ .FiberId = 31,
+ .TraceId = TTraceId(1, 2, 3, 4),
+ .SourceFile = "a/b.cpp",
+ .SourceLine = 123,
+ };
for (auto enableSourceLocation : {false, true}) {
for (auto format : {ELogFormat::Yson, ELogFormat::Json}) {
@@ -549,7 +574,7 @@ TEST_F(TLoggingTest, PlainTextLoggingStructuredFormatter)
writerConfig->FileName = logFile.Name();
auto writer = CreateFileLogWriter(
- std::make_unique<TStructuredLogFormatter>(format, THashMap<std::string, INodePtr>{}, enableSourceLocation),
+ std::make_unique<TStructuredLogFormatter>(TStructuredLogFormatterOptions{.Format = format, .EnableSourceLocation = enableSourceLocation}),
CreateDefaultSystemLogEventProvider(writerConfig),
"test_writer",
writerConfig,
@@ -579,18 +604,16 @@ TEST_F(TLoggingTest, PlainTextLoggingStructuredFormatter)
TEST_F(TLoggingTest, StructuredLogging)
{
- TLogEvent event;
- event.Family = ELogFamily::Structured;
- event.Category = Logger().GetCategory();
- event.Level = ELogLevel::Debug;
- event.MessageRef = BuildYsonStringFluently<EYsonType::MapFragment>()
- .Item("message").Value("test_message")
- .Finish()
- .ToSharedRef();
- event.MessageKind = ELogMessageKind::Structured;
-
- event.FiberId = 31;
- event.TraceId = TGuid(1, 2, 3, 4);
+ TLogEvent event{
+ .Category = Logger().GetCategory(),
+ .Level = ELogLevel::Debug,
+ .Family = ELogFamily::Structured,
+ .Payload = MakeStructuredPayloadFromYson(BuildYsonStringFluently<EYsonType::MapFragment>()
+ .Item("message").Value("test_message")
+ .Finish()),
+ .FiberId = 31,
+ .TraceId = TTraceId(1, 2, 3, 4),
+ };
for (auto format : {ELogFormat::Yson, ELogFormat::Json}) {
TTempFile logFile(GenerateLogFileName());
@@ -599,7 +622,7 @@ TEST_F(TLoggingTest, StructuredLogging)
writerConfig->FileName = logFile.Name();
auto writer = CreateFileLogWriter(
- std::make_unique<TStructuredLogFormatter>(format, THashMap<std::string, INodePtr>{}),
+ std::make_unique<TStructuredLogFormatter>(TStructuredLogFormatterOptions{.Format = format}),
CreateDefaultSystemLogEventProvider(writerConfig),
"test_writer",
writerConfig,
@@ -728,14 +751,14 @@ TEST_F(TLoggingTest, SlogMacroConstexprLogging)
}
}
-TEST_F(TLoggingTest, UnstructuredLogging)
+TEST_F(TLoggingTest, TaggedLogging)
{
- TLogEvent event;
- event.Family = ELogFamily::Structured;
- event.Category = Logger().GetCategory();
- event.Level = ELogLevel::Debug;
- event.MessageRef = TSharedRef::FromString(std::string("test_message"));
- event.MessageKind = ELogMessageKind::Unstructured;
+ TLogEvent event{
+ .Category = Logger().GetCategory(),
+ .Level = ELogLevel::Debug,
+ .Family = ELogFamily::Structured,
+ .Payload = MakeTaggedPayloadFromMessage("test_message"),
+ };
for (auto format : {ELogFormat::Yson, ELogFormat::Json}) {
TTempFile logFile(GenerateLogFileName());
@@ -744,7 +767,7 @@ TEST_F(TLoggingTest, UnstructuredLogging)
writerConfig->FileName = logFile.Name();
auto writer = CreateFileLogWriter(
- std::make_unique<TStructuredLogFormatter>(format, THashMap<std::string, INodePtr>{}),
+ std::make_unique<TStructuredLogFormatter>(TStructuredLogFormatterOptions{.Format = format}),
CreateDefaultSystemLogEventProvider(writerConfig),
"test_writer",
writerConfig,
@@ -768,17 +791,16 @@ TEST_F(TLoggingTest, StructuredLoggingJsonFormat)
std::string longString(1000, 'a');
std::string longStringPrefix(100, 'a');
- TLogEvent event;
- event.Family = ELogFamily::Structured;
- event.Category = Logger().GetCategory();
- event.Level = ELogLevel::Debug;
- event.MessageRef = BuildYsonStringFluently<EYsonType::MapFragment>()
- .Item("message").Value("test_message")
- .Item("nan_value").Value(std::nan("1"))
- .Item("long_string_value").Value(longString)
- .Finish()
- .ToSharedRef();
- event.MessageKind = ELogMessageKind::Structured;
+ TLogEvent event{
+ .Category = Logger().GetCategory(),
+ .Level = ELogLevel::Debug,
+ .Family = ELogFamily::Structured,
+ .Payload = MakeStructuredPayloadFromYson(BuildYsonStringFluently<EYsonType::MapFragment>()
+ .Item("message").Value("test_message")
+ .Item("nan_value").Value(std::nan("1"))
+ .Item("long_string_value").Value(longString)
+ .Finish()),
+ };
auto jsonFormat = New<TJsonFormatConfig>();
jsonFormat->StringifyNanAndInfinity = true;
@@ -789,13 +811,10 @@ TEST_F(TLoggingTest, StructuredLoggingJsonFormat)
auto writerConfig = New<TFileLogWriterConfig>();
writerConfig->FileName = logFile.Name();
- auto formatter = std::make_unique<TStructuredLogFormatter>(
- ELogFormat::Json,
- /*commonFields*/ THashMap<std::string, INodePtr>{},
- /*enableSourceLocation*/ false,
- /*enableSystemFields*/ true,
- /*enableHostField*/ false,
- jsonFormat);
+ auto formatter = std::make_unique<TStructuredLogFormatter>(TStructuredLogFormatterOptions{
+ .Format = ELogFormat::Json,
+ .JsonFormat = jsonFormat,
+ });
auto writer = CreateFileLogWriter(
std::move(formatter),
@@ -943,22 +962,20 @@ TEST_F(TLoggingTest, StructuredValidationWithSamplingRate)
TEST_F(TLoggingTest, StructuredLoggingDisableSystemFields)
{
- TLogEvent event;
- event.Family = ELogFamily::Structured;
- event.Category = Logger().GetCategory();
- event.Level = ELogLevel::Debug;
- event.MessageRef = BuildYsonStringFluently<EYsonType::MapFragment>()
- .Item("message").Value("test_message")
- .Finish()
- .ToSharedRef();
- event.MessageKind = ELogMessageKind::Structured;
+ TLogEvent event{
+ .Category = Logger().GetCategory(),
+ .Level = ELogLevel::Debug,
+ .Family = ELogFamily::Structured,
+ .Payload = MakeStructuredPayloadFromYson(BuildYsonStringFluently<EYsonType::MapFragment>()
+ .Item("message").Value("test_message")
+ .Finish()),
+ };
- auto formatter = std::make_unique<TStructuredLogFormatter>(
- ELogFormat::Yson,
- /*commonFields*/ THashMap<std::string, INodePtr>{},
- /*enableControlMessages*/ true,
- /*enableSourceLocation*/ false,
- /*enableSystemFields*/ false);
+ auto formatter = std::make_unique<TStructuredLogFormatter>(TStructuredLogFormatterOptions{
+ .Format = ELogFormat::Yson,
+ .EnableSourceLocation = false,
+ .EnableSystemFields = false,
+ });
TStringStream stringStream;
formatter->WriteFormatted(&stringStream, event);
@@ -971,6 +988,63 @@ TEST_F(TLoggingTest, StructuredLoggingDisableSystemFields)
EXPECT_EQ(message->FindChild("category"), nullptr);
}
+TEST_F(TLoggingTest, PlainTextFormatterTags)
+{
+ TTaggedPayloadWriter writer;
+ WriteMessage(&writer, "Message");
+ WriteTag(&writer, "Key1", "Value1");
+ WriteTag(&writer, "Key2", "Value2");
+
+ TLogEvent event{
+ .Category = Logger().GetCategory(),
+ .Level = ELogLevel::Debug,
+ .Family = ELogFamily::PlainText,
+ .Payload = writer.Finish(),
+ .ThreadId = 42,
+ .FiberId = 31,
+ .TraceId = TTraceId(1, 2, 3, 4),
+ };
+
+ auto formatter = std::make_unique<TPlainTextLogFormatter>();
+ TStringStream stringStream;
+ formatter->WriteFormatted(&stringStream, event);
+
+ // The tab-separated fields are: <date> <level> <category> <message> <thread> <fiber> <trace>.
+ EXPECT_EQ(
+ stringStream.Str().substr(DateLength),
+ Format("\tD\t%v\tMessage (Key1: Value1, Key2: Value2)\t2a\t1f\t4-3-2-1\n", Logger().GetCategory()->Name));
+}
+
+TEST_F(TLoggingTest, PlainTextFormatterWellKnownTag)
+{
+ TTaggedPayloadWriter writer;
+ WriteMessage(&writer, "Message");
+ WriteTag(&writer, "Key", "Value");
+ // A well-known tag (e.g. an error) with a newline in its value.
+ WriteWellKnownTag(&writer, "Error", "a\nb");
+
+ TLogEvent event{
+ .Category = Logger().GetCategory(),
+ .Level = ELogLevel::Debug,
+ .Family = ELogFamily::PlainText,
+ .Payload = writer.Finish(),
+ .ThreadId = 42,
+ .FiberId = 31,
+ .TraceId = TTraceId(1, 2, 3, 4),
+ };
+
+ auto formatter = std::make_unique<TPlainTextLogFormatter>();
+ TStringStream stringStream;
+ formatter->WriteFormatted(&stringStream, event);
+
+ // The regular tag renders inline; the well-known tag is appended after the |(...)|
+ // group, separated by an escaped newline; its value's own newline is escaped too,
+ // so the record stays a single physical line.
+ EXPECT_EQ(
+ stringStream.Str().substr(DateLength),
+ Format("\tD\t%v\tMessage (Key: Value)\\na\\nb\t2a\t1f\t4-3-2-1\n", Logger().GetCategory()->Name));
+}
+
TEST_F(TLoggingTest, WithMinLevel)
{
TTempFile debugFile(GenerateLogFileName());
@@ -1551,18 +1625,18 @@ TEST_P(TLoggingTagsTest, All)
if (hasMessageTag) {
EXPECT_EQ(
expected,
- ToString(NLogging::NDetail::BuildLogMessage(
+ ToString(GetMessageFromTaggedPayload(NLogging::BuildLogMessage(
loggingContext,
logger,
"Log message (Value: %v)",
- 123).MessageRef));
+ 123).Payload)));
} else {
EXPECT_EQ(
expected,
- ToString(NLogging::NDetail::BuildLogMessage(
+ ToString(GetMessageFromTaggedPayload(NLogging::BuildLogMessage(
loggingContext,
logger,
- "Log message").MessageRef));
+ "Log message").Payload)));
}
}
@@ -1654,7 +1728,7 @@ TEST_F(TLongMessagesTest, WithoutPerThreadCache)
TTempFile logFile(GenerateLogFileName());
ConfigureForLongMessages(logFile.Name());
std::thread thread([&] {
- NLogging::NDetail::TMessageStringBuilder::DisablePerThreadCache();
+ TTaggedPayloadWriter::DisablePerThreadCache();
LogLongMessages();
});
thread.join();
@@ -1665,8 +1739,8 @@ TEST_F(TLoggingTest, Anchors)
{
NLogging::TLogger logger;
NLogging::TLoggingContext context{};
- EXPECT_EQ(NLogging::NDetail::BuildLogMessage(context, logger, "Simple message").Anchor, "Simple message");
- EXPECT_EQ(NLogging::NDetail::BuildLogMessage(context, logger, "Simple message (Param: %v)", 1).Anchor, "Simple message (Param: %v)");
+ EXPECT_EQ(NLogging::BuildLogMessage(context, logger, "Simple message").Anchor, "Simple message");
+ EXPECT_EQ(NLogging::BuildLogMessage(context, logger, "Simple message (Param: %v)", 1).Anchor, "Simple message (Param: %v)");
}
////////////////////////////////////////////////////////////////////////////////
@@ -1703,7 +1777,7 @@ public:
void Write(const TLogEvent& event) override
{
if (event.Category == Logger().GetCategory()) {
- Messages_.push_back(std::string(Config_->Padding, ' ') + std::string(event.MessageRef.ToStringBuf()));
+ Messages_.push_back(std::string(Config_->Padding, ' ') + std::string(GetMessageFromTaggedPayload(std::get<TTaggedLogEventPayload>(event.Payload))));
}
}
diff --git a/yt/yt/core/ytree/serialize-inl.h b/yt/yt/core/ytree/serialize-inl.h
index d73c02fe37e..d71395b34c1 100644
--- a/yt/yt/core/ytree/serialize-inl.h
+++ b/yt/yt/core/ytree/serialize-inl.h
@@ -8,6 +8,7 @@
#include <yt/yt/core/misc/error.h>
#include <yt/yt/core/misc/collection_helpers.h>
+#include <yt/yt/core/misc/protobuf_helpers.h>
#include <yt/yt/core/yson/stream.h>
#include <yt/yt/core/yson/string.h>
@@ -517,7 +518,7 @@ void Serialize(
const T& message,
NYson::IYsonConsumer* consumer)
{
- consumer->OnStringScalar(message.SerializeAsStringOrThrow());
+ consumer->OnStringScalar(SerializeProtoToString(message));
}
template <class T, class TTag, TStrongTypedefOptions Options>
@@ -746,7 +747,7 @@ void Deserialize(
{
std::string string;
Deserialize(string, node);
- message.ParseFromStringOrThrow(string);
+ DeserializeProto(&message, TRef::FromString(string));
}
template <class T, class TTag, TStrongTypedefOptions Options>
diff --git a/yt/yt_proto/yt/client/node_tracker_client/proto/node.proto b/yt/yt_proto/yt/client/node_tracker_client/proto/node.proto
index 42ba562054c..1058837e54a 100644
--- a/yt/yt_proto/yt/client/node_tracker_client/proto/node.proto
+++ b/yt/yt_proto/yt/client/node_tracker_client/proto/node.proto
@@ -97,6 +97,7 @@ message TNetworkStatistics
{
required string network = 1;
required bool throttling_reads = 2 [default = false];
+ optional bool throttling_writes = 3 [default = false];
}
// Describes the current state of the whole node.